From 4bc4254209c9e3c5e672331bbf4ebae5421c6292 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 15:32:32 +0200 Subject: [PATCH 001/215] #1394 copy privileged starter code from the old branch --- base/build.gradle.kts | 1 + priv/.gitignore | 2 + priv/build.gradle.kts | 70 ++++ {shizuku => priv}/consumer-rules.pro | 0 {shizuku => priv}/proguard-rules.pro | 0 .../src/main/AndroidManifest.xml | 0 priv/src/main/cpp/CMakeLists.txt | 86 ++++ priv/src/main/cpp/adb_pairing.cpp | 230 ++++++++++ priv/src/main/cpp/adb_pairing.h | 4 + priv/src/main/cpp/android.cpp | 30 ++ priv/src/main/cpp/android.h | 8 + priv/src/main/cpp/cgroup.cpp | 73 ++++ priv/src/main/cpp/cgroup.h | 9 + priv/src/main/cpp/helper.cpp | 73 ++++ priv/src/main/cpp/logging.h | 30 ++ priv/src/main/cpp/misc.cpp | 195 +++++++++ priv/src/main/cpp/misc.h | 14 + priv/src/main/cpp/privstarter.cpp | 9 + priv/src/main/cpp/selinux.cpp | 105 +++++ priv/src/main/cpp/selinux.h | 21 + priv/src/main/cpp/starter.cpp | 326 ++++++++++++++ .../sds100/keymapper/priv/adb/AdbClient.kt | 189 +++++++++ .../sds100/keymapper/priv/adb/AdbException.kt | 16 + .../sds100/keymapper/priv/adb/AdbKey.kt | 396 ++++++++++++++++++ .../sds100/keymapper/priv/adb/AdbMdns.kt | 135 ++++++ .../sds100/keymapper/priv/adb/AdbMessage.kt | 132 ++++++ .../keymapper/priv/adb/AdbPairingClient.kt | 331 +++++++++++++++ .../keymapper/priv/adb/AdbPairingService.kt | 368 ++++++++++++++++ .../sds100/keymapper/priv/adb/AdbProtocol.kt | 22 + .../sds100/keymapper/priv/ktx/Context.kt | 21 + .../github/sds100/keymapper/priv/ktx/Log.kt | 24 ++ .../sds100/keymapper/priv/starter/Starter.kt | 138 ++++++ priv/src/main/res/raw/start.sh | 51 +++ priv/src/main/res/values/strings.xml | 169 ++++++++ settings.gradle.kts | 2 +- shizuku/.gitignore | 1 - shizuku/build.gradle.kts | 42 -- .../shizuku/ExampleInstrumentedTest.kt | 24 -- .../keymapper/shizuku/ExampleUnitTest.kt | 17 - 39 files changed, 3279 insertions(+), 85 deletions(-) create mode 100644 priv/.gitignore create mode 100644 priv/build.gradle.kts rename {shizuku => priv}/consumer-rules.pro (100%) rename {shizuku => priv}/proguard-rules.pro (100%) rename {shizuku => priv}/src/main/AndroidManifest.xml (100%) create mode 100644 priv/src/main/cpp/CMakeLists.txt create mode 100644 priv/src/main/cpp/adb_pairing.cpp create mode 100644 priv/src/main/cpp/adb_pairing.h create mode 100644 priv/src/main/cpp/android.cpp create mode 100644 priv/src/main/cpp/android.h create mode 100644 priv/src/main/cpp/cgroup.cpp create mode 100644 priv/src/main/cpp/cgroup.h create mode 100644 priv/src/main/cpp/helper.cpp create mode 100644 priv/src/main/cpp/logging.h create mode 100644 priv/src/main/cpp/misc.cpp create mode 100644 priv/src/main/cpp/misc.h create mode 100644 priv/src/main/cpp/privstarter.cpp create mode 100644 priv/src/main/cpp/selinux.cpp create mode 100644 priv/src/main/cpp/selinux.h create mode 100644 priv/src/main/cpp/starter.cpp create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/starter/Starter.kt create mode 100644 priv/src/main/res/raw/start.sh create mode 100644 priv/src/main/res/values/strings.xml delete mode 100644 shizuku/.gitignore delete mode 100644 shizuku/build.gradle.kts delete mode 100644 shizuku/src/androidTest/java/io/github/sds100/keymapper/shizuku/ExampleInstrumentedTest.kt delete mode 100644 shizuku/src/test/java/io/github/sds100/keymapper/shizuku/ExampleUnitTest.kt diff --git a/base/build.gradle.kts b/base/build.gradle.kts index 86e093e95d..2d0d758eed 100644 --- a/base/build.gradle.kts +++ b/base/build.gradle.kts @@ -73,6 +73,7 @@ android { dependencies { implementation(project(":common")) implementation(project(":data")) + implementation(project(":priv")) implementation(project(":system")) implementation(project(":systemstubs")) diff --git a/priv/.gitignore b/priv/.gitignore new file mode 100644 index 0000000000..c591fdeb45 --- /dev/null +++ b/priv/.gitignore @@ -0,0 +1,2 @@ +/build +.cxx \ No newline at end of file diff --git a/priv/build.gradle.kts b/priv/build.gradle.kts new file mode 100644 index 0000000000..826716325c --- /dev/null +++ b/priv/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "io.github.sds100.keymapper.priv" + compileSdk = libs.versions.compile.sdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.min.sdk.get().toInt() + + 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 { + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + buildFeatures { + prefab = true + } + + 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/shizuku/consumer-rules.pro b/priv/consumer-rules.pro similarity index 100% rename from shizuku/consumer-rules.pro rename to priv/consumer-rules.pro diff --git a/shizuku/proguard-rules.pro b/priv/proguard-rules.pro similarity index 100% rename from shizuku/proguard-rules.pro rename to priv/proguard-rules.pro diff --git a/shizuku/src/main/AndroidManifest.xml b/priv/src/main/AndroidManifest.xml similarity index 100% rename from shizuku/src/main/AndroidManifest.xml rename to priv/src/main/AndroidManifest.xml diff --git a/priv/src/main/cpp/CMakeLists.txt b/priv/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..f9d9670ce2 --- /dev/null +++ b/priv/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/priv/src/main/cpp/adb_pairing.cpp b/priv/src/main/cpp/adb_pairing.cpp new file mode 100644 index 0000000000..1a44981e28 --- /dev/null +++ b/priv/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/priv/src/main/cpp/adb_pairing.h b/priv/src/main/cpp/adb_pairing.h new file mode 100644 index 0000000000..3f9eb70a66 --- /dev/null +++ b/priv/src/main/cpp/adb_pairing.h @@ -0,0 +1,4 @@ +#ifndef ADB_H +#define ADB_H + +#endif // ADB_H diff --git a/priv/src/main/cpp/android.cpp b/priv/src/main/cpp/android.cpp new file mode 100644 index 0000000000..4bb6c44e0e --- /dev/null +++ b/priv/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/priv/src/main/cpp/android.h b/priv/src/main/cpp/android.h new file mode 100644 index 0000000000..a4b78fba96 --- /dev/null +++ b/priv/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/priv/src/main/cpp/cgroup.cpp b/priv/src/main/cpp/cgroup.cpp new file mode 100644 index 0000000000..c1015b72f5 --- /dev/null +++ b/priv/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/priv/src/main/cpp/cgroup.h b/priv/src/main/cpp/cgroup.h new file mode 100644 index 0000000000..361a0b1919 --- /dev/null +++ b/priv/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/priv/src/main/cpp/helper.cpp b/priv/src/main/cpp/helper.cpp new file mode 100644 index 0000000000..1001f8316d --- /dev/null +++ b/priv/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/priv/src/main/cpp/logging.h b/priv/src/main/cpp/logging.h new file mode 100644 index 0000000000..91a750f4a0 --- /dev/null +++ b/priv/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/priv/src/main/cpp/misc.cpp b/priv/src/main/cpp/misc.cpp new file mode 100644 index 0000000000..e7d2f8ccd7 --- /dev/null +++ b/priv/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/priv/src/main/cpp/misc.h b/priv/src/main/cpp/misc.h new file mode 100644 index 0000000000..3ab3e87d4a --- /dev/null +++ b/priv/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/priv/src/main/cpp/privstarter.cpp b/priv/src/main/cpp/privstarter.cpp new file mode 100644 index 0000000000..303a24597b --- /dev/null +++ b/priv/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/priv/src/main/cpp/selinux.cpp b/priv/src/main/cpp/selinux.cpp new file mode 100644 index 0000000000..9b9ebc63cf --- /dev/null +++ b/priv/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/priv/src/main/cpp/selinux.h b/priv/src/main/cpp/selinux.h new file mode 100644 index 0000000000..f5e47eb291 --- /dev/null +++ b/priv/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/priv/src/main/cpp/starter.cpp b/priv/src/main/cpp/starter.cpp new file mode 100644 index 0000000000..372c154e79 --- /dev/null +++ b/priv/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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt new file mode 100644 index 0000000000..19b018636b --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt @@ -0,0 +1,189 @@ +package io.github.sds100.keymapper.priv.adb + +import android.os.Build +import android.util.Log +import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY +import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_SIGNATURE +import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_TOKEN +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_AUTH +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_CLSE +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_CNXN +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_MAXDATA +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_OKAY +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_OPEN +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_STLS +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_STLS_VERSION +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_VERSION +import io.github.sds100.keymapper.priv.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.Companion.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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt new file mode 100644 index 0000000000..df1f222487 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt @@ -0,0 +1,16 @@ +package io.github.sds100.keymapper.priv.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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt new file mode 100644 index 0000000000..b0b00be5ff --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt @@ -0,0 +1,396 @@ +package io.github.sds100.keymapper.priv.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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt new file mode 100644 index 0000000000..23995a7363 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt @@ -0,0 +1,135 @@ +package io.github.sds100.keymapper.priv.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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt new file mode 100644 index 0000000000..20aabce680 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt @@ -0,0 +1,132 @@ +package io.github.sds100.keymapper.priv.adb + +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_AUTH +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_CLSE +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_CNXN +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_OKAY +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_OPEN +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_STLS +import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_SYNC +import io.github.sds100.keymapper.priv.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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt new file mode 100644 index 0000000000..b306ceb9d2 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt @@ -0,0 +1,331 @@ +package io.github.sds100.keymapper.priv.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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt new file mode 100644 index 0000000000..02a62b9b58 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt @@ -0,0 +1,368 @@ +package io.github.sds100.keymapper.priv.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.priv.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import io.github.sds100.keymapper.priv.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(me.zhanghai.android.appiconloader.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(me.zhanghai.android.appiconloader.R.drawable.ic_instant_app_badge) + .setContentTitle("Searching") + .addAction(stopNotificationAction) + .build() + } + + private fun createInputNotification(port: Int): Notification { + return Notification.Builder(this, notificationChannel) + .setSmallIcon(me.zhanghai.android.appiconloader.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(me.zhanghai.android.appiconloader.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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt new file mode 100644 index 0000000000..91e7f68fff --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt @@ -0,0 +1,22 @@ +package io.github.sds100.keymapper.priv.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/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt new file mode 100644 index 0000000000..6405c697c4 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt @@ -0,0 +1,21 @@ +package io.github.sds100.keymapper.priv.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/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt new file mode 100644 index 0000000000..74cac54e85 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt @@ -0,0 +1,24 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package io.github.sds100.keymapper.priv.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/priv/src/main/java/io/github/sds100/keymapper/priv/starter/Starter.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/starter/Starter.kt new file mode 100644 index 0000000000..cb5cc94352 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/starter/Starter.kt @@ -0,0 +1,138 @@ +package io.github.sds100.keymapper.priv.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.priv.R +import io.github.sds100.keymapper.priv.ktx.createDeviceProtectedStorageContextCompat +import io.github.sds100.keymapper.priv.ktx.logd +import io.github.sds100.keymapper.priv.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/priv/src/main/res/raw/start.sh b/priv/src/main/res/raw/start.sh new file mode 100644 index 0000000000..815483b755 --- /dev/null +++ b/priv/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/priv/src/main/res/values/strings.xml b/priv/src/main/res/values/strings.xml new file mode 100644 index 0000000000..5e469e7d09 --- /dev/null +++ b/priv/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.kts b/settings.gradle.kts index 266e977f2e..0779070117 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,4 +30,4 @@ include(":api") include(":system") include(":common") include(":data") -include(":shizuku") +include(":priv") diff --git a/shizuku/.gitignore b/shizuku/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/shizuku/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/shizuku/build.gradle.kts b/shizuku/build.gradle.kts deleted file mode 100644 index 3b8d3dfb26..0000000000 --- a/shizuku/build.gradle.kts +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) -} - -android { - namespace = "io.github.sds100.keymapper.shizuku" - compileSdk = 35 - - defaultConfig { - minSdk = 21 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" - } -} - -dependencies { - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.google.android.material) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.test.ext.junit) - androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file diff --git a/shizuku/src/androidTest/java/io/github/sds100/keymapper/shizuku/ExampleInstrumentedTest.kt b/shizuku/src/androidTest/java/io/github/sds100/keymapper/shizuku/ExampleInstrumentedTest.kt deleted file mode 100644 index 44e338cfc3..0000000000 --- a/shizuku/src/androidTest/java/io/github/sds100/keymapper/shizuku/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.sds100.keymapper.shizuku - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("io.github.sds100.keymapper.shizuku.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/shizuku/src/test/java/io/github/sds100/keymapper/shizuku/ExampleUnitTest.kt b/shizuku/src/test/java/io/github/sds100/keymapper/shizuku/ExampleUnitTest.kt deleted file mode 100644 index c372bb7be5..0000000000 --- a/shizuku/src/test/java/io/github/sds100/keymapper/shizuku/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.sds100.keymapper.shizuku - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file From 48c8fe20f39b5e1584b22d1f9c391aeed9063f7d Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 17:11:10 +0200 Subject: [PATCH 002/215] #1394 create basic PrivService --- priv/build.gradle.kts | 16 +++++++- .../sds100/keymapper/priv/IPrivService.aidl | 10 +++++ priv/src/main/cpp/starter.cpp | 1 + .../sds100/keymapper/priv/ktx/Context.kt | 9 ---- .../github/sds100/keymapper/priv/ktx/Log.kt | 2 + .../keymapper/priv/service/PrivService.kt | 41 +++++++++++++++++++ systemstubs/build.gradle.kts | 2 + .../java/android/ddm/DdmHandleAppName.java | 8 ++++ .../com/android/org/conscrypt/Conscrypt.java | 28 +++++++++++++ 9 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 priv/src/main/aidl/io/github/sds100/keymapper/priv/IPrivService.aidl create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt create mode 100644 systemstubs/src/main/java/android/ddm/DdmHandleAppName.java create mode 100644 systemstubs/src/main/java/com/android/org/conscrypt/Conscrypt.java diff --git a/priv/build.gradle.kts b/priv/build.gradle.kts index 826716325c..aeee724809 100644 --- a/priv/build.gradle.kts +++ b/priv/build.gradle.kts @@ -31,7 +31,15 @@ android { } } + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + buildFeatures { + aidl = true prefab = true } @@ -55,10 +63,14 @@ android { } dependencies { + implementation(project(":systemstubs")) + + implementation(libs.jakewharton.timber) + // TODO use version catalog implementation("org.conscrypt:conscrypt-android:2.5.3") - implementation("androidx.core:core-ktx:1.16.0") - implementation("androidx.appcompat:appcompat:1.7.0") + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) // From Shizuku :manager module build.gradle file. implementation("io.github.vvb2060.ndk:boringssl:20250114") diff --git a/priv/src/main/aidl/io/github/sds100/keymapper/priv/IPrivService.aidl b/priv/src/main/aidl/io/github/sds100/keymapper/priv/IPrivService.aidl new file mode 100644 index 0000000000..12931cb84c --- /dev/null +++ b/priv/src/main/aidl/io/github/sds100/keymapper/priv/IPrivService.aidl @@ -0,0 +1,10 @@ +package io.github.sds100.keymapper.priv; + +interface IPrivService { + // 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/priv/src/main/cpp/starter.cpp b/priv/src/main/cpp/starter.cpp index 372c154e79..91689b8a29 100644 --- a/priv/src/main/cpp/starter.cpp +++ b/priv/src/main/cpp/starter.cpp @@ -30,6 +30,7 @@ #define EXIT_FATAL_KILL 9 #define EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX 10 +// TODO take package name as argument #define PACKAGE_NAME "io.github.sds100.keymapper.debug" #define SERVER_NAME "shizuku_server" #define SERVER_CLASS_PATH "io.github.sds100.keymapper.nativelib.EvdevService" diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt index 6405c697c4..459f0b0e1a 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.priv.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) { @@ -11,11 +10,3 @@ fun Context.createDeviceProtectedStorageContextCompat(): Context { 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/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt index 74cac54e85..918398e294 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt @@ -4,6 +4,8 @@ package io.github.sds100.keymapper.priv.ktx import android.util.Log +// TODO replace with Timber usage. + inline val T.TAG: String get() = T::class.java.simpleName.let { diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt new file mode 100644 index 0000000000..1a143b1166 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt @@ -0,0 +1,41 @@ +package io.github.sds100.keymapper.priv.service + +import android.annotation.SuppressLint +import android.ddm.DdmHandleAppName +import io.github.sds100.keymapper.priv.IPrivService +import timber.log.Timber +import kotlin.system.exitProcess + +class PrivService : IPrivService.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 = "PrivService" + + @JvmStatic + fun main(args: Array) { + DdmHandleAppName.setAppName("keymapper_evdev", 0) + PrivService() + } + } + + init { + @SuppressLint("UnsafeDynamicallyLoadedCode") + System.load("${System.getProperty("shizuku.library.path")}/libevdev.so") + stringFromJNI() + } + + override fun destroy() { + Timber.d("Destroy PrivService") + exitProcess(0) + } + + override fun sendEvent(): String? { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/systemstubs/build.gradle.kts b/systemstubs/build.gradle.kts index 98e0963f57..081785188e 100644 --- a/systemstubs/build.gradle.kts +++ b/systemstubs/build.gradle.kts @@ -36,4 +36,6 @@ android { } dependencies { + // TODO use version catalogs + implementation("androidx.annotation:annotation-jvm:1.9.1") } 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..14396b0421 --- /dev/null +++ b/systemstubs/src/main/java/com/android/org/conscrypt/Conscrypt.java @@ -0,0 +1,28 @@ +package com.android.org.conscrypt; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocket; + +import androidx.annotation.RequiresApi; + +@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"); + } +} From fdb6036a3b14a48a3cb7bd7b1755f283067fb53f Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 17:41:48 +0200 Subject: [PATCH 003/215] #1394 add libevdev code to priv module --- priv/build.gradle.kts | 39 + priv/src/main/cpp/CMakeLists.txt | 6 +- priv/src/main/cpp/libevdev/Makefile.am | 42 + priv/src/main/cpp/libevdev/event-names.h | 1656 ++++++++++++ priv/src/main/cpp/libevdev/libevdev-int.h | 317 +++ priv/src/main/cpp/libevdev/libevdev-names.c | 212 ++ .../main/cpp/libevdev/libevdev-uinput-int.h | 13 + priv/src/main/cpp/libevdev/libevdev-uinput.c | 494 ++++ priv/src/main/cpp/libevdev/libevdev-uinput.h | 255 ++ priv/src/main/cpp/libevdev/libevdev-util.h | 63 + priv/src/main/cpp/libevdev/libevdev.c | 1848 +++++++++++++ priv/src/main/cpp/libevdev/libevdev.h | 2387 +++++++++++++++++ priv/src/main/cpp/libevdev/libevdev.sym | 123 + .../src/main/cpp/libevdev/make-event-names.py | 231 ++ priv/src/main/cpp/privservice.cpp | 60 + .../keymapper/priv/service/PrivService.kt | 5 +- settings.gradle.kts | 1 + 17 files changed, 7750 insertions(+), 2 deletions(-) create mode 100644 priv/src/main/cpp/libevdev/Makefile.am create mode 100644 priv/src/main/cpp/libevdev/event-names.h create mode 100644 priv/src/main/cpp/libevdev/libevdev-int.h create mode 100644 priv/src/main/cpp/libevdev/libevdev-names.c create mode 100644 priv/src/main/cpp/libevdev/libevdev-uinput-int.h create mode 100644 priv/src/main/cpp/libevdev/libevdev-uinput.c create mode 100644 priv/src/main/cpp/libevdev/libevdev-uinput.h create mode 100644 priv/src/main/cpp/libevdev/libevdev-util.h create mode 100644 priv/src/main/cpp/libevdev/libevdev.c create mode 100644 priv/src/main/cpp/libevdev/libevdev.h create mode 100644 priv/src/main/cpp/libevdev/libevdev.sym create mode 100755 priv/src/main/cpp/libevdev/make-event-names.py create mode 100644 priv/src/main/cpp/privservice.cpp diff --git a/priv/build.gradle.kts b/priv/build.gradle.kts index aeee724809..89af198e13 100644 --- a/priv/build.gradle.kts +++ b/priv/build.gradle.kts @@ -79,4 +79,43 @@ dependencies { 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") +} + +tasks.named("preBuild") { + dependsOn(generateLibEvDevEventNames) +} + +// The list of event names needs to be parsed from the input.h file in the NDK. +// input.h can be found in the Android/sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/linux/input.h +// folder on macOS. +val generateLibEvDevEventNames by tasks.registering(Exec::class) { + group = "build" + description = "Generates event names header from input.h" + + val prebuiltDir = File(android.ndkDirectory, "toolchains/llvm/prebuilt") + + // The "darwin-x86_64" part of the path is different on each operating system but it seems like + // the SDK Manager only downloads the NDK specific to the local operating system. So, just + // go into the only directory that the "prebuilt" directory contains. + val hostDirs = prebuiltDir.listFiles { file -> file.isDirectory } + ?: throw GradleException("No prebuilt toolchain directories found in $prebuiltDir") + + if (hostDirs.size != 1) { + throw GradleException("Expected exactly one prebuilt toolchain directory in $prebuiltDir, found ${hostDirs.size}") + } + val toolchainDir = hostDirs[0].absolutePath + + val inputHeader = "$toolchainDir/sysroot/usr/include/linux/input.h" + val inputEventCodesHeader = "$toolchainDir/sysroot/usr/include/linux/input-event-codes.h" + val outputHeader = "$projectDir/src/main/cpp/libevdev/event-names.h" + val pythonScript = "$projectDir/src/main/cpp/libevdev/make-event-names.py" + + commandLine("python3", pythonScript, inputHeader, inputEventCodesHeader) + + standardOutput = File(outputHeader).outputStream() + + inputs.file(pythonScript) + inputs.file(inputHeader) + inputs.file(inputEventCodesHeader) + outputs.file(outputHeader) } \ No newline at end of file diff --git a/priv/src/main/cpp/CMakeLists.txt b/priv/src/main/cpp/CMakeLists.txt index f9d9670ce2..4a9b59944e 100644 --- a/priv/src/main/cpp/CMakeLists.txt +++ b/priv/src/main/cpp/CMakeLists.txt @@ -74,7 +74,11 @@ endif () # 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) + privstarter.cpp + privservice.cpp + libevdev/libevdev.c + libevdev/libevdev-names.c + libevdev/libevdev-uinput.c) # Specifies libraries CMake should link to your target library. You # can link libraries from various origins, such as libraries defined in this diff --git a/priv/src/main/cpp/libevdev/Makefile.am b/priv/src/main/cpp/libevdev/Makefile.am new file mode 100644 index 0000000000..f577900827 --- /dev/null +++ b/priv/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/priv/src/main/cpp/libevdev/event-names.h b/priv/src/main/cpp/libevdev/event-names.h new file mode 100644 index 0000000000..8d433faea1 --- /dev/null +++ b/priv/src/main/cpp/libevdev/event-names.h @@ -0,0 +1,1656 @@ +/* THIS FILE IS GENERATED, DO NOT EDIT */ + +#ifndef EVENT_NAMES_H +#define EVENT_NAMES_H + +static const char *const ev_map[EV_MAX + 1] = { + [EV_SYN] = "EV_SYN", + [EV_KEY] = "EV_KEY", + [EV_REL] = "EV_REL", + [EV_ABS] = "EV_ABS", + [EV_MSC] = "EV_MSC", + [EV_SW] = "EV_SW", + [EV_LED] = "EV_LED", + [EV_SND] = "EV_SND", + [EV_REP] = "EV_REP", + [EV_FF] = "EV_FF", + [EV_PWR] = "EV_PWR", + [EV_FF_STATUS] = "EV_FF_STATUS", + [EV_MAX] = "EV_MAX", +}; + +static const char *const rel_map[REL_MAX + 1] = { + [REL_X] = "REL_X", + [REL_Y] = "REL_Y", + [REL_Z] = "REL_Z", + [REL_RX] = "REL_RX", + [REL_RY] = "REL_RY", + [REL_RZ] = "REL_RZ", + [REL_HWHEEL] = "REL_HWHEEL", + [REL_DIAL] = "REL_DIAL", + [REL_WHEEL] = "REL_WHEEL", + [REL_MISC] = "REL_MISC", + [REL_RESERVED] = "REL_RESERVED", + [REL_WHEEL_HI_RES] = "REL_WHEEL_HI_RES", + [REL_HWHEEL_HI_RES] = "REL_HWHEEL_HI_RES", + [REL_MAX] = "REL_MAX", +}; + +static const char *const abs_map[ABS_MAX + 1] = { + [ABS_X] = "ABS_X", + [ABS_Y] = "ABS_Y", + [ABS_Z] = "ABS_Z", + [ABS_RX] = "ABS_RX", + [ABS_RY] = "ABS_RY", + [ABS_RZ] = "ABS_RZ", + [ABS_THROTTLE] = "ABS_THROTTLE", + [ABS_RUDDER] = "ABS_RUDDER", + [ABS_WHEEL] = "ABS_WHEEL", + [ABS_GAS] = "ABS_GAS", + [ABS_BRAKE] = "ABS_BRAKE", + [ABS_HAT0X] = "ABS_HAT0X", + [ABS_HAT0Y] = "ABS_HAT0Y", + [ABS_HAT1X] = "ABS_HAT1X", + [ABS_HAT1Y] = "ABS_HAT1Y", + [ABS_HAT2X] = "ABS_HAT2X", + [ABS_HAT2Y] = "ABS_HAT2Y", + [ABS_HAT3X] = "ABS_HAT3X", + [ABS_HAT3Y] = "ABS_HAT3Y", + [ABS_PRESSURE] = "ABS_PRESSURE", + [ABS_DISTANCE] = "ABS_DISTANCE", + [ABS_TILT_X] = "ABS_TILT_X", + [ABS_TILT_Y] = "ABS_TILT_Y", + [ABS_TOOL_WIDTH] = "ABS_TOOL_WIDTH", + [ABS_VOLUME] = "ABS_VOLUME", + [ABS_PROFILE] = "ABS_PROFILE", + [ABS_MISC] = "ABS_MISC", + [ABS_RESERVED] = "ABS_RESERVED", + [ABS_MT_SLOT] = "ABS_MT_SLOT", + [ABS_MT_TOUCH_MAJOR] = "ABS_MT_TOUCH_MAJOR", + [ABS_MT_TOUCH_MINOR] = "ABS_MT_TOUCH_MINOR", + [ABS_MT_WIDTH_MAJOR] = "ABS_MT_WIDTH_MAJOR", + [ABS_MT_WIDTH_MINOR] = "ABS_MT_WIDTH_MINOR", + [ABS_MT_ORIENTATION] = "ABS_MT_ORIENTATION", + [ABS_MT_POSITION_X] = "ABS_MT_POSITION_X", + [ABS_MT_POSITION_Y] = "ABS_MT_POSITION_Y", + [ABS_MT_TOOL_TYPE] = "ABS_MT_TOOL_TYPE", + [ABS_MT_BLOB_ID] = "ABS_MT_BLOB_ID", + [ABS_MT_TRACKING_ID] = "ABS_MT_TRACKING_ID", + [ABS_MT_PRESSURE] = "ABS_MT_PRESSURE", + [ABS_MT_DISTANCE] = "ABS_MT_DISTANCE", + [ABS_MT_TOOL_X] = "ABS_MT_TOOL_X", + [ABS_MT_TOOL_Y] = "ABS_MT_TOOL_Y", + [ABS_MAX] = "ABS_MAX", +}; + +static const char *const key_map[KEY_MAX + 1] = { + [KEY_RESERVED] = "KEY_RESERVED", + [KEY_ESC] = "KEY_ESC", + [KEY_1] = "KEY_1", + [KEY_2] = "KEY_2", + [KEY_3] = "KEY_3", + [KEY_4] = "KEY_4", + [KEY_5] = "KEY_5", + [KEY_6] = "KEY_6", + [KEY_7] = "KEY_7", + [KEY_8] = "KEY_8", + [KEY_9] = "KEY_9", + [KEY_0] = "KEY_0", + [KEY_MINUS] = "KEY_MINUS", + [KEY_EQUAL] = "KEY_EQUAL", + [KEY_BACKSPACE] = "KEY_BACKSPACE", + [KEY_TAB] = "KEY_TAB", + [KEY_Q] = "KEY_Q", + [KEY_W] = "KEY_W", + [KEY_E] = "KEY_E", + [KEY_R] = "KEY_R", + [KEY_T] = "KEY_T", + [KEY_Y] = "KEY_Y", + [KEY_U] = "KEY_U", + [KEY_I] = "KEY_I", + [KEY_O] = "KEY_O", + [KEY_P] = "KEY_P", + [KEY_LEFTBRACE] = "KEY_LEFTBRACE", + [KEY_RIGHTBRACE] = "KEY_RIGHTBRACE", + [KEY_ENTER] = "KEY_ENTER", + [KEY_LEFTCTRL] = "KEY_LEFTCTRL", + [KEY_A] = "KEY_A", + [KEY_S] = "KEY_S", + [KEY_D] = "KEY_D", + [KEY_F] = "KEY_F", + [KEY_G] = "KEY_G", + [KEY_H] = "KEY_H", + [KEY_J] = "KEY_J", + [KEY_K] = "KEY_K", + [KEY_L] = "KEY_L", + [KEY_SEMICOLON] = "KEY_SEMICOLON", + [KEY_APOSTROPHE] = "KEY_APOSTROPHE", + [KEY_GRAVE] = "KEY_GRAVE", + [KEY_LEFTSHIFT] = "KEY_LEFTSHIFT", + [KEY_BACKSLASH] = "KEY_BACKSLASH", + [KEY_Z] = "KEY_Z", + [KEY_X] = "KEY_X", + [KEY_C] = "KEY_C", + [KEY_V] = "KEY_V", + [KEY_B] = "KEY_B", + [KEY_N] = "KEY_N", + [KEY_M] = "KEY_M", + [KEY_COMMA] = "KEY_COMMA", + [KEY_DOT] = "KEY_DOT", + [KEY_SLASH] = "KEY_SLASH", + [KEY_RIGHTSHIFT] = "KEY_RIGHTSHIFT", + [KEY_KPASTERISK] = "KEY_KPASTERISK", + [KEY_LEFTALT] = "KEY_LEFTALT", + [KEY_SPACE] = "KEY_SPACE", + [KEY_CAPSLOCK] = "KEY_CAPSLOCK", + [KEY_F1] = "KEY_F1", + [KEY_F2] = "KEY_F2", + [KEY_F3] = "KEY_F3", + [KEY_F4] = "KEY_F4", + [KEY_F5] = "KEY_F5", + [KEY_F6] = "KEY_F6", + [KEY_F7] = "KEY_F7", + [KEY_F8] = "KEY_F8", + [KEY_F9] = "KEY_F9", + [KEY_F10] = "KEY_F10", + [KEY_NUMLOCK] = "KEY_NUMLOCK", + [KEY_SCROLLLOCK] = "KEY_SCROLLLOCK", + [KEY_KP7] = "KEY_KP7", + [KEY_KP8] = "KEY_KP8", + [KEY_KP9] = "KEY_KP9", + [KEY_KPMINUS] = "KEY_KPMINUS", + [KEY_KP4] = "KEY_KP4", + [KEY_KP5] = "KEY_KP5", + [KEY_KP6] = "KEY_KP6", + [KEY_KPPLUS] = "KEY_KPPLUS", + [KEY_KP1] = "KEY_KP1", + [KEY_KP2] = "KEY_KP2", + [KEY_KP3] = "KEY_KP3", + [KEY_KP0] = "KEY_KP0", + [KEY_KPDOT] = "KEY_KPDOT", + [KEY_ZENKAKUHANKAKU] = "KEY_ZENKAKUHANKAKU", + [KEY_102ND] = "KEY_102ND", + [KEY_F11] = "KEY_F11", + [KEY_F12] = "KEY_F12", + [KEY_RO] = "KEY_RO", + [KEY_KATAKANA] = "KEY_KATAKANA", + [KEY_HIRAGANA] = "KEY_HIRAGANA", + [KEY_HENKAN] = "KEY_HENKAN", + [KEY_KATAKANAHIRAGANA] = "KEY_KATAKANAHIRAGANA", + [KEY_MUHENKAN] = "KEY_MUHENKAN", + [KEY_KPJPCOMMA] = "KEY_KPJPCOMMA", + [KEY_KPENTER] = "KEY_KPENTER", + [KEY_RIGHTCTRL] = "KEY_RIGHTCTRL", + [KEY_KPSLASH] = "KEY_KPSLASH", + [KEY_SYSRQ] = "KEY_SYSRQ", + [KEY_RIGHTALT] = "KEY_RIGHTALT", + [KEY_LINEFEED] = "KEY_LINEFEED", + [KEY_HOME] = "KEY_HOME", + [KEY_UP] = "KEY_UP", + [KEY_PAGEUP] = "KEY_PAGEUP", + [KEY_LEFT] = "KEY_LEFT", + [KEY_RIGHT] = "KEY_RIGHT", + [KEY_END] = "KEY_END", + [KEY_DOWN] = "KEY_DOWN", + [KEY_PAGEDOWN] = "KEY_PAGEDOWN", + [KEY_INSERT] = "KEY_INSERT", + [KEY_DELETE] = "KEY_DELETE", + [KEY_MACRO] = "KEY_MACRO", + [KEY_MUTE] = "KEY_MUTE", + [KEY_VOLUMEDOWN] = "KEY_VOLUMEDOWN", + [KEY_VOLUMEUP] = "KEY_VOLUMEUP", + [KEY_POWER] = "KEY_POWER", + [KEY_KPEQUAL] = "KEY_KPEQUAL", + [KEY_KPPLUSMINUS] = "KEY_KPPLUSMINUS", + [KEY_PAUSE] = "KEY_PAUSE", + [KEY_SCALE] = "KEY_SCALE", + [KEY_KPCOMMA] = "KEY_KPCOMMA", + [KEY_HANGEUL] = "KEY_HANGEUL", + [KEY_HANJA] = "KEY_HANJA", + [KEY_YEN] = "KEY_YEN", + [KEY_LEFTMETA] = "KEY_LEFTMETA", + [KEY_RIGHTMETA] = "KEY_RIGHTMETA", + [KEY_COMPOSE] = "KEY_COMPOSE", + [KEY_STOP] = "KEY_STOP", + [KEY_AGAIN] = "KEY_AGAIN", + [KEY_PROPS] = "KEY_PROPS", + [KEY_UNDO] = "KEY_UNDO", + [KEY_FRONT] = "KEY_FRONT", + [KEY_COPY] = "KEY_COPY", + [KEY_OPEN] = "KEY_OPEN", + [KEY_PASTE] = "KEY_PASTE", + [KEY_FIND] = "KEY_FIND", + [KEY_CUT] = "KEY_CUT", + [KEY_HELP] = "KEY_HELP", + [KEY_MENU] = "KEY_MENU", + [KEY_CALC] = "KEY_CALC", + [KEY_SETUP] = "KEY_SETUP", + [KEY_SLEEP] = "KEY_SLEEP", + [KEY_WAKEUP] = "KEY_WAKEUP", + [KEY_FILE] = "KEY_FILE", + [KEY_SENDFILE] = "KEY_SENDFILE", + [KEY_DELETEFILE] = "KEY_DELETEFILE", + [KEY_XFER] = "KEY_XFER", + [KEY_PROG1] = "KEY_PROG1", + [KEY_PROG2] = "KEY_PROG2", + [KEY_WWW] = "KEY_WWW", + [KEY_MSDOS] = "KEY_MSDOS", + [KEY_COFFEE] = "KEY_COFFEE", + [KEY_ROTATE_DISPLAY] = "KEY_ROTATE_DISPLAY", + [KEY_CYCLEWINDOWS] = "KEY_CYCLEWINDOWS", + [KEY_MAIL] = "KEY_MAIL", + [KEY_BOOKMARKS] = "KEY_BOOKMARKS", + [KEY_COMPUTER] = "KEY_COMPUTER", + [KEY_BACK] = "KEY_BACK", + [KEY_FORWARD] = "KEY_FORWARD", + [KEY_CLOSECD] = "KEY_CLOSECD", + [KEY_EJECTCD] = "KEY_EJECTCD", + [KEY_EJECTCLOSECD] = "KEY_EJECTCLOSECD", + [KEY_NEXTSONG] = "KEY_NEXTSONG", + [KEY_PLAYPAUSE] = "KEY_PLAYPAUSE", + [KEY_PREVIOUSSONG] = "KEY_PREVIOUSSONG", + [KEY_STOPCD] = "KEY_STOPCD", + [KEY_RECORD] = "KEY_RECORD", + [KEY_REWIND] = "KEY_REWIND", + [KEY_PHONE] = "KEY_PHONE", + [KEY_ISO] = "KEY_ISO", + [KEY_CONFIG] = "KEY_CONFIG", + [KEY_HOMEPAGE] = "KEY_HOMEPAGE", + [KEY_REFRESH] = "KEY_REFRESH", + [KEY_EXIT] = "KEY_EXIT", + [KEY_MOVE] = "KEY_MOVE", + [KEY_EDIT] = "KEY_EDIT", + [KEY_SCROLLUP] = "KEY_SCROLLUP", + [KEY_SCROLLDOWN] = "KEY_SCROLLDOWN", + [KEY_KPLEFTPAREN] = "KEY_KPLEFTPAREN", + [KEY_KPRIGHTPAREN] = "KEY_KPRIGHTPAREN", + [KEY_NEW] = "KEY_NEW", + [KEY_REDO] = "KEY_REDO", + [KEY_F13] = "KEY_F13", + [KEY_F14] = "KEY_F14", + [KEY_F15] = "KEY_F15", + [KEY_F16] = "KEY_F16", + [KEY_F17] = "KEY_F17", + [KEY_F18] = "KEY_F18", + [KEY_F19] = "KEY_F19", + [KEY_F20] = "KEY_F20", + [KEY_F21] = "KEY_F21", + [KEY_F22] = "KEY_F22", + [KEY_F23] = "KEY_F23", + [KEY_F24] = "KEY_F24", + [KEY_PLAYCD] = "KEY_PLAYCD", + [KEY_PAUSECD] = "KEY_PAUSECD", + [KEY_PROG3] = "KEY_PROG3", + [KEY_PROG4] = "KEY_PROG4", + [KEY_ALL_APPLICATIONS] = "KEY_ALL_APPLICATIONS", + [KEY_SUSPEND] = "KEY_SUSPEND", + [KEY_CLOSE] = "KEY_CLOSE", + [KEY_PLAY] = "KEY_PLAY", + [KEY_FASTFORWARD] = "KEY_FASTFORWARD", + [KEY_BASSBOOST] = "KEY_BASSBOOST", + [KEY_PRINT] = "KEY_PRINT", + [KEY_HP] = "KEY_HP", + [KEY_CAMERA] = "KEY_CAMERA", + [KEY_SOUND] = "KEY_SOUND", + [KEY_QUESTION] = "KEY_QUESTION", + [KEY_EMAIL] = "KEY_EMAIL", + [KEY_CHAT] = "KEY_CHAT", + [KEY_SEARCH] = "KEY_SEARCH", + [KEY_CONNECT] = "KEY_CONNECT", + [KEY_FINANCE] = "KEY_FINANCE", + [KEY_SPORT] = "KEY_SPORT", + [KEY_SHOP] = "KEY_SHOP", + [KEY_ALTERASE] = "KEY_ALTERASE", + [KEY_CANCEL] = "KEY_CANCEL", + [KEY_BRIGHTNESSDOWN] = "KEY_BRIGHTNESSDOWN", + [KEY_BRIGHTNESSUP] = "KEY_BRIGHTNESSUP", + [KEY_MEDIA] = "KEY_MEDIA", + [KEY_SWITCHVIDEOMODE] = "KEY_SWITCHVIDEOMODE", + [KEY_KBDILLUMTOGGLE] = "KEY_KBDILLUMTOGGLE", + [KEY_KBDILLUMDOWN] = "KEY_KBDILLUMDOWN", + [KEY_KBDILLUMUP] = "KEY_KBDILLUMUP", + [KEY_SEND] = "KEY_SEND", + [KEY_REPLY] = "KEY_REPLY", + [KEY_FORWARDMAIL] = "KEY_FORWARDMAIL", + [KEY_SAVE] = "KEY_SAVE", + [KEY_DOCUMENTS] = "KEY_DOCUMENTS", + [KEY_BATTERY] = "KEY_BATTERY", + [KEY_BLUETOOTH] = "KEY_BLUETOOTH", + [KEY_WLAN] = "KEY_WLAN", + [KEY_UWB] = "KEY_UWB", + [KEY_UNKNOWN] = "KEY_UNKNOWN", + [KEY_VIDEO_NEXT] = "KEY_VIDEO_NEXT", + [KEY_VIDEO_PREV] = "KEY_VIDEO_PREV", + [KEY_BRIGHTNESS_CYCLE] = "KEY_BRIGHTNESS_CYCLE", + [KEY_BRIGHTNESS_AUTO] = "KEY_BRIGHTNESS_AUTO", + [KEY_DISPLAY_OFF] = "KEY_DISPLAY_OFF", + [KEY_WWAN] = "KEY_WWAN", + [KEY_RFKILL] = "KEY_RFKILL", + [KEY_MICMUTE] = "KEY_MICMUTE", + [KEY_OK] = "KEY_OK", + [KEY_SELECT] = "KEY_SELECT", + [KEY_GOTO] = "KEY_GOTO", + [KEY_CLEAR] = "KEY_CLEAR", + [KEY_POWER2] = "KEY_POWER2", + [KEY_OPTION] = "KEY_OPTION", + [KEY_INFO] = "KEY_INFO", + [KEY_TIME] = "KEY_TIME", + [KEY_VENDOR] = "KEY_VENDOR", + [KEY_ARCHIVE] = "KEY_ARCHIVE", + [KEY_PROGRAM] = "KEY_PROGRAM", + [KEY_CHANNEL] = "KEY_CHANNEL", + [KEY_FAVORITES] = "KEY_FAVORITES", + [KEY_EPG] = "KEY_EPG", + [KEY_PVR] = "KEY_PVR", + [KEY_MHP] = "KEY_MHP", + [KEY_LANGUAGE] = "KEY_LANGUAGE", + [KEY_TITLE] = "KEY_TITLE", + [KEY_SUBTITLE] = "KEY_SUBTITLE", + [KEY_ANGLE] = "KEY_ANGLE", + [KEY_FULL_SCREEN] = "KEY_FULL_SCREEN", + [KEY_MODE] = "KEY_MODE", + [KEY_KEYBOARD] = "KEY_KEYBOARD", + [KEY_ASPECT_RATIO] = "KEY_ASPECT_RATIO", + [KEY_PC] = "KEY_PC", + [KEY_TV] = "KEY_TV", + [KEY_TV2] = "KEY_TV2", + [KEY_VCR] = "KEY_VCR", + [KEY_VCR2] = "KEY_VCR2", + [KEY_SAT] = "KEY_SAT", + [KEY_SAT2] = "KEY_SAT2", + [KEY_CD] = "KEY_CD", + [KEY_TAPE] = "KEY_TAPE", + [KEY_RADIO] = "KEY_RADIO", + [KEY_TUNER] = "KEY_TUNER", + [KEY_PLAYER] = "KEY_PLAYER", + [KEY_TEXT] = "KEY_TEXT", + [KEY_DVD] = "KEY_DVD", + [KEY_AUX] = "KEY_AUX", + [KEY_MP3] = "KEY_MP3", + [KEY_AUDIO] = "KEY_AUDIO", + [KEY_VIDEO] = "KEY_VIDEO", + [KEY_DIRECTORY] = "KEY_DIRECTORY", + [KEY_LIST] = "KEY_LIST", + [KEY_MEMO] = "KEY_MEMO", + [KEY_CALENDAR] = "KEY_CALENDAR", + [KEY_RED] = "KEY_RED", + [KEY_GREEN] = "KEY_GREEN", + [KEY_YELLOW] = "KEY_YELLOW", + [KEY_BLUE] = "KEY_BLUE", + [KEY_CHANNELUP] = "KEY_CHANNELUP", + [KEY_CHANNELDOWN] = "KEY_CHANNELDOWN", + [KEY_FIRST] = "KEY_FIRST", + [KEY_LAST] = "KEY_LAST", + [KEY_AB] = "KEY_AB", + [KEY_NEXT] = "KEY_NEXT", + [KEY_RESTART] = "KEY_RESTART", + [KEY_SLOW] = "KEY_SLOW", + [KEY_SHUFFLE] = "KEY_SHUFFLE", + [KEY_BREAK] = "KEY_BREAK", + [KEY_PREVIOUS] = "KEY_PREVIOUS", + [KEY_DIGITS] = "KEY_DIGITS", + [KEY_TEEN] = "KEY_TEEN", + [KEY_TWEN] = "KEY_TWEN", + [KEY_VIDEOPHONE] = "KEY_VIDEOPHONE", + [KEY_GAMES] = "KEY_GAMES", + [KEY_ZOOMIN] = "KEY_ZOOMIN", + [KEY_ZOOMOUT] = "KEY_ZOOMOUT", + [KEY_ZOOMRESET] = "KEY_ZOOMRESET", + [KEY_WORDPROCESSOR] = "KEY_WORDPROCESSOR", + [KEY_EDITOR] = "KEY_EDITOR", + [KEY_SPREADSHEET] = "KEY_SPREADSHEET", + [KEY_GRAPHICSEDITOR] = "KEY_GRAPHICSEDITOR", + [KEY_PRESENTATION] = "KEY_PRESENTATION", + [KEY_DATABASE] = "KEY_DATABASE", + [KEY_NEWS] = "KEY_NEWS", + [KEY_VOICEMAIL] = "KEY_VOICEMAIL", + [KEY_ADDRESSBOOK] = "KEY_ADDRESSBOOK", + [KEY_MESSENGER] = "KEY_MESSENGER", + [KEY_DISPLAYTOGGLE] = "KEY_DISPLAYTOGGLE", + [KEY_SPELLCHECK] = "KEY_SPELLCHECK", + [KEY_LOGOFF] = "KEY_LOGOFF", + [KEY_DOLLAR] = "KEY_DOLLAR", + [KEY_EURO] = "KEY_EURO", + [KEY_FRAMEBACK] = "KEY_FRAMEBACK", + [KEY_FRAMEFORWARD] = "KEY_FRAMEFORWARD", + [KEY_CONTEXT_MENU] = "KEY_CONTEXT_MENU", + [KEY_MEDIA_REPEAT] = "KEY_MEDIA_REPEAT", + [KEY_10CHANNELSUP] = "KEY_10CHANNELSUP", + [KEY_10CHANNELSDOWN] = "KEY_10CHANNELSDOWN", + [KEY_IMAGES] = "KEY_IMAGES", + [KEY_NOTIFICATION_CENTER] = "KEY_NOTIFICATION_CENTER", + [KEY_PICKUP_PHONE] = "KEY_PICKUP_PHONE", + [KEY_HANGUP_PHONE] = "KEY_HANGUP_PHONE", + [KEY_DEL_EOL] = "KEY_DEL_EOL", + [KEY_DEL_EOS] = "KEY_DEL_EOS", + [KEY_INS_LINE] = "KEY_INS_LINE", + [KEY_DEL_LINE] = "KEY_DEL_LINE", + [KEY_FN] = "KEY_FN", + [KEY_FN_ESC] = "KEY_FN_ESC", + [KEY_FN_F1] = "KEY_FN_F1", + [KEY_FN_F2] = "KEY_FN_F2", + [KEY_FN_F3] = "KEY_FN_F3", + [KEY_FN_F4] = "KEY_FN_F4", + [KEY_FN_F5] = "KEY_FN_F5", + [KEY_FN_F6] = "KEY_FN_F6", + [KEY_FN_F7] = "KEY_FN_F7", + [KEY_FN_F8] = "KEY_FN_F8", + [KEY_FN_F9] = "KEY_FN_F9", + [KEY_FN_F10] = "KEY_FN_F10", + [KEY_FN_F11] = "KEY_FN_F11", + [KEY_FN_F12] = "KEY_FN_F12", + [KEY_FN_1] = "KEY_FN_1", + [KEY_FN_2] = "KEY_FN_2", + [KEY_FN_D] = "KEY_FN_D", + [KEY_FN_E] = "KEY_FN_E", + [KEY_FN_F] = "KEY_FN_F", + [KEY_FN_S] = "KEY_FN_S", + [KEY_FN_B] = "KEY_FN_B", + [KEY_FN_RIGHT_SHIFT] = "KEY_FN_RIGHT_SHIFT", + [KEY_BRL_DOT1] = "KEY_BRL_DOT1", + [KEY_BRL_DOT2] = "KEY_BRL_DOT2", + [KEY_BRL_DOT3] = "KEY_BRL_DOT3", + [KEY_BRL_DOT4] = "KEY_BRL_DOT4", + [KEY_BRL_DOT5] = "KEY_BRL_DOT5", + [KEY_BRL_DOT6] = "KEY_BRL_DOT6", + [KEY_BRL_DOT7] = "KEY_BRL_DOT7", + [KEY_BRL_DOT8] = "KEY_BRL_DOT8", + [KEY_BRL_DOT9] = "KEY_BRL_DOT9", + [KEY_BRL_DOT10] = "KEY_BRL_DOT10", + [KEY_NUMERIC_0] = "KEY_NUMERIC_0", + [KEY_NUMERIC_1] = "KEY_NUMERIC_1", + [KEY_NUMERIC_2] = "KEY_NUMERIC_2", + [KEY_NUMERIC_3] = "KEY_NUMERIC_3", + [KEY_NUMERIC_4] = "KEY_NUMERIC_4", + [KEY_NUMERIC_5] = "KEY_NUMERIC_5", + [KEY_NUMERIC_6] = "KEY_NUMERIC_6", + [KEY_NUMERIC_7] = "KEY_NUMERIC_7", + [KEY_NUMERIC_8] = "KEY_NUMERIC_8", + [KEY_NUMERIC_9] = "KEY_NUMERIC_9", + [KEY_NUMERIC_STAR] = "KEY_NUMERIC_STAR", + [KEY_NUMERIC_POUND] = "KEY_NUMERIC_POUND", + [KEY_NUMERIC_A] = "KEY_NUMERIC_A", + [KEY_NUMERIC_B] = "KEY_NUMERIC_B", + [KEY_NUMERIC_C] = "KEY_NUMERIC_C", + [KEY_NUMERIC_D] = "KEY_NUMERIC_D", + [KEY_CAMERA_FOCUS] = "KEY_CAMERA_FOCUS", + [KEY_WPS_BUTTON] = "KEY_WPS_BUTTON", + [KEY_TOUCHPAD_TOGGLE] = "KEY_TOUCHPAD_TOGGLE", + [KEY_TOUCHPAD_ON] = "KEY_TOUCHPAD_ON", + [KEY_TOUCHPAD_OFF] = "KEY_TOUCHPAD_OFF", + [KEY_CAMERA_ZOOMIN] = "KEY_CAMERA_ZOOMIN", + [KEY_CAMERA_ZOOMOUT] = "KEY_CAMERA_ZOOMOUT", + [KEY_CAMERA_UP] = "KEY_CAMERA_UP", + [KEY_CAMERA_DOWN] = "KEY_CAMERA_DOWN", + [KEY_CAMERA_LEFT] = "KEY_CAMERA_LEFT", + [KEY_CAMERA_RIGHT] = "KEY_CAMERA_RIGHT", + [KEY_ATTENDANT_ON] = "KEY_ATTENDANT_ON", + [KEY_ATTENDANT_OFF] = "KEY_ATTENDANT_OFF", + [KEY_ATTENDANT_TOGGLE] = "KEY_ATTENDANT_TOGGLE", + [KEY_LIGHTS_TOGGLE] = "KEY_LIGHTS_TOGGLE", + [KEY_ALS_TOGGLE] = "KEY_ALS_TOGGLE", + [KEY_ROTATE_LOCK_TOGGLE] = "KEY_ROTATE_LOCK_TOGGLE", + [KEY_BUTTONCONFIG] = "KEY_BUTTONCONFIG", + [KEY_TASKMANAGER] = "KEY_TASKMANAGER", + [KEY_JOURNAL] = "KEY_JOURNAL", + [KEY_CONTROLPANEL] = "KEY_CONTROLPANEL", + [KEY_APPSELECT] = "KEY_APPSELECT", + [KEY_SCREENSAVER] = "KEY_SCREENSAVER", + [KEY_VOICECOMMAND] = "KEY_VOICECOMMAND", + [KEY_ASSISTANT] = "KEY_ASSISTANT", + [KEY_KBD_LAYOUT_NEXT] = "KEY_KBD_LAYOUT_NEXT", + [KEY_EMOJI_PICKER] = "KEY_EMOJI_PICKER", + [KEY_DICTATE] = "KEY_DICTATE", + [KEY_CAMERA_ACCESS_ENABLE] = "KEY_CAMERA_ACCESS_ENABLE", + [KEY_CAMERA_ACCESS_DISABLE] = "KEY_CAMERA_ACCESS_DISABLE", + [KEY_CAMERA_ACCESS_TOGGLE] = "KEY_CAMERA_ACCESS_TOGGLE", + [KEY_BRIGHTNESS_MIN] = "KEY_BRIGHTNESS_MIN", + [KEY_BRIGHTNESS_MAX] = "KEY_BRIGHTNESS_MAX", + [KEY_KBDINPUTASSIST_PREV] = "KEY_KBDINPUTASSIST_PREV", + [KEY_KBDINPUTASSIST_NEXT] = "KEY_KBDINPUTASSIST_NEXT", + [KEY_KBDINPUTASSIST_PREVGROUP] = "KEY_KBDINPUTASSIST_PREVGROUP", + [KEY_KBDINPUTASSIST_NEXTGROUP] = "KEY_KBDINPUTASSIST_NEXTGROUP", + [KEY_KBDINPUTASSIST_ACCEPT] = "KEY_KBDINPUTASSIST_ACCEPT", + [KEY_KBDINPUTASSIST_CANCEL] = "KEY_KBDINPUTASSIST_CANCEL", + [KEY_RIGHT_UP] = "KEY_RIGHT_UP", + [KEY_RIGHT_DOWN] = "KEY_RIGHT_DOWN", + [KEY_LEFT_UP] = "KEY_LEFT_UP", + [KEY_LEFT_DOWN] = "KEY_LEFT_DOWN", + [KEY_ROOT_MENU] = "KEY_ROOT_MENU", + [KEY_MEDIA_TOP_MENU] = "KEY_MEDIA_TOP_MENU", + [KEY_NUMERIC_11] = "KEY_NUMERIC_11", + [KEY_NUMERIC_12] = "KEY_NUMERIC_12", + [KEY_AUDIO_DESC] = "KEY_AUDIO_DESC", + [KEY_3D_MODE] = "KEY_3D_MODE", + [KEY_NEXT_FAVORITE] = "KEY_NEXT_FAVORITE", + [KEY_STOP_RECORD] = "KEY_STOP_RECORD", + [KEY_PAUSE_RECORD] = "KEY_PAUSE_RECORD", + [KEY_VOD] = "KEY_VOD", + [KEY_UNMUTE] = "KEY_UNMUTE", + [KEY_FASTREVERSE] = "KEY_FASTREVERSE", + [KEY_SLOWREVERSE] = "KEY_SLOWREVERSE", + [KEY_DATA] = "KEY_DATA", + [KEY_ONSCREEN_KEYBOARD] = "KEY_ONSCREEN_KEYBOARD", + [KEY_PRIVACY_SCREEN_TOGGLE] = "KEY_PRIVACY_SCREEN_TOGGLE", + [KEY_SELECTIVE_SCREENSHOT] = "KEY_SELECTIVE_SCREENSHOT", + [KEY_NEXT_ELEMENT] = "KEY_NEXT_ELEMENT", + [KEY_PREVIOUS_ELEMENT] = "KEY_PREVIOUS_ELEMENT", + [KEY_AUTOPILOT_ENGAGE_TOGGLE] = "KEY_AUTOPILOT_ENGAGE_TOGGLE", + [KEY_MARK_WAYPOINT] = "KEY_MARK_WAYPOINT", + [KEY_SOS] = "KEY_SOS", + [KEY_NAV_CHART] = "KEY_NAV_CHART", + [KEY_FISHING_CHART] = "KEY_FISHING_CHART", + [KEY_SINGLE_RANGE_RADAR] = "KEY_SINGLE_RANGE_RADAR", + [KEY_DUAL_RANGE_RADAR] = "KEY_DUAL_RANGE_RADAR", + [KEY_RADAR_OVERLAY] = "KEY_RADAR_OVERLAY", + [KEY_TRADITIONAL_SONAR] = "KEY_TRADITIONAL_SONAR", + [KEY_CLEARVU_SONAR] = "KEY_CLEARVU_SONAR", + [KEY_SIDEVU_SONAR] = "KEY_SIDEVU_SONAR", + [KEY_NAV_INFO] = "KEY_NAV_INFO", + [KEY_BRIGHTNESS_MENU] = "KEY_BRIGHTNESS_MENU", + [KEY_MACRO1] = "KEY_MACRO1", + [KEY_MACRO2] = "KEY_MACRO2", + [KEY_MACRO3] = "KEY_MACRO3", + [KEY_MACRO4] = "KEY_MACRO4", + [KEY_MACRO5] = "KEY_MACRO5", + [KEY_MACRO6] = "KEY_MACRO6", + [KEY_MACRO7] = "KEY_MACRO7", + [KEY_MACRO8] = "KEY_MACRO8", + [KEY_MACRO9] = "KEY_MACRO9", + [KEY_MACRO10] = "KEY_MACRO10", + [KEY_MACRO11] = "KEY_MACRO11", + [KEY_MACRO12] = "KEY_MACRO12", + [KEY_MACRO13] = "KEY_MACRO13", + [KEY_MACRO14] = "KEY_MACRO14", + [KEY_MACRO15] = "KEY_MACRO15", + [KEY_MACRO16] = "KEY_MACRO16", + [KEY_MACRO17] = "KEY_MACRO17", + [KEY_MACRO18] = "KEY_MACRO18", + [KEY_MACRO19] = "KEY_MACRO19", + [KEY_MACRO20] = "KEY_MACRO20", + [KEY_MACRO21] = "KEY_MACRO21", + [KEY_MACRO22] = "KEY_MACRO22", + [KEY_MACRO23] = "KEY_MACRO23", + [KEY_MACRO24] = "KEY_MACRO24", + [KEY_MACRO25] = "KEY_MACRO25", + [KEY_MACRO26] = "KEY_MACRO26", + [KEY_MACRO27] = "KEY_MACRO27", + [KEY_MACRO28] = "KEY_MACRO28", + [KEY_MACRO29] = "KEY_MACRO29", + [KEY_MACRO30] = "KEY_MACRO30", + [KEY_MACRO_RECORD_START] = "KEY_MACRO_RECORD_START", + [KEY_MACRO_RECORD_STOP] = "KEY_MACRO_RECORD_STOP", + [KEY_MACRO_PRESET_CYCLE] = "KEY_MACRO_PRESET_CYCLE", + [KEY_MACRO_PRESET1] = "KEY_MACRO_PRESET1", + [KEY_MACRO_PRESET2] = "KEY_MACRO_PRESET2", + [KEY_MACRO_PRESET3] = "KEY_MACRO_PRESET3", + [KEY_KBD_LCD_MENU1] = "KEY_KBD_LCD_MENU1", + [KEY_KBD_LCD_MENU2] = "KEY_KBD_LCD_MENU2", + [KEY_KBD_LCD_MENU3] = "KEY_KBD_LCD_MENU3", + [KEY_KBD_LCD_MENU4] = "KEY_KBD_LCD_MENU4", + [KEY_KBD_LCD_MENU5] = "KEY_KBD_LCD_MENU5", + [KEY_MAX] = "KEY_MAX", + [BTN_0] = "BTN_0", + [BTN_1] = "BTN_1", + [BTN_2] = "BTN_2", + [BTN_3] = "BTN_3", + [BTN_4] = "BTN_4", + [BTN_5] = "BTN_5", + [BTN_6] = "BTN_6", + [BTN_7] = "BTN_7", + [BTN_8] = "BTN_8", + [BTN_9] = "BTN_9", + [BTN_LEFT] = "BTN_LEFT", + [BTN_RIGHT] = "BTN_RIGHT", + [BTN_MIDDLE] = "BTN_MIDDLE", + [BTN_SIDE] = "BTN_SIDE", + [BTN_EXTRA] = "BTN_EXTRA", + [BTN_FORWARD] = "BTN_FORWARD", + [BTN_BACK] = "BTN_BACK", + [BTN_TASK] = "BTN_TASK", + [BTN_TRIGGER] = "BTN_TRIGGER", + [BTN_THUMB] = "BTN_THUMB", + [BTN_THUMB2] = "BTN_THUMB2", + [BTN_TOP] = "BTN_TOP", + [BTN_TOP2] = "BTN_TOP2", + [BTN_PINKIE] = "BTN_PINKIE", + [BTN_BASE] = "BTN_BASE", + [BTN_BASE2] = "BTN_BASE2", + [BTN_BASE3] = "BTN_BASE3", + [BTN_BASE4] = "BTN_BASE4", + [BTN_BASE5] = "BTN_BASE5", + [BTN_BASE6] = "BTN_BASE6", + [BTN_DEAD] = "BTN_DEAD", + [BTN_SOUTH] = "BTN_SOUTH", + [BTN_EAST] = "BTN_EAST", + [BTN_C] = "BTN_C", + [BTN_NORTH] = "BTN_NORTH", + [BTN_WEST] = "BTN_WEST", + [BTN_Z] = "BTN_Z", + [BTN_TL] = "BTN_TL", + [BTN_TR] = "BTN_TR", + [BTN_TL2] = "BTN_TL2", + [BTN_TR2] = "BTN_TR2", + [BTN_SELECT] = "BTN_SELECT", + [BTN_START] = "BTN_START", + [BTN_MODE] = "BTN_MODE", + [BTN_THUMBL] = "BTN_THUMBL", + [BTN_THUMBR] = "BTN_THUMBR", + [BTN_TOOL_PEN] = "BTN_TOOL_PEN", + [BTN_TOOL_RUBBER] = "BTN_TOOL_RUBBER", + [BTN_TOOL_BRUSH] = "BTN_TOOL_BRUSH", + [BTN_TOOL_PENCIL] = "BTN_TOOL_PENCIL", + [BTN_TOOL_AIRBRUSH] = "BTN_TOOL_AIRBRUSH", + [BTN_TOOL_FINGER] = "BTN_TOOL_FINGER", + [BTN_TOOL_MOUSE] = "BTN_TOOL_MOUSE", + [BTN_TOOL_LENS] = "BTN_TOOL_LENS", + [BTN_TOOL_QUINTTAP] = "BTN_TOOL_QUINTTAP", + [BTN_STYLUS3] = "BTN_STYLUS3", + [BTN_TOUCH] = "BTN_TOUCH", + [BTN_STYLUS] = "BTN_STYLUS", + [BTN_STYLUS2] = "BTN_STYLUS2", + [BTN_TOOL_DOUBLETAP] = "BTN_TOOL_DOUBLETAP", + [BTN_TOOL_TRIPLETAP] = "BTN_TOOL_TRIPLETAP", + [BTN_TOOL_QUADTAP] = "BTN_TOOL_QUADTAP", + [BTN_GEAR_DOWN] = "BTN_GEAR_DOWN", + [BTN_GEAR_UP] = "BTN_GEAR_UP", + [BTN_DPAD_UP] = "BTN_DPAD_UP", + [BTN_DPAD_DOWN] = "BTN_DPAD_DOWN", + [BTN_DPAD_LEFT] = "BTN_DPAD_LEFT", + [BTN_DPAD_RIGHT] = "BTN_DPAD_RIGHT", + [BTN_TRIGGER_HAPPY1] = "BTN_TRIGGER_HAPPY1", + [BTN_TRIGGER_HAPPY2] = "BTN_TRIGGER_HAPPY2", + [BTN_TRIGGER_HAPPY3] = "BTN_TRIGGER_HAPPY3", + [BTN_TRIGGER_HAPPY4] = "BTN_TRIGGER_HAPPY4", + [BTN_TRIGGER_HAPPY5] = "BTN_TRIGGER_HAPPY5", + [BTN_TRIGGER_HAPPY6] = "BTN_TRIGGER_HAPPY6", + [BTN_TRIGGER_HAPPY7] = "BTN_TRIGGER_HAPPY7", + [BTN_TRIGGER_HAPPY8] = "BTN_TRIGGER_HAPPY8", + [BTN_TRIGGER_HAPPY9] = "BTN_TRIGGER_HAPPY9", + [BTN_TRIGGER_HAPPY10] = "BTN_TRIGGER_HAPPY10", + [BTN_TRIGGER_HAPPY11] = "BTN_TRIGGER_HAPPY11", + [BTN_TRIGGER_HAPPY12] = "BTN_TRIGGER_HAPPY12", + [BTN_TRIGGER_HAPPY13] = "BTN_TRIGGER_HAPPY13", + [BTN_TRIGGER_HAPPY14] = "BTN_TRIGGER_HAPPY14", + [BTN_TRIGGER_HAPPY15] = "BTN_TRIGGER_HAPPY15", + [BTN_TRIGGER_HAPPY16] = "BTN_TRIGGER_HAPPY16", + [BTN_TRIGGER_HAPPY17] = "BTN_TRIGGER_HAPPY17", + [BTN_TRIGGER_HAPPY18] = "BTN_TRIGGER_HAPPY18", + [BTN_TRIGGER_HAPPY19] = "BTN_TRIGGER_HAPPY19", + [BTN_TRIGGER_HAPPY20] = "BTN_TRIGGER_HAPPY20", + [BTN_TRIGGER_HAPPY21] = "BTN_TRIGGER_HAPPY21", + [BTN_TRIGGER_HAPPY22] = "BTN_TRIGGER_HAPPY22", + [BTN_TRIGGER_HAPPY23] = "BTN_TRIGGER_HAPPY23", + [BTN_TRIGGER_HAPPY24] = "BTN_TRIGGER_HAPPY24", + [BTN_TRIGGER_HAPPY25] = "BTN_TRIGGER_HAPPY25", + [BTN_TRIGGER_HAPPY26] = "BTN_TRIGGER_HAPPY26", + [BTN_TRIGGER_HAPPY27] = "BTN_TRIGGER_HAPPY27", + [BTN_TRIGGER_HAPPY28] = "BTN_TRIGGER_HAPPY28", + [BTN_TRIGGER_HAPPY29] = "BTN_TRIGGER_HAPPY29", + [BTN_TRIGGER_HAPPY30] = "BTN_TRIGGER_HAPPY30", + [BTN_TRIGGER_HAPPY31] = "BTN_TRIGGER_HAPPY31", + [BTN_TRIGGER_HAPPY32] = "BTN_TRIGGER_HAPPY32", + [BTN_TRIGGER_HAPPY33] = "BTN_TRIGGER_HAPPY33", + [BTN_TRIGGER_HAPPY34] = "BTN_TRIGGER_HAPPY34", + [BTN_TRIGGER_HAPPY35] = "BTN_TRIGGER_HAPPY35", + [BTN_TRIGGER_HAPPY36] = "BTN_TRIGGER_HAPPY36", + [BTN_TRIGGER_HAPPY37] = "BTN_TRIGGER_HAPPY37", + [BTN_TRIGGER_HAPPY38] = "BTN_TRIGGER_HAPPY38", + [BTN_TRIGGER_HAPPY39] = "BTN_TRIGGER_HAPPY39", + [BTN_TRIGGER_HAPPY40] = "BTN_TRIGGER_HAPPY40", +}; + +static const char *const led_map[LED_MAX + 1] = { + [LED_NUML] = "LED_NUML", + [LED_CAPSL] = "LED_CAPSL", + [LED_SCROLLL] = "LED_SCROLLL", + [LED_COMPOSE] = "LED_COMPOSE", + [LED_KANA] = "LED_KANA", + [LED_SLEEP] = "LED_SLEEP", + [LED_SUSPEND] = "LED_SUSPEND", + [LED_MUTE] = "LED_MUTE", + [LED_MISC] = "LED_MISC", + [LED_MAIL] = "LED_MAIL", + [LED_CHARGING] = "LED_CHARGING", + [LED_MAX] = "LED_MAX", +}; + +static const char *const snd_map[SND_MAX + 1] = { + [SND_CLICK] = "SND_CLICK", + [SND_BELL] = "SND_BELL", + [SND_TONE] = "SND_TONE", + [SND_MAX] = "SND_MAX", +}; + +static const char *const msc_map[MSC_MAX + 1] = { + [MSC_SERIAL] = "MSC_SERIAL", + [MSC_PULSELED] = "MSC_PULSELED", + [MSC_GESTURE] = "MSC_GESTURE", + [MSC_RAW] = "MSC_RAW", + [MSC_SCAN] = "MSC_SCAN", + [MSC_TIMESTAMP] = "MSC_TIMESTAMP", + [MSC_MAX] = "MSC_MAX", +}; + +static const char *const sw_map[SW_MAX + 1] = { + [SW_LID] = "SW_LID", + [SW_TABLET_MODE] = "SW_TABLET_MODE", + [SW_HEADPHONE_INSERT] = "SW_HEADPHONE_INSERT", + [SW_RFKILL_ALL] = "SW_RFKILL_ALL", + [SW_MICROPHONE_INSERT] = "SW_MICROPHONE_INSERT", + [SW_DOCK] = "SW_DOCK", + [SW_LINEOUT_INSERT] = "SW_LINEOUT_INSERT", + [SW_JACK_PHYSICAL_INSERT] = "SW_JACK_PHYSICAL_INSERT", + [SW_VIDEOOUT_INSERT] = "SW_VIDEOOUT_INSERT", + [SW_CAMERA_LENS_COVER] = "SW_CAMERA_LENS_COVER", + [SW_KEYPAD_SLIDE] = "SW_KEYPAD_SLIDE", + [SW_FRONT_PROXIMITY] = "SW_FRONT_PROXIMITY", + [SW_ROTATE_LOCK] = "SW_ROTATE_LOCK", + [SW_LINEIN_INSERT] = "SW_LINEIN_INSERT", + [SW_MUTE_DEVICE] = "SW_MUTE_DEVICE", + [SW_PEN_INSERTED] = "SW_PEN_INSERTED", + [SW_MACHINE_COVER] = "SW_MACHINE_COVER", +}; + +static const char *const ff_map[FF_MAX + 1] = { + [FF_STATUS_STOPPED] = "FF_STATUS_STOPPED", + [FF_STATUS_MAX] = "FF_STATUS_MAX", + [FF_RUMBLE] = "FF_RUMBLE", + [FF_PERIODIC] = "FF_PERIODIC", + [FF_CONSTANT] = "FF_CONSTANT", + [FF_SPRING] = "FF_SPRING", + [FF_FRICTION] = "FF_FRICTION", + [FF_DAMPER] = "FF_DAMPER", + [FF_INERTIA] = "FF_INERTIA", + [FF_RAMP] = "FF_RAMP", + [FF_SQUARE] = "FF_SQUARE", + [FF_TRIANGLE] = "FF_TRIANGLE", + [FF_SINE] = "FF_SINE", + [FF_SAW_UP] = "FF_SAW_UP", + [FF_SAW_DOWN] = "FF_SAW_DOWN", + [FF_CUSTOM] = "FF_CUSTOM", + [FF_GAIN] = "FF_GAIN", + [FF_AUTOCENTER] = "FF_AUTOCENTER", + [FF_MAX] = "FF_MAX", +}; + +static const char *const syn_map[SYN_MAX + 1] = { + [SYN_REPORT] = "SYN_REPORT", + [SYN_CONFIG] = "SYN_CONFIG", + [SYN_MT_REPORT] = "SYN_MT_REPORT", + [SYN_DROPPED] = "SYN_DROPPED", + [SYN_MAX] = "SYN_MAX", +}; + +static const char *const rep_map[REP_MAX + 1] = { + [REP_DELAY] = "REP_DELAY", + [REP_PERIOD] = "REP_PERIOD", +}; + +static const char *const input_prop_map[INPUT_PROP_MAX + 1] = { + [INPUT_PROP_POINTER] = "INPUT_PROP_POINTER", + [INPUT_PROP_DIRECT] = "INPUT_PROP_DIRECT", + [INPUT_PROP_BUTTONPAD] = "INPUT_PROP_BUTTONPAD", + [INPUT_PROP_SEMI_MT] = "INPUT_PROP_SEMI_MT", + [INPUT_PROP_TOPBUTTONPAD] = "INPUT_PROP_TOPBUTTONPAD", + [INPUT_PROP_POINTING_STICK] = "INPUT_PROP_POINTING_STICK", + [INPUT_PROP_ACCELEROMETER] = "INPUT_PROP_ACCELEROMETER", + [INPUT_PROP_MAX] = "INPUT_PROP_MAX", +}; + +static const char *const mt_tool_map[MT_TOOL_MAX + 1] = { + [MT_TOOL_FINGER] = "MT_TOOL_FINGER", + [MT_TOOL_PEN] = "MT_TOOL_PEN", + [MT_TOOL_PALM] = "MT_TOOL_PALM", + [MT_TOOL_DIAL] = "MT_TOOL_DIAL", + [MT_TOOL_MAX] = "MT_TOOL_MAX", +}; + +static const char *const *const event_type_map[EV_MAX + 1] = { + [EV_REL] = rel_map, + [EV_ABS] = abs_map, + [EV_KEY] = key_map, + [EV_LED] = led_map, + [EV_SND] = snd_map, + [EV_MSC] = msc_map, + [EV_SW] = sw_map, + [EV_FF] = ff_map, + [EV_SYN] = syn_map, + [EV_REP] = rep_map, +}; + +#if __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Winitializer-overrides" +#elif __GNUC__ + #pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Woverride-init" +#endif +static const int ev_max[EV_MAX + 1] = { + SYN_MAX, + KEY_MAX, + REL_MAX, + ABS_MAX, + MSC_MAX, + SW_MAX, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + LED_MAX, + SND_MAX, + -1, + REP_MAX, + FF_MAX, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, +}; +#if __clang__ +#pragma clang diagnostic pop /* "-Winitializer-overrides" */ +#elif __GNUC__ +#pragma GCC diagnostic pop /* "-Woverride-init" */ +#endif + +struct name_entry { + const char *name; + unsigned int value; +}; + +static const struct name_entry tool_type_names[] = { + {.name = "MT_TOOL_DIAL", .value = MT_TOOL_DIAL}, + {.name = "MT_TOOL_FINGER", .value = MT_TOOL_FINGER}, + {.name = "MT_TOOL_MAX", .value = MT_TOOL_MAX}, + {.name = "MT_TOOL_PALM", .value = MT_TOOL_PALM}, + {.name = "MT_TOOL_PEN", .value = MT_TOOL_PEN}, +}; + +static const struct name_entry ev_names[] = { + {.name = "EV_ABS", .value = EV_ABS}, + {.name = "EV_FF", .value = EV_FF}, + {.name = "EV_FF_STATUS", .value = EV_FF_STATUS}, + {.name = "EV_KEY", .value = EV_KEY}, + {.name = "EV_LED", .value = EV_LED}, + {.name = "EV_MAX", .value = EV_MAX}, + {.name = "EV_MSC", .value = EV_MSC}, + {.name = "EV_PWR", .value = EV_PWR}, + {.name = "EV_REL", .value = EV_REL}, + {.name = "EV_REP", .value = EV_REP}, + {.name = "EV_SND", .value = EV_SND}, + {.name = "EV_SW", .value = EV_SW}, + {.name = "EV_SYN", .value = EV_SYN}, +}; + +static const struct name_entry code_names[] = { + {.name = "ABS_BRAKE", .value = ABS_BRAKE}, + {.name = "ABS_DISTANCE", .value = ABS_DISTANCE}, + {.name = "ABS_GAS", .value = ABS_GAS}, + {.name = "ABS_HAT0X", .value = ABS_HAT0X}, + {.name = "ABS_HAT0Y", .value = ABS_HAT0Y}, + {.name = "ABS_HAT1X", .value = ABS_HAT1X}, + {.name = "ABS_HAT1Y", .value = ABS_HAT1Y}, + {.name = "ABS_HAT2X", .value = ABS_HAT2X}, + {.name = "ABS_HAT2Y", .value = ABS_HAT2Y}, + {.name = "ABS_HAT3X", .value = ABS_HAT3X}, + {.name = "ABS_HAT3Y", .value = ABS_HAT3Y}, + {.name = "ABS_MAX", .value = ABS_MAX}, + {.name = "ABS_MISC", .value = ABS_MISC}, + {.name = "ABS_MT_BLOB_ID", .value = ABS_MT_BLOB_ID}, + {.name = "ABS_MT_DISTANCE", .value = ABS_MT_DISTANCE}, + {.name = "ABS_MT_ORIENTATION", .value = ABS_MT_ORIENTATION}, + {.name = "ABS_MT_POSITION_X", .value = ABS_MT_POSITION_X}, + {.name = "ABS_MT_POSITION_Y", .value = ABS_MT_POSITION_Y}, + {.name = "ABS_MT_PRESSURE", .value = ABS_MT_PRESSURE}, + {.name = "ABS_MT_SLOT", .value = ABS_MT_SLOT}, + {.name = "ABS_MT_TOOL_TYPE", .value = ABS_MT_TOOL_TYPE}, + {.name = "ABS_MT_TOOL_X", .value = ABS_MT_TOOL_X}, + {.name = "ABS_MT_TOOL_Y", .value = ABS_MT_TOOL_Y}, + {.name = "ABS_MT_TOUCH_MAJOR", .value = ABS_MT_TOUCH_MAJOR}, + {.name = "ABS_MT_TOUCH_MINOR", .value = ABS_MT_TOUCH_MINOR}, + {.name = "ABS_MT_TRACKING_ID", .value = ABS_MT_TRACKING_ID}, + {.name = "ABS_MT_WIDTH_MAJOR", .value = ABS_MT_WIDTH_MAJOR}, + {.name = "ABS_MT_WIDTH_MINOR", .value = ABS_MT_WIDTH_MINOR}, + {.name = "ABS_PRESSURE", .value = ABS_PRESSURE}, + {.name = "ABS_PROFILE", .value = ABS_PROFILE}, + {.name = "ABS_RESERVED", .value = ABS_RESERVED}, + {.name = "ABS_RUDDER", .value = ABS_RUDDER}, + {.name = "ABS_RX", .value = ABS_RX}, + {.name = "ABS_RY", .value = ABS_RY}, + {.name = "ABS_RZ", .value = ABS_RZ}, + {.name = "ABS_THROTTLE", .value = ABS_THROTTLE}, + {.name = "ABS_TILT_X", .value = ABS_TILT_X}, + {.name = "ABS_TILT_Y", .value = ABS_TILT_Y}, + {.name = "ABS_TOOL_WIDTH", .value = ABS_TOOL_WIDTH}, + {.name = "ABS_VOLUME", .value = ABS_VOLUME}, + {.name = "ABS_WHEEL", .value = ABS_WHEEL}, + {.name = "ABS_X", .value = ABS_X}, + {.name = "ABS_Y", .value = ABS_Y}, + {.name = "ABS_Z", .value = ABS_Z}, + {.name = "BTN_0", .value = BTN_0}, + {.name = "BTN_1", .value = BTN_1}, + {.name = "BTN_2", .value = BTN_2}, + {.name = "BTN_3", .value = BTN_3}, + {.name = "BTN_4", .value = BTN_4}, + {.name = "BTN_5", .value = BTN_5}, + {.name = "BTN_6", .value = BTN_6}, + {.name = "BTN_7", .value = BTN_7}, + {.name = "BTN_8", .value = BTN_8}, + {.name = "BTN_9", .value = BTN_9}, + {.name = "BTN_A", .value = BTN_A}, + {.name = "BTN_B", .value = BTN_B}, + {.name = "BTN_BACK", .value = BTN_BACK}, + {.name = "BTN_BASE", .value = BTN_BASE}, + {.name = "BTN_BASE2", .value = BTN_BASE2}, + {.name = "BTN_BASE3", .value = BTN_BASE3}, + {.name = "BTN_BASE4", .value = BTN_BASE4}, + {.name = "BTN_BASE5", .value = BTN_BASE5}, + {.name = "BTN_BASE6", .value = BTN_BASE6}, + {.name = "BTN_C", .value = BTN_C}, + {.name = "BTN_DEAD", .value = BTN_DEAD}, + {.name = "BTN_DPAD_DOWN", .value = BTN_DPAD_DOWN}, + {.name = "BTN_DPAD_LEFT", .value = BTN_DPAD_LEFT}, + {.name = "BTN_DPAD_RIGHT", .value = BTN_DPAD_RIGHT}, + {.name = "BTN_DPAD_UP", .value = BTN_DPAD_UP}, + {.name = "BTN_EAST", .value = BTN_EAST}, + {.name = "BTN_EXTRA", .value = BTN_EXTRA}, + {.name = "BTN_FORWARD", .value = BTN_FORWARD}, + {.name = "BTN_GEAR_DOWN", .value = BTN_GEAR_DOWN}, + {.name = "BTN_GEAR_UP", .value = BTN_GEAR_UP}, + {.name = "BTN_LEFT", .value = BTN_LEFT}, + {.name = "BTN_MIDDLE", .value = BTN_MIDDLE}, + {.name = "BTN_MODE", .value = BTN_MODE}, + {.name = "BTN_NORTH", .value = BTN_NORTH}, + {.name = "BTN_PINKIE", .value = BTN_PINKIE}, + {.name = "BTN_RIGHT", .value = BTN_RIGHT}, + {.name = "BTN_SELECT", .value = BTN_SELECT}, + {.name = "BTN_SIDE", .value = BTN_SIDE}, + {.name = "BTN_SOUTH", .value = BTN_SOUTH}, + {.name = "BTN_START", .value = BTN_START}, + {.name = "BTN_STYLUS", .value = BTN_STYLUS}, + {.name = "BTN_STYLUS2", .value = BTN_STYLUS2}, + {.name = "BTN_STYLUS3", .value = BTN_STYLUS3}, + {.name = "BTN_TASK", .value = BTN_TASK}, + {.name = "BTN_THUMB", .value = BTN_THUMB}, + {.name = "BTN_THUMB2", .value = BTN_THUMB2}, + {.name = "BTN_THUMBL", .value = BTN_THUMBL}, + {.name = "BTN_THUMBR", .value = BTN_THUMBR}, + {.name = "BTN_TL", .value = BTN_TL}, + {.name = "BTN_TL2", .value = BTN_TL2}, + {.name = "BTN_TOOL_AIRBRUSH", .value = BTN_TOOL_AIRBRUSH}, + {.name = "BTN_TOOL_BRUSH", .value = BTN_TOOL_BRUSH}, + {.name = "BTN_TOOL_DOUBLETAP", .value = BTN_TOOL_DOUBLETAP}, + {.name = "BTN_TOOL_FINGER", .value = BTN_TOOL_FINGER}, + {.name = "BTN_TOOL_LENS", .value = BTN_TOOL_LENS}, + {.name = "BTN_TOOL_MOUSE", .value = BTN_TOOL_MOUSE}, + {.name = "BTN_TOOL_PEN", .value = BTN_TOOL_PEN}, + {.name = "BTN_TOOL_PENCIL", .value = BTN_TOOL_PENCIL}, + {.name = "BTN_TOOL_QUADTAP", .value = BTN_TOOL_QUADTAP}, + {.name = "BTN_TOOL_QUINTTAP", .value = BTN_TOOL_QUINTTAP}, + {.name = "BTN_TOOL_RUBBER", .value = BTN_TOOL_RUBBER}, + {.name = "BTN_TOOL_TRIPLETAP", .value = BTN_TOOL_TRIPLETAP}, + {.name = "BTN_TOP", .value = BTN_TOP}, + {.name = "BTN_TOP2", .value = BTN_TOP2}, + {.name = "BTN_TOUCH", .value = BTN_TOUCH}, + {.name = "BTN_TR", .value = BTN_TR}, + {.name = "BTN_TR2", .value = BTN_TR2}, + {.name = "BTN_TRIGGER", .value = BTN_TRIGGER}, + {.name = "BTN_TRIGGER_HAPPY1", .value = BTN_TRIGGER_HAPPY1}, + {.name = "BTN_TRIGGER_HAPPY10", .value = BTN_TRIGGER_HAPPY10}, + {.name = "BTN_TRIGGER_HAPPY11", .value = BTN_TRIGGER_HAPPY11}, + {.name = "BTN_TRIGGER_HAPPY12", .value = BTN_TRIGGER_HAPPY12}, + {.name = "BTN_TRIGGER_HAPPY13", .value = BTN_TRIGGER_HAPPY13}, + {.name = "BTN_TRIGGER_HAPPY14", .value = BTN_TRIGGER_HAPPY14}, + {.name = "BTN_TRIGGER_HAPPY15", .value = BTN_TRIGGER_HAPPY15}, + {.name = "BTN_TRIGGER_HAPPY16", .value = BTN_TRIGGER_HAPPY16}, + {.name = "BTN_TRIGGER_HAPPY17", .value = BTN_TRIGGER_HAPPY17}, + {.name = "BTN_TRIGGER_HAPPY18", .value = BTN_TRIGGER_HAPPY18}, + {.name = "BTN_TRIGGER_HAPPY19", .value = BTN_TRIGGER_HAPPY19}, + {.name = "BTN_TRIGGER_HAPPY2", .value = BTN_TRIGGER_HAPPY2}, + {.name = "BTN_TRIGGER_HAPPY20", .value = BTN_TRIGGER_HAPPY20}, + {.name = "BTN_TRIGGER_HAPPY21", .value = BTN_TRIGGER_HAPPY21}, + {.name = "BTN_TRIGGER_HAPPY22", .value = BTN_TRIGGER_HAPPY22}, + {.name = "BTN_TRIGGER_HAPPY23", .value = BTN_TRIGGER_HAPPY23}, + {.name = "BTN_TRIGGER_HAPPY24", .value = BTN_TRIGGER_HAPPY24}, + {.name = "BTN_TRIGGER_HAPPY25", .value = BTN_TRIGGER_HAPPY25}, + {.name = "BTN_TRIGGER_HAPPY26", .value = BTN_TRIGGER_HAPPY26}, + {.name = "BTN_TRIGGER_HAPPY27", .value = BTN_TRIGGER_HAPPY27}, + {.name = "BTN_TRIGGER_HAPPY28", .value = BTN_TRIGGER_HAPPY28}, + {.name = "BTN_TRIGGER_HAPPY29", .value = BTN_TRIGGER_HAPPY29}, + {.name = "BTN_TRIGGER_HAPPY3", .value = BTN_TRIGGER_HAPPY3}, + {.name = "BTN_TRIGGER_HAPPY30", .value = BTN_TRIGGER_HAPPY30}, + {.name = "BTN_TRIGGER_HAPPY31", .value = BTN_TRIGGER_HAPPY31}, + {.name = "BTN_TRIGGER_HAPPY32", .value = BTN_TRIGGER_HAPPY32}, + {.name = "BTN_TRIGGER_HAPPY33", .value = BTN_TRIGGER_HAPPY33}, + {.name = "BTN_TRIGGER_HAPPY34", .value = BTN_TRIGGER_HAPPY34}, + {.name = "BTN_TRIGGER_HAPPY35", .value = BTN_TRIGGER_HAPPY35}, + {.name = "BTN_TRIGGER_HAPPY36", .value = BTN_TRIGGER_HAPPY36}, + {.name = "BTN_TRIGGER_HAPPY37", .value = BTN_TRIGGER_HAPPY37}, + {.name = "BTN_TRIGGER_HAPPY38", .value = BTN_TRIGGER_HAPPY38}, + {.name = "BTN_TRIGGER_HAPPY39", .value = BTN_TRIGGER_HAPPY39}, + {.name = "BTN_TRIGGER_HAPPY4", .value = BTN_TRIGGER_HAPPY4}, + {.name = "BTN_TRIGGER_HAPPY40", .value = BTN_TRIGGER_HAPPY40}, + {.name = "BTN_TRIGGER_HAPPY5", .value = BTN_TRIGGER_HAPPY5}, + {.name = "BTN_TRIGGER_HAPPY6", .value = BTN_TRIGGER_HAPPY6}, + {.name = "BTN_TRIGGER_HAPPY7", .value = BTN_TRIGGER_HAPPY7}, + {.name = "BTN_TRIGGER_HAPPY8", .value = BTN_TRIGGER_HAPPY8}, + {.name = "BTN_TRIGGER_HAPPY9", .value = BTN_TRIGGER_HAPPY9}, + {.name = "BTN_WEST", .value = BTN_WEST}, + {.name = "BTN_X", .value = BTN_X}, + {.name = "BTN_Y", .value = BTN_Y}, + {.name = "BTN_Z", .value = BTN_Z}, + {.name = "FF_AUTOCENTER", .value = FF_AUTOCENTER}, + {.name = "FF_CONSTANT", .value = FF_CONSTANT}, + {.name = "FF_CUSTOM", .value = FF_CUSTOM}, + {.name = "FF_DAMPER", .value = FF_DAMPER}, + {.name = "FF_FRICTION", .value = FF_FRICTION}, + {.name = "FF_GAIN", .value = FF_GAIN}, + {.name = "FF_INERTIA", .value = FF_INERTIA}, + {.name = "FF_MAX", .value = FF_MAX}, + {.name = "FF_PERIODIC", .value = FF_PERIODIC}, + {.name = "FF_RAMP", .value = FF_RAMP}, + {.name = "FF_RUMBLE", .value = FF_RUMBLE}, + {.name = "FF_SAW_DOWN", .value = FF_SAW_DOWN}, + {.name = "FF_SAW_UP", .value = FF_SAW_UP}, + {.name = "FF_SINE", .value = FF_SINE}, + {.name = "FF_SPRING", .value = FF_SPRING}, + {.name = "FF_SQUARE", .value = FF_SQUARE}, + {.name = "FF_STATUS_MAX", .value = FF_STATUS_MAX}, + {.name = "FF_STATUS_STOPPED", .value = FF_STATUS_STOPPED}, + {.name = "FF_TRIANGLE", .value = FF_TRIANGLE}, + {.name = "KEY_0", .value = KEY_0}, + {.name = "KEY_1", .value = KEY_1}, + {.name = "KEY_102ND", .value = KEY_102ND}, + {.name = "KEY_10CHANNELSDOWN", .value = KEY_10CHANNELSDOWN}, + {.name = "KEY_10CHANNELSUP", .value = KEY_10CHANNELSUP}, + {.name = "KEY_2", .value = KEY_2}, + {.name = "KEY_3", .value = KEY_3}, + {.name = "KEY_3D_MODE", .value = KEY_3D_MODE}, + {.name = "KEY_4", .value = KEY_4}, + {.name = "KEY_5", .value = KEY_5}, + {.name = "KEY_6", .value = KEY_6}, + {.name = "KEY_7", .value = KEY_7}, + {.name = "KEY_8", .value = KEY_8}, + {.name = "KEY_9", .value = KEY_9}, + {.name = "KEY_A", .value = KEY_A}, + {.name = "KEY_AB", .value = KEY_AB}, + {.name = "KEY_ADDRESSBOOK", .value = KEY_ADDRESSBOOK}, + {.name = "KEY_AGAIN", .value = KEY_AGAIN}, + {.name = "KEY_ALL_APPLICATIONS", .value = KEY_ALL_APPLICATIONS}, + {.name = "KEY_ALS_TOGGLE", .value = KEY_ALS_TOGGLE}, + {.name = "KEY_ALTERASE", .value = KEY_ALTERASE}, + {.name = "KEY_ANGLE", .value = KEY_ANGLE}, + {.name = "KEY_APOSTROPHE", .value = KEY_APOSTROPHE}, + {.name = "KEY_APPSELECT", .value = KEY_APPSELECT}, + {.name = "KEY_ARCHIVE", .value = KEY_ARCHIVE}, + {.name = "KEY_ASPECT_RATIO", .value = KEY_ASPECT_RATIO}, + {.name = "KEY_ASSISTANT", .value = KEY_ASSISTANT}, + {.name = "KEY_ATTENDANT_OFF", .value = KEY_ATTENDANT_OFF}, + {.name = "KEY_ATTENDANT_ON", .value = KEY_ATTENDANT_ON}, + {.name = "KEY_ATTENDANT_TOGGLE", .value = KEY_ATTENDANT_TOGGLE}, + {.name = "KEY_AUDIO", .value = KEY_AUDIO}, + {.name = "KEY_AUDIO_DESC", .value = KEY_AUDIO_DESC}, + {.name = "KEY_AUTOPILOT_ENGAGE_TOGGLE", .value = KEY_AUTOPILOT_ENGAGE_TOGGLE}, + {.name = "KEY_AUX", .value = KEY_AUX}, + {.name = "KEY_B", .value = KEY_B}, + {.name = "KEY_BACK", .value = KEY_BACK}, + {.name = "KEY_BACKSLASH", .value = KEY_BACKSLASH}, + {.name = "KEY_BACKSPACE", .value = KEY_BACKSPACE}, + {.name = "KEY_BASSBOOST", .value = KEY_BASSBOOST}, + {.name = "KEY_BATTERY", .value = KEY_BATTERY}, + {.name = "KEY_BLUE", .value = KEY_BLUE}, + {.name = "KEY_BLUETOOTH", .value = KEY_BLUETOOTH}, + {.name = "KEY_BOOKMARKS", .value = KEY_BOOKMARKS}, + {.name = "KEY_BREAK", .value = KEY_BREAK}, + {.name = "KEY_BRIGHTNESSDOWN", .value = KEY_BRIGHTNESSDOWN}, + {.name = "KEY_BRIGHTNESSUP", .value = KEY_BRIGHTNESSUP}, + {.name = "KEY_BRIGHTNESS_AUTO", .value = KEY_BRIGHTNESS_AUTO}, + {.name = "KEY_BRIGHTNESS_CYCLE", .value = KEY_BRIGHTNESS_CYCLE}, + {.name = "KEY_BRIGHTNESS_MAX", .value = KEY_BRIGHTNESS_MAX}, + {.name = "KEY_BRIGHTNESS_MENU", .value = KEY_BRIGHTNESS_MENU}, + {.name = "KEY_BRIGHTNESS_MIN", .value = KEY_BRIGHTNESS_MIN}, + {.name = "KEY_BRL_DOT1", .value = KEY_BRL_DOT1}, + {.name = "KEY_BRL_DOT10", .value = KEY_BRL_DOT10}, + {.name = "KEY_BRL_DOT2", .value = KEY_BRL_DOT2}, + {.name = "KEY_BRL_DOT3", .value = KEY_BRL_DOT3}, + {.name = "KEY_BRL_DOT4", .value = KEY_BRL_DOT4}, + {.name = "KEY_BRL_DOT5", .value = KEY_BRL_DOT5}, + {.name = "KEY_BRL_DOT6", .value = KEY_BRL_DOT6}, + {.name = "KEY_BRL_DOT7", .value = KEY_BRL_DOT7}, + {.name = "KEY_BRL_DOT8", .value = KEY_BRL_DOT8}, + {.name = "KEY_BRL_DOT9", .value = KEY_BRL_DOT9}, + {.name = "KEY_BUTTONCONFIG", .value = KEY_BUTTONCONFIG}, + {.name = "KEY_C", .value = KEY_C}, + {.name = "KEY_CALC", .value = KEY_CALC}, + {.name = "KEY_CALENDAR", .value = KEY_CALENDAR}, + {.name = "KEY_CAMERA", .value = KEY_CAMERA}, + {.name = "KEY_CAMERA_ACCESS_DISABLE", .value = KEY_CAMERA_ACCESS_DISABLE}, + {.name = "KEY_CAMERA_ACCESS_ENABLE", .value = KEY_CAMERA_ACCESS_ENABLE}, + {.name = "KEY_CAMERA_ACCESS_TOGGLE", .value = KEY_CAMERA_ACCESS_TOGGLE}, + {.name = "KEY_CAMERA_DOWN", .value = KEY_CAMERA_DOWN}, + {.name = "KEY_CAMERA_FOCUS", .value = KEY_CAMERA_FOCUS}, + {.name = "KEY_CAMERA_LEFT", .value = KEY_CAMERA_LEFT}, + {.name = "KEY_CAMERA_RIGHT", .value = KEY_CAMERA_RIGHT}, + {.name = "KEY_CAMERA_UP", .value = KEY_CAMERA_UP}, + {.name = "KEY_CAMERA_ZOOMIN", .value = KEY_CAMERA_ZOOMIN}, + {.name = "KEY_CAMERA_ZOOMOUT", .value = KEY_CAMERA_ZOOMOUT}, + {.name = "KEY_CANCEL", .value = KEY_CANCEL}, + {.name = "KEY_CAPSLOCK", .value = KEY_CAPSLOCK}, + {.name = "KEY_CD", .value = KEY_CD}, + {.name = "KEY_CHANNEL", .value = KEY_CHANNEL}, + {.name = "KEY_CHANNELDOWN", .value = KEY_CHANNELDOWN}, + {.name = "KEY_CHANNELUP", .value = KEY_CHANNELUP}, + {.name = "KEY_CHAT", .value = KEY_CHAT}, + {.name = "KEY_CLEAR", .value = KEY_CLEAR}, + {.name = "KEY_CLEARVU_SONAR", .value = KEY_CLEARVU_SONAR}, + {.name = "KEY_CLOSE", .value = KEY_CLOSE}, + {.name = "KEY_CLOSECD", .value = KEY_CLOSECD}, + {.name = "KEY_COFFEE", .value = KEY_COFFEE}, + {.name = "KEY_COMMA", .value = KEY_COMMA}, + {.name = "KEY_COMPOSE", .value = KEY_COMPOSE}, + {.name = "KEY_COMPUTER", .value = KEY_COMPUTER}, + {.name = "KEY_CONFIG", .value = KEY_CONFIG}, + {.name = "KEY_CONNECT", .value = KEY_CONNECT}, + {.name = "KEY_CONTEXT_MENU", .value = KEY_CONTEXT_MENU}, + {.name = "KEY_CONTROLPANEL", .value = KEY_CONTROLPANEL}, + {.name = "KEY_COPY", .value = KEY_COPY}, + {.name = "KEY_CUT", .value = KEY_CUT}, + {.name = "KEY_CYCLEWINDOWS", .value = KEY_CYCLEWINDOWS}, + {.name = "KEY_D", .value = KEY_D}, + {.name = "KEY_DATA", .value = KEY_DATA}, + {.name = "KEY_DATABASE", .value = KEY_DATABASE}, + {.name = "KEY_DELETE", .value = KEY_DELETE}, + {.name = "KEY_DELETEFILE", .value = KEY_DELETEFILE}, + {.name = "KEY_DEL_EOL", .value = KEY_DEL_EOL}, + {.name = "KEY_DEL_EOS", .value = KEY_DEL_EOS}, + {.name = "KEY_DEL_LINE", .value = KEY_DEL_LINE}, + {.name = "KEY_DICTATE", .value = KEY_DICTATE}, + {.name = "KEY_DIGITS", .value = KEY_DIGITS}, + {.name = "KEY_DIRECTORY", .value = KEY_DIRECTORY}, + {.name = "KEY_DISPLAYTOGGLE", .value = KEY_DISPLAYTOGGLE}, + {.name = "KEY_DISPLAY_OFF", .value = KEY_DISPLAY_OFF}, + {.name = "KEY_DOCUMENTS", .value = KEY_DOCUMENTS}, + {.name = "KEY_DOLLAR", .value = KEY_DOLLAR}, + {.name = "KEY_DOT", .value = KEY_DOT}, + {.name = "KEY_DOWN", .value = KEY_DOWN}, + {.name = "KEY_DUAL_RANGE_RADAR", .value = KEY_DUAL_RANGE_RADAR}, + {.name = "KEY_DVD", .value = KEY_DVD}, + {.name = "KEY_E", .value = KEY_E}, + {.name = "KEY_EDIT", .value = KEY_EDIT}, + {.name = "KEY_EDITOR", .value = KEY_EDITOR}, + {.name = "KEY_EJECTCD", .value = KEY_EJECTCD}, + {.name = "KEY_EJECTCLOSECD", .value = KEY_EJECTCLOSECD}, + {.name = "KEY_EMAIL", .value = KEY_EMAIL}, + {.name = "KEY_EMOJI_PICKER", .value = KEY_EMOJI_PICKER}, + {.name = "KEY_END", .value = KEY_END}, + {.name = "KEY_ENTER", .value = KEY_ENTER}, + {.name = "KEY_EPG", .value = KEY_EPG}, + {.name = "KEY_EQUAL", .value = KEY_EQUAL}, + {.name = "KEY_ESC", .value = KEY_ESC}, + {.name = "KEY_EURO", .value = KEY_EURO}, + {.name = "KEY_EXIT", .value = KEY_EXIT}, + {.name = "KEY_F", .value = KEY_F}, + {.name = "KEY_F1", .value = KEY_F1}, + {.name = "KEY_F10", .value = KEY_F10}, + {.name = "KEY_F11", .value = KEY_F11}, + {.name = "KEY_F12", .value = KEY_F12}, + {.name = "KEY_F13", .value = KEY_F13}, + {.name = "KEY_F14", .value = KEY_F14}, + {.name = "KEY_F15", .value = KEY_F15}, + {.name = "KEY_F16", .value = KEY_F16}, + {.name = "KEY_F17", .value = KEY_F17}, + {.name = "KEY_F18", .value = KEY_F18}, + {.name = "KEY_F19", .value = KEY_F19}, + {.name = "KEY_F2", .value = KEY_F2}, + {.name = "KEY_F20", .value = KEY_F20}, + {.name = "KEY_F21", .value = KEY_F21}, + {.name = "KEY_F22", .value = KEY_F22}, + {.name = "KEY_F23", .value = KEY_F23}, + {.name = "KEY_F24", .value = KEY_F24}, + {.name = "KEY_F3", .value = KEY_F3}, + {.name = "KEY_F4", .value = KEY_F4}, + {.name = "KEY_F5", .value = KEY_F5}, + {.name = "KEY_F6", .value = KEY_F6}, + {.name = "KEY_F7", .value = KEY_F7}, + {.name = "KEY_F8", .value = KEY_F8}, + {.name = "KEY_F9", .value = KEY_F9}, + {.name = "KEY_FASTFORWARD", .value = KEY_FASTFORWARD}, + {.name = "KEY_FASTREVERSE", .value = KEY_FASTREVERSE}, + {.name = "KEY_FAVORITES", .value = KEY_FAVORITES}, + {.name = "KEY_FILE", .value = KEY_FILE}, + {.name = "KEY_FINANCE", .value = KEY_FINANCE}, + {.name = "KEY_FIND", .value = KEY_FIND}, + {.name = "KEY_FIRST", .value = KEY_FIRST}, + {.name = "KEY_FISHING_CHART", .value = KEY_FISHING_CHART}, + {.name = "KEY_FN", .value = KEY_FN}, + {.name = "KEY_FN_1", .value = KEY_FN_1}, + {.name = "KEY_FN_2", .value = KEY_FN_2}, + {.name = "KEY_FN_B", .value = KEY_FN_B}, + {.name = "KEY_FN_D", .value = KEY_FN_D}, + {.name = "KEY_FN_E", .value = KEY_FN_E}, + {.name = "KEY_FN_ESC", .value = KEY_FN_ESC}, + {.name = "KEY_FN_F", .value = KEY_FN_F}, + {.name = "KEY_FN_F1", .value = KEY_FN_F1}, + {.name = "KEY_FN_F10", .value = KEY_FN_F10}, + {.name = "KEY_FN_F11", .value = KEY_FN_F11}, + {.name = "KEY_FN_F12", .value = KEY_FN_F12}, + {.name = "KEY_FN_F2", .value = KEY_FN_F2}, + {.name = "KEY_FN_F3", .value = KEY_FN_F3}, + {.name = "KEY_FN_F4", .value = KEY_FN_F4}, + {.name = "KEY_FN_F5", .value = KEY_FN_F5}, + {.name = "KEY_FN_F6", .value = KEY_FN_F6}, + {.name = "KEY_FN_F7", .value = KEY_FN_F7}, + {.name = "KEY_FN_F8", .value = KEY_FN_F8}, + {.name = "KEY_FN_F9", .value = KEY_FN_F9}, + {.name = "KEY_FN_RIGHT_SHIFT", .value = KEY_FN_RIGHT_SHIFT}, + {.name = "KEY_FN_S", .value = KEY_FN_S}, + {.name = "KEY_FORWARD", .value = KEY_FORWARD}, + {.name = "KEY_FORWARDMAIL", .value = KEY_FORWARDMAIL}, + {.name = "KEY_FRAMEBACK", .value = KEY_FRAMEBACK}, + {.name = "KEY_FRAMEFORWARD", .value = KEY_FRAMEFORWARD}, + {.name = "KEY_FRONT", .value = KEY_FRONT}, + {.name = "KEY_FULL_SCREEN", .value = KEY_FULL_SCREEN}, + {.name = "KEY_G", .value = KEY_G}, + {.name = "KEY_GAMES", .value = KEY_GAMES}, + {.name = "KEY_GOTO", .value = KEY_GOTO}, + {.name = "KEY_GRAPHICSEDITOR", .value = KEY_GRAPHICSEDITOR}, + {.name = "KEY_GRAVE", .value = KEY_GRAVE}, + {.name = "KEY_GREEN", .value = KEY_GREEN}, + {.name = "KEY_H", .value = KEY_H}, + {.name = "KEY_HANGEUL", .value = KEY_HANGEUL}, + {.name = "KEY_HANGUP_PHONE", .value = KEY_HANGUP_PHONE}, + {.name = "KEY_HANJA", .value = KEY_HANJA}, + {.name = "KEY_HELP", .value = KEY_HELP}, + {.name = "KEY_HENKAN", .value = KEY_HENKAN}, + {.name = "KEY_HIRAGANA", .value = KEY_HIRAGANA}, + {.name = "KEY_HOME", .value = KEY_HOME}, + {.name = "KEY_HOMEPAGE", .value = KEY_HOMEPAGE}, + {.name = "KEY_HP", .value = KEY_HP}, + {.name = "KEY_I", .value = KEY_I}, + {.name = "KEY_IMAGES", .value = KEY_IMAGES}, + {.name = "KEY_INFO", .value = KEY_INFO}, + {.name = "KEY_INSERT", .value = KEY_INSERT}, + {.name = "KEY_INS_LINE", .value = KEY_INS_LINE}, + {.name = "KEY_ISO", .value = KEY_ISO}, + {.name = "KEY_J", .value = KEY_J}, + {.name = "KEY_JOURNAL", .value = KEY_JOURNAL}, + {.name = "KEY_K", .value = KEY_K}, + {.name = "KEY_KATAKANA", .value = KEY_KATAKANA}, + {.name = "KEY_KATAKANAHIRAGANA", .value = KEY_KATAKANAHIRAGANA}, + {.name = "KEY_KBDILLUMDOWN", .value = KEY_KBDILLUMDOWN}, + {.name = "KEY_KBDILLUMTOGGLE", .value = KEY_KBDILLUMTOGGLE}, + {.name = "KEY_KBDILLUMUP", .value = KEY_KBDILLUMUP}, + {.name = "KEY_KBDINPUTASSIST_ACCEPT", .value = KEY_KBDINPUTASSIST_ACCEPT}, + {.name = "KEY_KBDINPUTASSIST_CANCEL", .value = KEY_KBDINPUTASSIST_CANCEL}, + {.name = "KEY_KBDINPUTASSIST_NEXT", .value = KEY_KBDINPUTASSIST_NEXT}, + {.name = "KEY_KBDINPUTASSIST_NEXTGROUP", .value = KEY_KBDINPUTASSIST_NEXTGROUP}, + {.name = "KEY_KBDINPUTASSIST_PREV", .value = KEY_KBDINPUTASSIST_PREV}, + {.name = "KEY_KBDINPUTASSIST_PREVGROUP", .value = KEY_KBDINPUTASSIST_PREVGROUP}, + {.name = "KEY_KBD_LAYOUT_NEXT", .value = KEY_KBD_LAYOUT_NEXT}, + {.name = "KEY_KBD_LCD_MENU1", .value = KEY_KBD_LCD_MENU1}, + {.name = "KEY_KBD_LCD_MENU2", .value = KEY_KBD_LCD_MENU2}, + {.name = "KEY_KBD_LCD_MENU3", .value = KEY_KBD_LCD_MENU3}, + {.name = "KEY_KBD_LCD_MENU4", .value = KEY_KBD_LCD_MENU4}, + {.name = "KEY_KBD_LCD_MENU5", .value = KEY_KBD_LCD_MENU5}, + {.name = "KEY_KEYBOARD", .value = KEY_KEYBOARD}, + {.name = "KEY_KP0", .value = KEY_KP0}, + {.name = "KEY_KP1", .value = KEY_KP1}, + {.name = "KEY_KP2", .value = KEY_KP2}, + {.name = "KEY_KP3", .value = KEY_KP3}, + {.name = "KEY_KP4", .value = KEY_KP4}, + {.name = "KEY_KP5", .value = KEY_KP5}, + {.name = "KEY_KP6", .value = KEY_KP6}, + {.name = "KEY_KP7", .value = KEY_KP7}, + {.name = "KEY_KP8", .value = KEY_KP8}, + {.name = "KEY_KP9", .value = KEY_KP9}, + {.name = "KEY_KPASTERISK", .value = KEY_KPASTERISK}, + {.name = "KEY_KPCOMMA", .value = KEY_KPCOMMA}, + {.name = "KEY_KPDOT", .value = KEY_KPDOT}, + {.name = "KEY_KPENTER", .value = KEY_KPENTER}, + {.name = "KEY_KPEQUAL", .value = KEY_KPEQUAL}, + {.name = "KEY_KPJPCOMMA", .value = KEY_KPJPCOMMA}, + {.name = "KEY_KPLEFTPAREN", .value = KEY_KPLEFTPAREN}, + {.name = "KEY_KPMINUS", .value = KEY_KPMINUS}, + {.name = "KEY_KPPLUS", .value = KEY_KPPLUS}, + {.name = "KEY_KPPLUSMINUS", .value = KEY_KPPLUSMINUS}, + {.name = "KEY_KPRIGHTPAREN", .value = KEY_KPRIGHTPAREN}, + {.name = "KEY_KPSLASH", .value = KEY_KPSLASH}, + {.name = "KEY_L", .value = KEY_L}, + {.name = "KEY_LANGUAGE", .value = KEY_LANGUAGE}, + {.name = "KEY_LAST", .value = KEY_LAST}, + {.name = "KEY_LEFT", .value = KEY_LEFT}, + {.name = "KEY_LEFTALT", .value = KEY_LEFTALT}, + {.name = "KEY_LEFTBRACE", .value = KEY_LEFTBRACE}, + {.name = "KEY_LEFTCTRL", .value = KEY_LEFTCTRL}, + {.name = "KEY_LEFTMETA", .value = KEY_LEFTMETA}, + {.name = "KEY_LEFTSHIFT", .value = KEY_LEFTSHIFT}, + {.name = "KEY_LEFT_DOWN", .value = KEY_LEFT_DOWN}, + {.name = "KEY_LEFT_UP", .value = KEY_LEFT_UP}, + {.name = "KEY_LIGHTS_TOGGLE", .value = KEY_LIGHTS_TOGGLE}, + {.name = "KEY_LINEFEED", .value = KEY_LINEFEED}, + {.name = "KEY_LIST", .value = KEY_LIST}, + {.name = "KEY_LOGOFF", .value = KEY_LOGOFF}, + {.name = "KEY_M", .value = KEY_M}, + {.name = "KEY_MACRO", .value = KEY_MACRO}, + {.name = "KEY_MACRO1", .value = KEY_MACRO1}, + {.name = "KEY_MACRO10", .value = KEY_MACRO10}, + {.name = "KEY_MACRO11", .value = KEY_MACRO11}, + {.name = "KEY_MACRO12", .value = KEY_MACRO12}, + {.name = "KEY_MACRO13", .value = KEY_MACRO13}, + {.name = "KEY_MACRO14", .value = KEY_MACRO14}, + {.name = "KEY_MACRO15", .value = KEY_MACRO15}, + {.name = "KEY_MACRO16", .value = KEY_MACRO16}, + {.name = "KEY_MACRO17", .value = KEY_MACRO17}, + {.name = "KEY_MACRO18", .value = KEY_MACRO18}, + {.name = "KEY_MACRO19", .value = KEY_MACRO19}, + {.name = "KEY_MACRO2", .value = KEY_MACRO2}, + {.name = "KEY_MACRO20", .value = KEY_MACRO20}, + {.name = "KEY_MACRO21", .value = KEY_MACRO21}, + {.name = "KEY_MACRO22", .value = KEY_MACRO22}, + {.name = "KEY_MACRO23", .value = KEY_MACRO23}, + {.name = "KEY_MACRO24", .value = KEY_MACRO24}, + {.name = "KEY_MACRO25", .value = KEY_MACRO25}, + {.name = "KEY_MACRO26", .value = KEY_MACRO26}, + {.name = "KEY_MACRO27", .value = KEY_MACRO27}, + {.name = "KEY_MACRO28", .value = KEY_MACRO28}, + {.name = "KEY_MACRO29", .value = KEY_MACRO29}, + {.name = "KEY_MACRO3", .value = KEY_MACRO3}, + {.name = "KEY_MACRO30", .value = KEY_MACRO30}, + {.name = "KEY_MACRO4", .value = KEY_MACRO4}, + {.name = "KEY_MACRO5", .value = KEY_MACRO5}, + {.name = "KEY_MACRO6", .value = KEY_MACRO6}, + {.name = "KEY_MACRO7", .value = KEY_MACRO7}, + {.name = "KEY_MACRO8", .value = KEY_MACRO8}, + {.name = "KEY_MACRO9", .value = KEY_MACRO9}, + {.name = "KEY_MACRO_PRESET1", .value = KEY_MACRO_PRESET1}, + {.name = "KEY_MACRO_PRESET2", .value = KEY_MACRO_PRESET2}, + {.name = "KEY_MACRO_PRESET3", .value = KEY_MACRO_PRESET3}, + {.name = "KEY_MACRO_PRESET_CYCLE", .value = KEY_MACRO_PRESET_CYCLE}, + {.name = "KEY_MACRO_RECORD_START", .value = KEY_MACRO_RECORD_START}, + {.name = "KEY_MACRO_RECORD_STOP", .value = KEY_MACRO_RECORD_STOP}, + {.name = "KEY_MAIL", .value = KEY_MAIL}, + {.name = "KEY_MARK_WAYPOINT", .value = KEY_MARK_WAYPOINT}, + {.name = "KEY_MAX", .value = KEY_MAX}, + {.name = "KEY_MEDIA", .value = KEY_MEDIA}, + {.name = "KEY_MEDIA_REPEAT", .value = KEY_MEDIA_REPEAT}, + {.name = "KEY_MEDIA_TOP_MENU", .value = KEY_MEDIA_TOP_MENU}, + {.name = "KEY_MEMO", .value = KEY_MEMO}, + {.name = "KEY_MENU", .value = KEY_MENU}, + {.name = "KEY_MESSENGER", .value = KEY_MESSENGER}, + {.name = "KEY_MHP", .value = KEY_MHP}, + {.name = "KEY_MICMUTE", .value = KEY_MICMUTE}, + {.name = "KEY_MINUS", .value = KEY_MINUS}, + {.name = "KEY_MODE", .value = KEY_MODE}, + {.name = "KEY_MOVE", .value = KEY_MOVE}, + {.name = "KEY_MP3", .value = KEY_MP3}, + {.name = "KEY_MSDOS", .value = KEY_MSDOS}, + {.name = "KEY_MUHENKAN", .value = KEY_MUHENKAN}, + {.name = "KEY_MUTE", .value = KEY_MUTE}, + {.name = "KEY_N", .value = KEY_N}, + {.name = "KEY_NAV_CHART", .value = KEY_NAV_CHART}, + {.name = "KEY_NAV_INFO", .value = KEY_NAV_INFO}, + {.name = "KEY_NEW", .value = KEY_NEW}, + {.name = "KEY_NEWS", .value = KEY_NEWS}, + {.name = "KEY_NEXT", .value = KEY_NEXT}, + {.name = "KEY_NEXTSONG", .value = KEY_NEXTSONG}, + {.name = "KEY_NEXT_ELEMENT", .value = KEY_NEXT_ELEMENT}, + {.name = "KEY_NEXT_FAVORITE", .value = KEY_NEXT_FAVORITE}, + {.name = "KEY_NOTIFICATION_CENTER", .value = KEY_NOTIFICATION_CENTER}, + {.name = "KEY_NUMERIC_0", .value = KEY_NUMERIC_0}, + {.name = "KEY_NUMERIC_1", .value = KEY_NUMERIC_1}, + {.name = "KEY_NUMERIC_11", .value = KEY_NUMERIC_11}, + {.name = "KEY_NUMERIC_12", .value = KEY_NUMERIC_12}, + {.name = "KEY_NUMERIC_2", .value = KEY_NUMERIC_2}, + {.name = "KEY_NUMERIC_3", .value = KEY_NUMERIC_3}, + {.name = "KEY_NUMERIC_4", .value = KEY_NUMERIC_4}, + {.name = "KEY_NUMERIC_5", .value = KEY_NUMERIC_5}, + {.name = "KEY_NUMERIC_6", .value = KEY_NUMERIC_6}, + {.name = "KEY_NUMERIC_7", .value = KEY_NUMERIC_7}, + {.name = "KEY_NUMERIC_8", .value = KEY_NUMERIC_8}, + {.name = "KEY_NUMERIC_9", .value = KEY_NUMERIC_9}, + {.name = "KEY_NUMERIC_A", .value = KEY_NUMERIC_A}, + {.name = "KEY_NUMERIC_B", .value = KEY_NUMERIC_B}, + {.name = "KEY_NUMERIC_C", .value = KEY_NUMERIC_C}, + {.name = "KEY_NUMERIC_D", .value = KEY_NUMERIC_D}, + {.name = "KEY_NUMERIC_POUND", .value = KEY_NUMERIC_POUND}, + {.name = "KEY_NUMERIC_STAR", .value = KEY_NUMERIC_STAR}, + {.name = "KEY_NUMLOCK", .value = KEY_NUMLOCK}, + {.name = "KEY_O", .value = KEY_O}, + {.name = "KEY_OK", .value = KEY_OK}, + {.name = "KEY_ONSCREEN_KEYBOARD", .value = KEY_ONSCREEN_KEYBOARD}, + {.name = "KEY_OPEN", .value = KEY_OPEN}, + {.name = "KEY_OPTION", .value = KEY_OPTION}, + {.name = "KEY_P", .value = KEY_P}, + {.name = "KEY_PAGEDOWN", .value = KEY_PAGEDOWN}, + {.name = "KEY_PAGEUP", .value = KEY_PAGEUP}, + {.name = "KEY_PASTE", .value = KEY_PASTE}, + {.name = "KEY_PAUSE", .value = KEY_PAUSE}, + {.name = "KEY_PAUSECD", .value = KEY_PAUSECD}, + {.name = "KEY_PAUSE_RECORD", .value = KEY_PAUSE_RECORD}, + {.name = "KEY_PC", .value = KEY_PC}, + {.name = "KEY_PHONE", .value = KEY_PHONE}, + {.name = "KEY_PICKUP_PHONE", .value = KEY_PICKUP_PHONE}, + {.name = "KEY_PLAY", .value = KEY_PLAY}, + {.name = "KEY_PLAYCD", .value = KEY_PLAYCD}, + {.name = "KEY_PLAYER", .value = KEY_PLAYER}, + {.name = "KEY_PLAYPAUSE", .value = KEY_PLAYPAUSE}, + {.name = "KEY_POWER", .value = KEY_POWER}, + {.name = "KEY_POWER2", .value = KEY_POWER2}, + {.name = "KEY_PRESENTATION", .value = KEY_PRESENTATION}, + {.name = "KEY_PREVIOUS", .value = KEY_PREVIOUS}, + {.name = "KEY_PREVIOUSSONG", .value = KEY_PREVIOUSSONG}, + {.name = "KEY_PREVIOUS_ELEMENT", .value = KEY_PREVIOUS_ELEMENT}, + {.name = "KEY_PRINT", .value = KEY_PRINT}, + {.name = "KEY_PRIVACY_SCREEN_TOGGLE", .value = KEY_PRIVACY_SCREEN_TOGGLE}, + {.name = "KEY_PROG1", .value = KEY_PROG1}, + {.name = "KEY_PROG2", .value = KEY_PROG2}, + {.name = "KEY_PROG3", .value = KEY_PROG3}, + {.name = "KEY_PROG4", .value = KEY_PROG4}, + {.name = "KEY_PROGRAM", .value = KEY_PROGRAM}, + {.name = "KEY_PROPS", .value = KEY_PROPS}, + {.name = "KEY_PVR", .value = KEY_PVR}, + {.name = "KEY_Q", .value = KEY_Q}, + {.name = "KEY_QUESTION", .value = KEY_QUESTION}, + {.name = "KEY_R", .value = KEY_R}, + {.name = "KEY_RADAR_OVERLAY", .value = KEY_RADAR_OVERLAY}, + {.name = "KEY_RADIO", .value = KEY_RADIO}, + {.name = "KEY_RECORD", .value = KEY_RECORD}, + {.name = "KEY_RED", .value = KEY_RED}, + {.name = "KEY_REDO", .value = KEY_REDO}, + {.name = "KEY_REFRESH", .value = KEY_REFRESH}, + {.name = "KEY_REPLY", .value = KEY_REPLY}, + {.name = "KEY_RESERVED", .value = KEY_RESERVED}, + {.name = "KEY_RESTART", .value = KEY_RESTART}, + {.name = "KEY_REWIND", .value = KEY_REWIND}, + {.name = "KEY_RFKILL", .value = KEY_RFKILL}, + {.name = "KEY_RIGHT", .value = KEY_RIGHT}, + {.name = "KEY_RIGHTALT", .value = KEY_RIGHTALT}, + {.name = "KEY_RIGHTBRACE", .value = KEY_RIGHTBRACE}, + {.name = "KEY_RIGHTCTRL", .value = KEY_RIGHTCTRL}, + {.name = "KEY_RIGHTMETA", .value = KEY_RIGHTMETA}, + {.name = "KEY_RIGHTSHIFT", .value = KEY_RIGHTSHIFT}, + {.name = "KEY_RIGHT_DOWN", .value = KEY_RIGHT_DOWN}, + {.name = "KEY_RIGHT_UP", .value = KEY_RIGHT_UP}, + {.name = "KEY_RO", .value = KEY_RO}, + {.name = "KEY_ROOT_MENU", .value = KEY_ROOT_MENU}, + {.name = "KEY_ROTATE_DISPLAY", .value = KEY_ROTATE_DISPLAY}, + {.name = "KEY_ROTATE_LOCK_TOGGLE", .value = KEY_ROTATE_LOCK_TOGGLE}, + {.name = "KEY_S", .value = KEY_S}, + {.name = "KEY_SAT", .value = KEY_SAT}, + {.name = "KEY_SAT2", .value = KEY_SAT2}, + {.name = "KEY_SAVE", .value = KEY_SAVE}, + {.name = "KEY_SCALE", .value = KEY_SCALE}, + {.name = "KEY_SCREENSAVER", .value = KEY_SCREENSAVER}, + {.name = "KEY_SCROLLDOWN", .value = KEY_SCROLLDOWN}, + {.name = "KEY_SCROLLLOCK", .value = KEY_SCROLLLOCK}, + {.name = "KEY_SCROLLUP", .value = KEY_SCROLLUP}, + {.name = "KEY_SEARCH", .value = KEY_SEARCH}, + {.name = "KEY_SELECT", .value = KEY_SELECT}, + {.name = "KEY_SELECTIVE_SCREENSHOT", .value = KEY_SELECTIVE_SCREENSHOT}, + {.name = "KEY_SEMICOLON", .value = KEY_SEMICOLON}, + {.name = "KEY_SEND", .value = KEY_SEND}, + {.name = "KEY_SENDFILE", .value = KEY_SENDFILE}, + {.name = "KEY_SETUP", .value = KEY_SETUP}, + {.name = "KEY_SHOP", .value = KEY_SHOP}, + {.name = "KEY_SHUFFLE", .value = KEY_SHUFFLE}, + {.name = "KEY_SIDEVU_SONAR", .value = KEY_SIDEVU_SONAR}, + {.name = "KEY_SINGLE_RANGE_RADAR", .value = KEY_SINGLE_RANGE_RADAR}, + {.name = "KEY_SLASH", .value = KEY_SLASH}, + {.name = "KEY_SLEEP", .value = KEY_SLEEP}, + {.name = "KEY_SLOW", .value = KEY_SLOW}, + {.name = "KEY_SLOWREVERSE", .value = KEY_SLOWREVERSE}, + {.name = "KEY_SOS", .value = KEY_SOS}, + {.name = "KEY_SOUND", .value = KEY_SOUND}, + {.name = "KEY_SPACE", .value = KEY_SPACE}, + {.name = "KEY_SPELLCHECK", .value = KEY_SPELLCHECK}, + {.name = "KEY_SPORT", .value = KEY_SPORT}, + {.name = "KEY_SPREADSHEET", .value = KEY_SPREADSHEET}, + {.name = "KEY_STOP", .value = KEY_STOP}, + {.name = "KEY_STOPCD", .value = KEY_STOPCD}, + {.name = "KEY_STOP_RECORD", .value = KEY_STOP_RECORD}, + {.name = "KEY_SUBTITLE", .value = KEY_SUBTITLE}, + {.name = "KEY_SUSPEND", .value = KEY_SUSPEND}, + {.name = "KEY_SWITCHVIDEOMODE", .value = KEY_SWITCHVIDEOMODE}, + {.name = "KEY_SYSRQ", .value = KEY_SYSRQ}, + {.name = "KEY_T", .value = KEY_T}, + {.name = "KEY_TAB", .value = KEY_TAB}, + {.name = "KEY_TAPE", .value = KEY_TAPE}, + {.name = "KEY_TASKMANAGER", .value = KEY_TASKMANAGER}, + {.name = "KEY_TEEN", .value = KEY_TEEN}, + {.name = "KEY_TEXT", .value = KEY_TEXT}, + {.name = "KEY_TIME", .value = KEY_TIME}, + {.name = "KEY_TITLE", .value = KEY_TITLE}, + {.name = "KEY_TOUCHPAD_OFF", .value = KEY_TOUCHPAD_OFF}, + {.name = "KEY_TOUCHPAD_ON", .value = KEY_TOUCHPAD_ON}, + {.name = "KEY_TOUCHPAD_TOGGLE", .value = KEY_TOUCHPAD_TOGGLE}, + {.name = "KEY_TRADITIONAL_SONAR", .value = KEY_TRADITIONAL_SONAR}, + {.name = "KEY_TUNER", .value = KEY_TUNER}, + {.name = "KEY_TV", .value = KEY_TV}, + {.name = "KEY_TV2", .value = KEY_TV2}, + {.name = "KEY_TWEN", .value = KEY_TWEN}, + {.name = "KEY_U", .value = KEY_U}, + {.name = "KEY_UNDO", .value = KEY_UNDO}, + {.name = "KEY_UNKNOWN", .value = KEY_UNKNOWN}, + {.name = "KEY_UNMUTE", .value = KEY_UNMUTE}, + {.name = "KEY_UP", .value = KEY_UP}, + {.name = "KEY_UWB", .value = KEY_UWB}, + {.name = "KEY_V", .value = KEY_V}, + {.name = "KEY_VCR", .value = KEY_VCR}, + {.name = "KEY_VCR2", .value = KEY_VCR2}, + {.name = "KEY_VENDOR", .value = KEY_VENDOR}, + {.name = "KEY_VIDEO", .value = KEY_VIDEO}, + {.name = "KEY_VIDEOPHONE", .value = KEY_VIDEOPHONE}, + {.name = "KEY_VIDEO_NEXT", .value = KEY_VIDEO_NEXT}, + {.name = "KEY_VIDEO_PREV", .value = KEY_VIDEO_PREV}, + {.name = "KEY_VOD", .value = KEY_VOD}, + {.name = "KEY_VOICECOMMAND", .value = KEY_VOICECOMMAND}, + {.name = "KEY_VOICEMAIL", .value = KEY_VOICEMAIL}, + {.name = "KEY_VOLUMEDOWN", .value = KEY_VOLUMEDOWN}, + {.name = "KEY_VOLUMEUP", .value = KEY_VOLUMEUP}, + {.name = "KEY_W", .value = KEY_W}, + {.name = "KEY_WAKEUP", .value = KEY_WAKEUP}, + {.name = "KEY_WLAN", .value = KEY_WLAN}, + {.name = "KEY_WORDPROCESSOR", .value = KEY_WORDPROCESSOR}, + {.name = "KEY_WPS_BUTTON", .value = KEY_WPS_BUTTON}, + {.name = "KEY_WWAN", .value = KEY_WWAN}, + {.name = "KEY_WWW", .value = KEY_WWW}, + {.name = "KEY_X", .value = KEY_X}, + {.name = "KEY_XFER", .value = KEY_XFER}, + {.name = "KEY_Y", .value = KEY_Y}, + {.name = "KEY_YELLOW", .value = KEY_YELLOW}, + {.name = "KEY_YEN", .value = KEY_YEN}, + {.name = "KEY_Z", .value = KEY_Z}, + {.name = "KEY_ZENKAKUHANKAKU", .value = KEY_ZENKAKUHANKAKU}, + {.name = "KEY_ZOOMIN", .value = KEY_ZOOMIN}, + {.name = "KEY_ZOOMOUT", .value = KEY_ZOOMOUT}, + {.name = "KEY_ZOOMRESET", .value = KEY_ZOOMRESET}, + {.name = "LED_CAPSL", .value = LED_CAPSL}, + {.name = "LED_CHARGING", .value = LED_CHARGING}, + {.name = "LED_COMPOSE", .value = LED_COMPOSE}, + {.name = "LED_KANA", .value = LED_KANA}, + {.name = "LED_MAIL", .value = LED_MAIL}, + {.name = "LED_MAX", .value = LED_MAX}, + {.name = "LED_MISC", .value = LED_MISC}, + {.name = "LED_MUTE", .value = LED_MUTE}, + {.name = "LED_NUML", .value = LED_NUML}, + {.name = "LED_SCROLLL", .value = LED_SCROLLL}, + {.name = "LED_SLEEP", .value = LED_SLEEP}, + {.name = "LED_SUSPEND", .value = LED_SUSPEND}, + {.name = "MSC_GESTURE", .value = MSC_GESTURE}, + {.name = "MSC_MAX", .value = MSC_MAX}, + {.name = "MSC_PULSELED", .value = MSC_PULSELED}, + {.name = "MSC_RAW", .value = MSC_RAW}, + {.name = "MSC_SCAN", .value = MSC_SCAN}, + {.name = "MSC_SERIAL", .value = MSC_SERIAL}, + {.name = "MSC_TIMESTAMP", .value = MSC_TIMESTAMP}, + {.name = "REL_DIAL", .value = REL_DIAL}, + {.name = "REL_HWHEEL", .value = REL_HWHEEL}, + {.name = "REL_HWHEEL_HI_RES", .value = REL_HWHEEL_HI_RES}, + {.name = "REL_MAX", .value = REL_MAX}, + {.name = "REL_MISC", .value = REL_MISC}, + {.name = "REL_RESERVED", .value = REL_RESERVED}, + {.name = "REL_RX", .value = REL_RX}, + {.name = "REL_RY", .value = REL_RY}, + {.name = "REL_RZ", .value = REL_RZ}, + {.name = "REL_WHEEL", .value = REL_WHEEL}, + {.name = "REL_WHEEL_HI_RES", .value = REL_WHEEL_HI_RES}, + {.name = "REL_X", .value = REL_X}, + {.name = "REL_Y", .value = REL_Y}, + {.name = "REL_Z", .value = REL_Z}, + {.name = "REP_DELAY", .value = REP_DELAY}, + {.name = "REP_MAX", .value = REP_MAX}, + {.name = "REP_PERIOD", .value = REP_PERIOD}, + {.name = "SND_BELL", .value = SND_BELL}, + {.name = "SND_CLICK", .value = SND_CLICK}, + {.name = "SND_MAX", .value = SND_MAX}, + {.name = "SND_TONE", .value = SND_TONE}, + {.name = "SW_CAMERA_LENS_COVER", .value = SW_CAMERA_LENS_COVER}, + {.name = "SW_DOCK", .value = SW_DOCK}, + {.name = "SW_FRONT_PROXIMITY", .value = SW_FRONT_PROXIMITY}, + {.name = "SW_HEADPHONE_INSERT", .value = SW_HEADPHONE_INSERT}, + {.name = "SW_JACK_PHYSICAL_INSERT", .value = SW_JACK_PHYSICAL_INSERT}, + {.name = "SW_KEYPAD_SLIDE", .value = SW_KEYPAD_SLIDE}, + {.name = "SW_LID", .value = SW_LID}, + {.name = "SW_LINEIN_INSERT", .value = SW_LINEIN_INSERT}, + {.name = "SW_LINEOUT_INSERT", .value = SW_LINEOUT_INSERT}, + {.name = "SW_MACHINE_COVER", .value = SW_MACHINE_COVER}, + {.name = "SW_MAX", .value = SW_MAX}, + {.name = "SW_MICROPHONE_INSERT", .value = SW_MICROPHONE_INSERT}, + {.name = "SW_MUTE_DEVICE", .value = SW_MUTE_DEVICE}, + {.name = "SW_PEN_INSERTED", .value = SW_PEN_INSERTED}, + {.name = "SW_RFKILL_ALL", .value = SW_RFKILL_ALL}, + {.name = "SW_ROTATE_LOCK", .value = SW_ROTATE_LOCK}, + {.name = "SW_TABLET_MODE", .value = SW_TABLET_MODE}, + {.name = "SW_VIDEOOUT_INSERT", .value = SW_VIDEOOUT_INSERT}, + {.name = "SYN_CONFIG", .value = SYN_CONFIG}, + {.name = "SYN_DROPPED", .value = SYN_DROPPED}, + {.name = "SYN_MAX", .value = SYN_MAX}, + {.name = "SYN_MT_REPORT", .value = SYN_MT_REPORT}, + {.name = "SYN_REPORT", .value = SYN_REPORT}, +}; + +static const struct name_entry prop_names[] = { + {.name = "INPUT_PROP_ACCELEROMETER", .value = INPUT_PROP_ACCELEROMETER}, + {.name = "INPUT_PROP_BUTTONPAD", .value = INPUT_PROP_BUTTONPAD}, + {.name = "INPUT_PROP_DIRECT", .value = INPUT_PROP_DIRECT}, + {.name = "INPUT_PROP_MAX", .value = INPUT_PROP_MAX}, + {.name = "INPUT_PROP_POINTER", .value = INPUT_PROP_POINTER}, + {.name = "INPUT_PROP_POINTING_STICK", .value = INPUT_PROP_POINTING_STICK}, + {.name = "INPUT_PROP_SEMI_MT", .value = INPUT_PROP_SEMI_MT}, + {.name = "INPUT_PROP_TOPBUTTONPAD", .value = INPUT_PROP_TOPBUTTONPAD}, +}; + +#endif /* EVENT_NAMES_H */ diff --git a/priv/src/main/cpp/libevdev/libevdev-int.h b/priv/src/main/cpp/libevdev/libevdev-int.h new file mode 100644 index 0000000000..3f47dfba15 --- /dev/null +++ b/priv/src/main/cpp/libevdev/libevdev-int.h @@ -0,0 +1,317 @@ +// 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/priv/src/main/cpp/libevdev/libevdev-names.c b/priv/src/main/cpp/libevdev/libevdev-names.c new file mode 100644 index 0000000000..f8c991b84a --- /dev/null +++ b/priv/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/priv/src/main/cpp/libevdev/libevdev-uinput-int.h b/priv/src/main/cpp/libevdev/libevdev-uinput-int.h new file mode 100644 index 0000000000..c6bf015497 --- /dev/null +++ b/priv/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/priv/src/main/cpp/libevdev/libevdev-uinput.c b/priv/src/main/cpp/libevdev/libevdev-uinput.c new file mode 100644 index 0000000000..cdce8dead4 --- /dev/null +++ b/priv/src/main/cpp/libevdev/libevdev-uinput.c @@ -0,0 +1,494 @@ +// 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/priv/src/main/cpp/libevdev/libevdev-uinput.h b/priv/src/main/cpp/libevdev/libevdev-uinput.h new file mode 100644 index 0000000000..2919788905 --- /dev/null +++ b/priv/src/main/cpp/libevdev/libevdev-uinput.h @@ -0,0 +1,255 @@ +/* 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/priv/src/main/cpp/libevdev/libevdev-util.h b/priv/src/main/cpp/libevdev/libevdev-util.h new file mode 100644 index 0000000000..380636d2b8 --- /dev/null +++ b/priv/src/main/cpp/libevdev/libevdev-util.h @@ -0,0 +1,63 @@ +// 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/priv/src/main/cpp/libevdev/libevdev.c b/priv/src/main/cpp/libevdev/libevdev.c new file mode 100644 index 0000000000..78f8f3cfae --- /dev/null +++ b/priv/src/main/cpp/libevdev/libevdev.c @@ -0,0 +1,1848 @@ +// 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/priv/src/main/cpp/libevdev/libevdev.h b/priv/src/main/cpp/libevdev/libevdev.h new file mode 100644 index 0000000000..b4b34ead63 --- /dev/null +++ b/priv/src/main/cpp/libevdev/libevdev.h @@ -0,0 +1,2387 @@ +/* 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/priv/src/main/cpp/libevdev/libevdev.sym b/priv/src/main/cpp/libevdev/libevdev.sym new file mode 100644 index 0000000000..4161962dc8 --- /dev/null +++ b/priv/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/priv/src/main/cpp/libevdev/make-event-names.py b/priv/src/main/cpp/libevdev/make-event-names.py new file mode 100755 index 0000000000..743b4b58b1 --- /dev/null +++ b/priv/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/priv/src/main/cpp/privservice.cpp b/priv/src/main/cpp/privservice.cpp new file mode 100644 index 0000000000..125ee9ebe6 --- /dev/null +++ b/priv/src/main/cpp/privservice.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include "libevdev/libevdev.h" + +#define LOG_TAG "KeyMapperPrivService" + +#include "logging.h" + +extern "C" +JNIEXPORT jstring JNICALL +Java_io_github_sds100_keymapper_priv_service_PrivService_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/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt index 1a143b1166..9a4f7306bd 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.priv.service import android.annotation.SuppressLint import android.ddm.DdmHandleAppName +import android.system.Os import io.github.sds100.keymapper.priv.IPrivService import timber.log.Timber import kotlin.system.exitProcess @@ -26,6 +27,7 @@ class PrivService : IPrivService.Stub() { init { @SuppressLint("UnsafeDynamicallyLoadedCode") + // TODO can we change "shizuku.library.path" property? System.load("${System.getProperty("shizuku.library.path")}/libevdev.so") stringFromJNI() } @@ -36,6 +38,7 @@ class PrivService : IPrivService.Stub() { } override fun sendEvent(): String? { - TODO("Not yet implemented") + Timber.e("UID = ${Os.getuid()}") + return stringFromJNI() } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0779070117..44166d41bf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,3 +31,4 @@ include(":system") include(":common") include(":data") include(":priv") +include(":libevdev") From ef2a1ef3767acf40e7a260a83fd3c352ee97c582 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 17:42:11 +0200 Subject: [PATCH 004/215] #1394 simplify code --- .../io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt index b306ceb9d2..c33a6205c5 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt @@ -270,8 +270,7 @@ class AdbPairingClient( val theirMessage = ByteArray(theirHeader.payload) inputStream.readFully(theirMessage) - if (!pairingContext.initCipher(theirMessage)) return false - return true + return pairingContext.initCipher(theirMessage) } private fun doExchangePeerInfo(): Boolean { From 364313cbb9fc92a7dfb5e32665cc3bb366257798 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 21:19:55 +0200 Subject: [PATCH 005/215] #1394 WIP: pair with ADB --- app/build.gradle.kts | 1 + .../github/sds100/keymapper/MainActivity.kt | 1 + .../sds100/keymapper/base/BaseMainActivity.kt | 9 + .../BaseAccessibilityServiceController.kt | 4 + priv/build.gradle.kts | 4 + priv/src/main/cpp/adb_pairing.cpp | 3 +- .../sds100/keymapper/priv/PrivHiltModule.kt | 18 ++ .../sds100/keymapper/priv/adb/AdbClient.kt | 5 +- .../sds100/keymapper/priv/adb/AdbException.kt | 8 +- .../sds100/keymapper/priv/adb/AdbKey.kt | 2 +- .../sds100/keymapper/priv/adb/AdbMdns.kt | 2 +- .../sds100/keymapper/priv/adb/AdbMessage.kt | 2 +- .../keymapper/priv/adb/AdbPairingClient.kt | 2 +- .../keymapper/priv/adb/AdbPairingService.kt | 50 +---- .../sds100/keymapper/priv/adb/AdbProtocol.kt | 2 +- .../service/PrivServiceSetupController.kt | 189 ++++++++++++++++++ settings.gradle.kts | 1 - 17 files changed, 245 insertions(+), 58 deletions(-) create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/PrivHiltModule.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e5df922030..c71b4a3782 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,6 +141,7 @@ dependencies { implementation(project(":base")) implementation(project(":api")) implementation(project(":data")) + implementation(project(":priv")) implementation(project(":system")) compileOnly(project(":systemstubs")) diff --git a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt b/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt index d48551a3dc..0fe4632810 100644 --- a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt @@ -10,6 +10,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.databinding.ActivityMainBinding import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.showDialogs +import io.github.sds100.keymapper.priv.service.PrivServiceSetupController import javax.inject.Inject @AndroidEntryPoint diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 6c6dae42eb..52d36b7b26 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.res.Configuration import android.net.Uri +import android.os.Build import android.os.Bundle import android.view.MotionEvent import androidx.activity.SystemBarStyle @@ -33,6 +34,7 @@ import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.priv.service.PrivServiceSetupController import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl @@ -82,6 +84,9 @@ abstract class BaseMainActivity : AppCompatActivity() { @Inject lateinit var buildConfigProvider: BuildConfigProvider + @Inject + lateinit var privServiceSetup: PrivServiceSetupController + private lateinit var requestPermissionDelegate: RequestPermissionDelegate private val currentNightMode: Int @@ -180,6 +185,10 @@ abstract class BaseMainActivity : AppCompatActivity() { ContextCompat.RECEIVER_EXPORTED, ) } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + privServiceSetup.pairWirelessAdb(34413, "158394") + } } override fun onResume() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 121afbadaa..86fca72d4d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -530,6 +530,10 @@ abstract class BaseAccessibilityServiceController( } } } + + if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { + Timber.e(service.rootInActiveWindow.toString()) + } } fun onFingerprintGesture(type: FingerprintGestureType) { diff --git a/priv/build.gradle.kts b/priv/build.gradle.kts index 89af198e13..0f18f173b3 100644 --- a/priv/build.gradle.kts +++ b/priv/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.google.devtools.ksp) + alias(libs.plugins.dagger.hilt.android) } android { @@ -71,6 +73,8 @@ dependencies { implementation("org.conscrypt:conscrypt-android:2.5.3") implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) + implementation(libs.dagger.hilt.android) + ksp(libs.dagger.hilt.android.compiler) // From Shizuku :manager module build.gradle file. implementation("io.github.vvb2060.ndk:boringssl:20250114") diff --git a/priv/src/main/cpp/adb_pairing.cpp b/priv/src/main/cpp/adb_pairing.cpp index 1a44981e28..80d10f45f9 100644 --- a/priv/src/main/cpp/adb_pairing.cpp +++ b/priv/src/main/cpp/adb_pairing.cpp @@ -223,7 +223,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { {"nativeDestroy", "(J)V", (void *) PairingContext_Destroy}, }; - env->RegisterNatives(env->FindClass("moe/shizuku/manager/adb/PairingContext"), methods_PairingContext, + env->RegisterNatives(env->FindClass("io/github/sds100/keymapper/priv/adb/PairingContext"), + methods_PairingContext, sizeof(methods_PairingContext) / sizeof(JNINativeMethod)); return JNI_VERSION_1_6; diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/PrivHiltModule.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/PrivHiltModule.kt new file mode 100644 index 0000000000..004044ce35 --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/PrivHiltModule.kt @@ -0,0 +1,18 @@ +package io.github.sds100.keymapper.priv + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import io.github.sds100.keymapper.priv.service.PrivServiceSetupController +import io.github.sds100.keymapper.priv.service.PrivServiceSetupControllerImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class PrivHiltModule { + + @Singleton + @Binds + abstract fun bindPrivServiceSetupController(impl: PrivServiceSetupControllerImpl): PrivServiceSetupController +} \ No newline at end of file diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt index 19b018636b..f6545c6894 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.priv.adb import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_SIGNATURE import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_TOKEN @@ -25,7 +26,9 @@ 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 { +@RequiresApi(Build.VERSION_CODES.M) +internal 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 diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt index df1f222487..097ab350f4 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt @@ -1,9 +1,9 @@ package io.github.sds100.keymapper.priv.adb @Suppress("NOTHING_TO_INLINE") -inline fun adbError(message: Any): Nothing = throw AdbException(message.toString()) +internal inline fun adbError(message: Any): Nothing = throw AdbException(message.toString()) -open class AdbException : Exception { +internal open class AdbException : Exception { constructor(message: String, cause: Throwable?) : super(message, cause) constructor(message: String) : super(message) @@ -11,6 +11,6 @@ open class AdbException : Exception { constructor() } -class AdbInvalidPairingCodeException : AdbException() +internal class AdbInvalidPairingCodeException : AdbException() -class AdbKeyException(cause: Throwable) : AdbException(cause) +internal class AdbKeyException(cause: Throwable) : AdbException(cause) diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt index b0b00be5ff..185435b05e 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt @@ -47,7 +47,7 @@ import javax.net.ssl.X509ExtendedTrustManager private const val TAG = "AdbKey" @RequiresApi(Build.VERSION_CODES.M) -class AdbKey(private val adbKeyStore: AdbKeyStore, name: String) { +internal class AdbKey(private val adbKeyStore: AdbKeyStore, name: String) { companion object { diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt index 23995a7363..76636c7ec8 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt @@ -13,7 +13,7 @@ import java.net.NetworkInterface import java.net.ServerSocket @RequiresApi(Build.VERSION_CODES.R) -class AdbMdns( +internal class AdbMdns( context: Context, private val serviceType: String, private val port: MutableLiveData ) { diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt index 20aabce680..bfae9db26c 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt @@ -11,7 +11,7 @@ import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_WRTE import java.nio.ByteBuffer import java.nio.ByteOrder -class AdbMessage( +internal class AdbMessage( val command: Int, val arg0: Int, val arg1: Int, diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt index c33a6205c5..7750c86262 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt @@ -167,7 +167,7 @@ private class PairingContext private constructor(private val nativePtr: Long) { } @RequiresApi(Build.VERSION_CODES.R) -class AdbPairingClient( +internal class AdbPairingClient( private val host: String, private val port: Int, private val pairCode: String, diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt index 02a62b9b58..ac9abc71d0 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt @@ -1,7 +1,5 @@ package io.github.sds100.keymapper.priv.adb -import android.annotation.TargetApi -import android.app.ForegroundServiceStartNotAllowedException import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -16,18 +14,19 @@ import android.os.IBinder import android.os.Looper import android.preference.PreferenceManager import android.util.Log +import androidx.annotation.RequiresApi import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import io.github.sds100.keymapper.priv.R +import io.github.sds100.keymapper.priv.ktx.TAG import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import io.github.sds100.keymapper.priv.ktx.TAG import rikka.core.ktx.unsafeLazy import java.net.ConnectException -@TargetApi(Build.VERSION_CODES.R) -class AdbPairingService : Service() { +@RequiresApi(Build.VERSION_CODES.R) +internal class AdbPairingService : Service() { companion object { @@ -94,48 +93,7 @@ class AdbPairingService : Service() { } 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 } diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt index 91e7f68fff..1c312cac0d 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.priv.adb -object AdbProtocol { +internal object AdbProtocol { const val A_SYNC = 0x434e5953 const val A_CNXN = 0x4e584e43 diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt new file mode 100644 index 0000000000..c1a2715d8e --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt @@ -0,0 +1,189 @@ +package io.github.sds100.keymapper.priv.service + +import android.app.AppOpsManager +import android.app.ForegroundServiceStartNotAllowedException +import android.content.Context +import android.graphics.Typeface +import android.os.Build +import android.preference.PreferenceManager +import android.util.Log +import android.widget.TextView +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.priv.adb.AdbClient +import io.github.sds100.keymapper.priv.adb.AdbKey +import io.github.sds100.keymapper.priv.adb.AdbKeyException +import io.github.sds100.keymapper.priv.adb.AdbPairingClient +import io.github.sds100.keymapper.priv.adb.AdbPairingService +import io.github.sds100.keymapper.priv.adb.PreferenceAdbKeyStore +import io.github.sds100.keymapper.priv.starter.Starter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This starter code is taken from the Shizuku project. + */ +@Singleton +class PrivServiceSetupControllerImpl @Inject constructor( + @ApplicationContext private val ctx: Context, + private val coroutineScope: CoroutineScope +) : PrivServiceSetupController { + + private val sb = StringBuilder() + + @RequiresApi(Build.VERSION_CODES.R) + override fun startWithAdb(host: String, port: Int) { + writeStarterFiles() + + sb.append("Starting with wireless adb...").append('\n').append('\n') + postResult() + + coroutineScope.launch(Dispatchers.IO) { + val key = try { + AdbKey( + PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), + "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/${ctx.packageName}/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(ctx, 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) + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun writeStarterFiles() { + coroutineScope.launch(Dispatchers.IO) { + try { + Starter.writeSdcardFiles(ctx) + } catch (e: Throwable) { + // TODO show error message if fails to start + } + } + } + + fun postResult(throwable: Throwable? = null) { + if (throwable == null) { + Timber.e(sb.toString()) + } else { + Timber.e(throwable) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + override fun pairWirelessAdb(port: Int, code: String) { + coroutineScope.launch(Dispatchers.IO) { + val host = "127.0.0.1" + + val key = try { + AdbKey( + PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), + "shizuku", + ) + } catch (e: Throwable) { + e.printStackTrace() + return@launch + } + + AdbPairingClient(host, port, code, key).runCatching { + start() + }.onFailure { + Timber.d("Pairing failed: $it") +// handleResult(false, it) + }.onSuccess { + Timber.d("Pairing success") +// handleResult(it, null) + } + } + +// val intent = AdbPairingService.startIntent(ctx) +// try { +// ctx.startForegroundService(intent) +// } catch (e: Throwable) { +// Timber.e("start ADB pairing service failed: $e") +// +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && +// e is ForegroundServiceStartNotAllowedException +// ) { +// val mode = ctx.getSystemService(AppOpsManager::class.java) +// .noteOpNoThrow( +// "android:start_foreground", +// android.os.Process.myUid(), +// ctx.packageName, +// null, +// null, +// ) +// if (mode == AppOpsManager.MODE_ERRORED) { +// Toast.makeText( +// ctx, +// "OP_START_FOREGROUND is denied. What are you doing?", +// Toast.LENGTH_LONG, +// ).show() +// } +// ctx.startService(intent) +// } +// } + } +} + +interface PrivServiceSetupController { + @RequiresApi(Build.VERSION_CODES.R) + fun pairWirelessAdb(port: Int, code: String) + + @RequiresApi(Build.VERSION_CODES.R) + fun startWithAdb(host: String, port: Int) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 44166d41bf..0779070117 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,4 +31,3 @@ include(":system") include(":common") include(":data") include(":priv") -include(":libevdev") From 3bb9af29733092f5b8113a54df0706e7224fbd4b Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 21:36:09 +0200 Subject: [PATCH 006/215] #1394 demo reading pairing code and port with BaseAccessibilityServiceController --- .../BaseAccessibilityServiceController.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 86fca72d4d..4712275b71 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -531,8 +531,25 @@ abstract class BaseAccessibilityServiceController( } } + // TODO only run this code, and listen for these events if searching for a pairing code. Check that package name is settings app. if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { - Timber.e(service.rootInActiveWindow.toString()) + val pairingCodeRegex = Regex("^\\d{6}$") + val portRegex = Regex(".*:([0-9]{1,5})") + val pairingCodeNode = + service.rootInActiveWindow.findNodeRecursively { + it.text != null && pairingCodeRegex.matches( + it.text + ) + } + + val portNode = service.rootInActiveWindow.findNodeRecursively { + it.text != null && portRegex.matches(it.text) + } + + if (pairingCodeNode != null && portNode != null) { + Timber.e("PAIRING CODE = ${pairingCodeNode.text}") + Timber.e("PORT = ${portNode.text.split(":").last()}") + } } } From 0b38692758870ae112102a7553b95edc4248e25d Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 21:41:43 +0200 Subject: [PATCH 007/215] #1394 automatically pairing with ADB when the pairing dialog is shown works --- .../AccessibilityServiceController.kt | 4 ++++ .../sds100/keymapper/base/BaseMainActivity.kt | 5 ----- .../BaseAccessibilityServiceController.kt | 18 +++++++++++++----- .../priv/service/PrivServiceSetupController.kt | 16 +++------------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 6bc72f0bbe..3002c68927 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -12,6 +12,7 @@ import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsControll import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeRecorder import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.priv.service.PrivServiceSetupController import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.root.SuAdapter @@ -28,6 +29,7 @@ class AccessibilityServiceController @AssistedInject constructor( devicesAdapter: DevicesAdapter, suAdapter: SuAdapter, settingsRepository: PreferenceRepository, + privServiceSetupController: PrivServiceSetupController ) : BaseAccessibilityServiceController( service = service, rerouteKeyEventsControllerFactory = rerouteKeyEventsControllerFactory, @@ -40,6 +42,8 @@ class AccessibilityServiceController @AssistedInject constructor( devicesAdapter = devicesAdapter, suAdapter = suAdapter, settingsRepository = settingsRepository, + privServiceSetupController = privServiceSetupController + ) { @AssistedFactory interface Factory { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 52d36b7b26..9ab375dec4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.content.IntentFilter import android.content.res.Configuration import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.MotionEvent import androidx.activity.SystemBarStyle @@ -185,10 +184,6 @@ abstract class BaseMainActivity : AppCompatActivity() { ContextCompat.RECEIVER_EXPORTED, ) } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - privServiceSetup.pairWirelessAdb(34413, "158394") - } } override fun onResume() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 4712275b71..6397c0274c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -29,6 +29,7 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.priv.service.PrivServiceSetupController import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils @@ -70,6 +71,7 @@ abstract class BaseAccessibilityServiceController( private val devicesAdapter: DevicesAdapter, private val suAdapter: SuAdapter, private val settingsRepository: PreferenceRepository, + private val privServiceSetupController: PrivServiceSetupController ) { companion object { @@ -537,9 +539,7 @@ abstract class BaseAccessibilityServiceController( val portRegex = Regex(".*:([0-9]{1,5})") val pairingCodeNode = service.rootInActiveWindow.findNodeRecursively { - it.text != null && pairingCodeRegex.matches( - it.text - ) + it.text != null && pairingCodeRegex.matches(it.text) } val portNode = service.rootInActiveWindow.findNodeRecursively { @@ -547,8 +547,16 @@ abstract class BaseAccessibilityServiceController( } if (pairingCodeNode != null && portNode != null) { - Timber.e("PAIRING CODE = ${pairingCodeNode.text}") - Timber.e("PORT = ${portNode.text.split(":").last()}") + val pairingCode = pairingCodeNode.text?.toString()?.toIntOrNull() + val port = portNode.text?.split(":")?.last()?.toIntOrNull() + Timber.e("PAIRING CODE = $pairingCode") + Timber.e("PORT = $port") + + if (pairingCode != null && port != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + privServiceSetupController.pairWirelessAdb(port, pairingCode) + } + } } } } diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt index c1a2715d8e..d8983fc673 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt @@ -1,30 +1,20 @@ package io.github.sds100.keymapper.priv.service -import android.app.AppOpsManager -import android.app.ForegroundServiceStartNotAllowedException import android.content.Context -import android.graphics.Typeface import android.os.Build import android.preference.PreferenceManager import android.util.Log -import android.widget.TextView -import android.widget.Toast import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.priv.adb.AdbClient import io.github.sds100.keymapper.priv.adb.AdbKey import io.github.sds100.keymapper.priv.adb.AdbKeyException import io.github.sds100.keymapper.priv.adb.AdbPairingClient -import io.github.sds100.keymapper.priv.adb.AdbPairingService import io.github.sds100.keymapper.priv.adb.PreferenceAdbKeyStore import io.github.sds100.keymapper.priv.starter.Starter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -125,7 +115,7 @@ class PrivServiceSetupControllerImpl @Inject constructor( } @RequiresApi(Build.VERSION_CODES.R) - override fun pairWirelessAdb(port: Int, code: String) { + override fun pairWirelessAdb(port: Int, code: Int) { coroutineScope.launch(Dispatchers.IO) { val host = "127.0.0.1" @@ -139,7 +129,7 @@ class PrivServiceSetupControllerImpl @Inject constructor( return@launch } - AdbPairingClient(host, port, code, key).runCatching { + AdbPairingClient(host, port, code.toString(), key).runCatching { start() }.onFailure { Timber.d("Pairing failed: $it") @@ -182,7 +172,7 @@ class PrivServiceSetupControllerImpl @Inject constructor( interface PrivServiceSetupController { @RequiresApi(Build.VERSION_CODES.R) - fun pairWirelessAdb(port: Int, code: String) + fun pairWirelessAdb(port: Int, code: Int) @RequiresApi(Build.VERSION_CODES.R) fun startWithAdb(host: String, port: Int) From 9e8fd351bb06328bfecd7d42d34e37fbe3b10c0f Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 21:43:35 +0200 Subject: [PATCH 008/215] #1394 delete event-names.h --- priv/src/main/cpp/libevdev/event-names.h | 1656 ---------------------- 1 file changed, 1656 deletions(-) delete mode 100644 priv/src/main/cpp/libevdev/event-names.h diff --git a/priv/src/main/cpp/libevdev/event-names.h b/priv/src/main/cpp/libevdev/event-names.h deleted file mode 100644 index 8d433faea1..0000000000 --- a/priv/src/main/cpp/libevdev/event-names.h +++ /dev/null @@ -1,1656 +0,0 @@ -/* THIS FILE IS GENERATED, DO NOT EDIT */ - -#ifndef EVENT_NAMES_H -#define EVENT_NAMES_H - -static const char *const ev_map[EV_MAX + 1] = { - [EV_SYN] = "EV_SYN", - [EV_KEY] = "EV_KEY", - [EV_REL] = "EV_REL", - [EV_ABS] = "EV_ABS", - [EV_MSC] = "EV_MSC", - [EV_SW] = "EV_SW", - [EV_LED] = "EV_LED", - [EV_SND] = "EV_SND", - [EV_REP] = "EV_REP", - [EV_FF] = "EV_FF", - [EV_PWR] = "EV_PWR", - [EV_FF_STATUS] = "EV_FF_STATUS", - [EV_MAX] = "EV_MAX", -}; - -static const char *const rel_map[REL_MAX + 1] = { - [REL_X] = "REL_X", - [REL_Y] = "REL_Y", - [REL_Z] = "REL_Z", - [REL_RX] = "REL_RX", - [REL_RY] = "REL_RY", - [REL_RZ] = "REL_RZ", - [REL_HWHEEL] = "REL_HWHEEL", - [REL_DIAL] = "REL_DIAL", - [REL_WHEEL] = "REL_WHEEL", - [REL_MISC] = "REL_MISC", - [REL_RESERVED] = "REL_RESERVED", - [REL_WHEEL_HI_RES] = "REL_WHEEL_HI_RES", - [REL_HWHEEL_HI_RES] = "REL_HWHEEL_HI_RES", - [REL_MAX] = "REL_MAX", -}; - -static const char *const abs_map[ABS_MAX + 1] = { - [ABS_X] = "ABS_X", - [ABS_Y] = "ABS_Y", - [ABS_Z] = "ABS_Z", - [ABS_RX] = "ABS_RX", - [ABS_RY] = "ABS_RY", - [ABS_RZ] = "ABS_RZ", - [ABS_THROTTLE] = "ABS_THROTTLE", - [ABS_RUDDER] = "ABS_RUDDER", - [ABS_WHEEL] = "ABS_WHEEL", - [ABS_GAS] = "ABS_GAS", - [ABS_BRAKE] = "ABS_BRAKE", - [ABS_HAT0X] = "ABS_HAT0X", - [ABS_HAT0Y] = "ABS_HAT0Y", - [ABS_HAT1X] = "ABS_HAT1X", - [ABS_HAT1Y] = "ABS_HAT1Y", - [ABS_HAT2X] = "ABS_HAT2X", - [ABS_HAT2Y] = "ABS_HAT2Y", - [ABS_HAT3X] = "ABS_HAT3X", - [ABS_HAT3Y] = "ABS_HAT3Y", - [ABS_PRESSURE] = "ABS_PRESSURE", - [ABS_DISTANCE] = "ABS_DISTANCE", - [ABS_TILT_X] = "ABS_TILT_X", - [ABS_TILT_Y] = "ABS_TILT_Y", - [ABS_TOOL_WIDTH] = "ABS_TOOL_WIDTH", - [ABS_VOLUME] = "ABS_VOLUME", - [ABS_PROFILE] = "ABS_PROFILE", - [ABS_MISC] = "ABS_MISC", - [ABS_RESERVED] = "ABS_RESERVED", - [ABS_MT_SLOT] = "ABS_MT_SLOT", - [ABS_MT_TOUCH_MAJOR] = "ABS_MT_TOUCH_MAJOR", - [ABS_MT_TOUCH_MINOR] = "ABS_MT_TOUCH_MINOR", - [ABS_MT_WIDTH_MAJOR] = "ABS_MT_WIDTH_MAJOR", - [ABS_MT_WIDTH_MINOR] = "ABS_MT_WIDTH_MINOR", - [ABS_MT_ORIENTATION] = "ABS_MT_ORIENTATION", - [ABS_MT_POSITION_X] = "ABS_MT_POSITION_X", - [ABS_MT_POSITION_Y] = "ABS_MT_POSITION_Y", - [ABS_MT_TOOL_TYPE] = "ABS_MT_TOOL_TYPE", - [ABS_MT_BLOB_ID] = "ABS_MT_BLOB_ID", - [ABS_MT_TRACKING_ID] = "ABS_MT_TRACKING_ID", - [ABS_MT_PRESSURE] = "ABS_MT_PRESSURE", - [ABS_MT_DISTANCE] = "ABS_MT_DISTANCE", - [ABS_MT_TOOL_X] = "ABS_MT_TOOL_X", - [ABS_MT_TOOL_Y] = "ABS_MT_TOOL_Y", - [ABS_MAX] = "ABS_MAX", -}; - -static const char *const key_map[KEY_MAX + 1] = { - [KEY_RESERVED] = "KEY_RESERVED", - [KEY_ESC] = "KEY_ESC", - [KEY_1] = "KEY_1", - [KEY_2] = "KEY_2", - [KEY_3] = "KEY_3", - [KEY_4] = "KEY_4", - [KEY_5] = "KEY_5", - [KEY_6] = "KEY_6", - [KEY_7] = "KEY_7", - [KEY_8] = "KEY_8", - [KEY_9] = "KEY_9", - [KEY_0] = "KEY_0", - [KEY_MINUS] = "KEY_MINUS", - [KEY_EQUAL] = "KEY_EQUAL", - [KEY_BACKSPACE] = "KEY_BACKSPACE", - [KEY_TAB] = "KEY_TAB", - [KEY_Q] = "KEY_Q", - [KEY_W] = "KEY_W", - [KEY_E] = "KEY_E", - [KEY_R] = "KEY_R", - [KEY_T] = "KEY_T", - [KEY_Y] = "KEY_Y", - [KEY_U] = "KEY_U", - [KEY_I] = "KEY_I", - [KEY_O] = "KEY_O", - [KEY_P] = "KEY_P", - [KEY_LEFTBRACE] = "KEY_LEFTBRACE", - [KEY_RIGHTBRACE] = "KEY_RIGHTBRACE", - [KEY_ENTER] = "KEY_ENTER", - [KEY_LEFTCTRL] = "KEY_LEFTCTRL", - [KEY_A] = "KEY_A", - [KEY_S] = "KEY_S", - [KEY_D] = "KEY_D", - [KEY_F] = "KEY_F", - [KEY_G] = "KEY_G", - [KEY_H] = "KEY_H", - [KEY_J] = "KEY_J", - [KEY_K] = "KEY_K", - [KEY_L] = "KEY_L", - [KEY_SEMICOLON] = "KEY_SEMICOLON", - [KEY_APOSTROPHE] = "KEY_APOSTROPHE", - [KEY_GRAVE] = "KEY_GRAVE", - [KEY_LEFTSHIFT] = "KEY_LEFTSHIFT", - [KEY_BACKSLASH] = "KEY_BACKSLASH", - [KEY_Z] = "KEY_Z", - [KEY_X] = "KEY_X", - [KEY_C] = "KEY_C", - [KEY_V] = "KEY_V", - [KEY_B] = "KEY_B", - [KEY_N] = "KEY_N", - [KEY_M] = "KEY_M", - [KEY_COMMA] = "KEY_COMMA", - [KEY_DOT] = "KEY_DOT", - [KEY_SLASH] = "KEY_SLASH", - [KEY_RIGHTSHIFT] = "KEY_RIGHTSHIFT", - [KEY_KPASTERISK] = "KEY_KPASTERISK", - [KEY_LEFTALT] = "KEY_LEFTALT", - [KEY_SPACE] = "KEY_SPACE", - [KEY_CAPSLOCK] = "KEY_CAPSLOCK", - [KEY_F1] = "KEY_F1", - [KEY_F2] = "KEY_F2", - [KEY_F3] = "KEY_F3", - [KEY_F4] = "KEY_F4", - [KEY_F5] = "KEY_F5", - [KEY_F6] = "KEY_F6", - [KEY_F7] = "KEY_F7", - [KEY_F8] = "KEY_F8", - [KEY_F9] = "KEY_F9", - [KEY_F10] = "KEY_F10", - [KEY_NUMLOCK] = "KEY_NUMLOCK", - [KEY_SCROLLLOCK] = "KEY_SCROLLLOCK", - [KEY_KP7] = "KEY_KP7", - [KEY_KP8] = "KEY_KP8", - [KEY_KP9] = "KEY_KP9", - [KEY_KPMINUS] = "KEY_KPMINUS", - [KEY_KP4] = "KEY_KP4", - [KEY_KP5] = "KEY_KP5", - [KEY_KP6] = "KEY_KP6", - [KEY_KPPLUS] = "KEY_KPPLUS", - [KEY_KP1] = "KEY_KP1", - [KEY_KP2] = "KEY_KP2", - [KEY_KP3] = "KEY_KP3", - [KEY_KP0] = "KEY_KP0", - [KEY_KPDOT] = "KEY_KPDOT", - [KEY_ZENKAKUHANKAKU] = "KEY_ZENKAKUHANKAKU", - [KEY_102ND] = "KEY_102ND", - [KEY_F11] = "KEY_F11", - [KEY_F12] = "KEY_F12", - [KEY_RO] = "KEY_RO", - [KEY_KATAKANA] = "KEY_KATAKANA", - [KEY_HIRAGANA] = "KEY_HIRAGANA", - [KEY_HENKAN] = "KEY_HENKAN", - [KEY_KATAKANAHIRAGANA] = "KEY_KATAKANAHIRAGANA", - [KEY_MUHENKAN] = "KEY_MUHENKAN", - [KEY_KPJPCOMMA] = "KEY_KPJPCOMMA", - [KEY_KPENTER] = "KEY_KPENTER", - [KEY_RIGHTCTRL] = "KEY_RIGHTCTRL", - [KEY_KPSLASH] = "KEY_KPSLASH", - [KEY_SYSRQ] = "KEY_SYSRQ", - [KEY_RIGHTALT] = "KEY_RIGHTALT", - [KEY_LINEFEED] = "KEY_LINEFEED", - [KEY_HOME] = "KEY_HOME", - [KEY_UP] = "KEY_UP", - [KEY_PAGEUP] = "KEY_PAGEUP", - [KEY_LEFT] = "KEY_LEFT", - [KEY_RIGHT] = "KEY_RIGHT", - [KEY_END] = "KEY_END", - [KEY_DOWN] = "KEY_DOWN", - [KEY_PAGEDOWN] = "KEY_PAGEDOWN", - [KEY_INSERT] = "KEY_INSERT", - [KEY_DELETE] = "KEY_DELETE", - [KEY_MACRO] = "KEY_MACRO", - [KEY_MUTE] = "KEY_MUTE", - [KEY_VOLUMEDOWN] = "KEY_VOLUMEDOWN", - [KEY_VOLUMEUP] = "KEY_VOLUMEUP", - [KEY_POWER] = "KEY_POWER", - [KEY_KPEQUAL] = "KEY_KPEQUAL", - [KEY_KPPLUSMINUS] = "KEY_KPPLUSMINUS", - [KEY_PAUSE] = "KEY_PAUSE", - [KEY_SCALE] = "KEY_SCALE", - [KEY_KPCOMMA] = "KEY_KPCOMMA", - [KEY_HANGEUL] = "KEY_HANGEUL", - [KEY_HANJA] = "KEY_HANJA", - [KEY_YEN] = "KEY_YEN", - [KEY_LEFTMETA] = "KEY_LEFTMETA", - [KEY_RIGHTMETA] = "KEY_RIGHTMETA", - [KEY_COMPOSE] = "KEY_COMPOSE", - [KEY_STOP] = "KEY_STOP", - [KEY_AGAIN] = "KEY_AGAIN", - [KEY_PROPS] = "KEY_PROPS", - [KEY_UNDO] = "KEY_UNDO", - [KEY_FRONT] = "KEY_FRONT", - [KEY_COPY] = "KEY_COPY", - [KEY_OPEN] = "KEY_OPEN", - [KEY_PASTE] = "KEY_PASTE", - [KEY_FIND] = "KEY_FIND", - [KEY_CUT] = "KEY_CUT", - [KEY_HELP] = "KEY_HELP", - [KEY_MENU] = "KEY_MENU", - [KEY_CALC] = "KEY_CALC", - [KEY_SETUP] = "KEY_SETUP", - [KEY_SLEEP] = "KEY_SLEEP", - [KEY_WAKEUP] = "KEY_WAKEUP", - [KEY_FILE] = "KEY_FILE", - [KEY_SENDFILE] = "KEY_SENDFILE", - [KEY_DELETEFILE] = "KEY_DELETEFILE", - [KEY_XFER] = "KEY_XFER", - [KEY_PROG1] = "KEY_PROG1", - [KEY_PROG2] = "KEY_PROG2", - [KEY_WWW] = "KEY_WWW", - [KEY_MSDOS] = "KEY_MSDOS", - [KEY_COFFEE] = "KEY_COFFEE", - [KEY_ROTATE_DISPLAY] = "KEY_ROTATE_DISPLAY", - [KEY_CYCLEWINDOWS] = "KEY_CYCLEWINDOWS", - [KEY_MAIL] = "KEY_MAIL", - [KEY_BOOKMARKS] = "KEY_BOOKMARKS", - [KEY_COMPUTER] = "KEY_COMPUTER", - [KEY_BACK] = "KEY_BACK", - [KEY_FORWARD] = "KEY_FORWARD", - [KEY_CLOSECD] = "KEY_CLOSECD", - [KEY_EJECTCD] = "KEY_EJECTCD", - [KEY_EJECTCLOSECD] = "KEY_EJECTCLOSECD", - [KEY_NEXTSONG] = "KEY_NEXTSONG", - [KEY_PLAYPAUSE] = "KEY_PLAYPAUSE", - [KEY_PREVIOUSSONG] = "KEY_PREVIOUSSONG", - [KEY_STOPCD] = "KEY_STOPCD", - [KEY_RECORD] = "KEY_RECORD", - [KEY_REWIND] = "KEY_REWIND", - [KEY_PHONE] = "KEY_PHONE", - [KEY_ISO] = "KEY_ISO", - [KEY_CONFIG] = "KEY_CONFIG", - [KEY_HOMEPAGE] = "KEY_HOMEPAGE", - [KEY_REFRESH] = "KEY_REFRESH", - [KEY_EXIT] = "KEY_EXIT", - [KEY_MOVE] = "KEY_MOVE", - [KEY_EDIT] = "KEY_EDIT", - [KEY_SCROLLUP] = "KEY_SCROLLUP", - [KEY_SCROLLDOWN] = "KEY_SCROLLDOWN", - [KEY_KPLEFTPAREN] = "KEY_KPLEFTPAREN", - [KEY_KPRIGHTPAREN] = "KEY_KPRIGHTPAREN", - [KEY_NEW] = "KEY_NEW", - [KEY_REDO] = "KEY_REDO", - [KEY_F13] = "KEY_F13", - [KEY_F14] = "KEY_F14", - [KEY_F15] = "KEY_F15", - [KEY_F16] = "KEY_F16", - [KEY_F17] = "KEY_F17", - [KEY_F18] = "KEY_F18", - [KEY_F19] = "KEY_F19", - [KEY_F20] = "KEY_F20", - [KEY_F21] = "KEY_F21", - [KEY_F22] = "KEY_F22", - [KEY_F23] = "KEY_F23", - [KEY_F24] = "KEY_F24", - [KEY_PLAYCD] = "KEY_PLAYCD", - [KEY_PAUSECD] = "KEY_PAUSECD", - [KEY_PROG3] = "KEY_PROG3", - [KEY_PROG4] = "KEY_PROG4", - [KEY_ALL_APPLICATIONS] = "KEY_ALL_APPLICATIONS", - [KEY_SUSPEND] = "KEY_SUSPEND", - [KEY_CLOSE] = "KEY_CLOSE", - [KEY_PLAY] = "KEY_PLAY", - [KEY_FASTFORWARD] = "KEY_FASTFORWARD", - [KEY_BASSBOOST] = "KEY_BASSBOOST", - [KEY_PRINT] = "KEY_PRINT", - [KEY_HP] = "KEY_HP", - [KEY_CAMERA] = "KEY_CAMERA", - [KEY_SOUND] = "KEY_SOUND", - [KEY_QUESTION] = "KEY_QUESTION", - [KEY_EMAIL] = "KEY_EMAIL", - [KEY_CHAT] = "KEY_CHAT", - [KEY_SEARCH] = "KEY_SEARCH", - [KEY_CONNECT] = "KEY_CONNECT", - [KEY_FINANCE] = "KEY_FINANCE", - [KEY_SPORT] = "KEY_SPORT", - [KEY_SHOP] = "KEY_SHOP", - [KEY_ALTERASE] = "KEY_ALTERASE", - [KEY_CANCEL] = "KEY_CANCEL", - [KEY_BRIGHTNESSDOWN] = "KEY_BRIGHTNESSDOWN", - [KEY_BRIGHTNESSUP] = "KEY_BRIGHTNESSUP", - [KEY_MEDIA] = "KEY_MEDIA", - [KEY_SWITCHVIDEOMODE] = "KEY_SWITCHVIDEOMODE", - [KEY_KBDILLUMTOGGLE] = "KEY_KBDILLUMTOGGLE", - [KEY_KBDILLUMDOWN] = "KEY_KBDILLUMDOWN", - [KEY_KBDILLUMUP] = "KEY_KBDILLUMUP", - [KEY_SEND] = "KEY_SEND", - [KEY_REPLY] = "KEY_REPLY", - [KEY_FORWARDMAIL] = "KEY_FORWARDMAIL", - [KEY_SAVE] = "KEY_SAVE", - [KEY_DOCUMENTS] = "KEY_DOCUMENTS", - [KEY_BATTERY] = "KEY_BATTERY", - [KEY_BLUETOOTH] = "KEY_BLUETOOTH", - [KEY_WLAN] = "KEY_WLAN", - [KEY_UWB] = "KEY_UWB", - [KEY_UNKNOWN] = "KEY_UNKNOWN", - [KEY_VIDEO_NEXT] = "KEY_VIDEO_NEXT", - [KEY_VIDEO_PREV] = "KEY_VIDEO_PREV", - [KEY_BRIGHTNESS_CYCLE] = "KEY_BRIGHTNESS_CYCLE", - [KEY_BRIGHTNESS_AUTO] = "KEY_BRIGHTNESS_AUTO", - [KEY_DISPLAY_OFF] = "KEY_DISPLAY_OFF", - [KEY_WWAN] = "KEY_WWAN", - [KEY_RFKILL] = "KEY_RFKILL", - [KEY_MICMUTE] = "KEY_MICMUTE", - [KEY_OK] = "KEY_OK", - [KEY_SELECT] = "KEY_SELECT", - [KEY_GOTO] = "KEY_GOTO", - [KEY_CLEAR] = "KEY_CLEAR", - [KEY_POWER2] = "KEY_POWER2", - [KEY_OPTION] = "KEY_OPTION", - [KEY_INFO] = "KEY_INFO", - [KEY_TIME] = "KEY_TIME", - [KEY_VENDOR] = "KEY_VENDOR", - [KEY_ARCHIVE] = "KEY_ARCHIVE", - [KEY_PROGRAM] = "KEY_PROGRAM", - [KEY_CHANNEL] = "KEY_CHANNEL", - [KEY_FAVORITES] = "KEY_FAVORITES", - [KEY_EPG] = "KEY_EPG", - [KEY_PVR] = "KEY_PVR", - [KEY_MHP] = "KEY_MHP", - [KEY_LANGUAGE] = "KEY_LANGUAGE", - [KEY_TITLE] = "KEY_TITLE", - [KEY_SUBTITLE] = "KEY_SUBTITLE", - [KEY_ANGLE] = "KEY_ANGLE", - [KEY_FULL_SCREEN] = "KEY_FULL_SCREEN", - [KEY_MODE] = "KEY_MODE", - [KEY_KEYBOARD] = "KEY_KEYBOARD", - [KEY_ASPECT_RATIO] = "KEY_ASPECT_RATIO", - [KEY_PC] = "KEY_PC", - [KEY_TV] = "KEY_TV", - [KEY_TV2] = "KEY_TV2", - [KEY_VCR] = "KEY_VCR", - [KEY_VCR2] = "KEY_VCR2", - [KEY_SAT] = "KEY_SAT", - [KEY_SAT2] = "KEY_SAT2", - [KEY_CD] = "KEY_CD", - [KEY_TAPE] = "KEY_TAPE", - [KEY_RADIO] = "KEY_RADIO", - [KEY_TUNER] = "KEY_TUNER", - [KEY_PLAYER] = "KEY_PLAYER", - [KEY_TEXT] = "KEY_TEXT", - [KEY_DVD] = "KEY_DVD", - [KEY_AUX] = "KEY_AUX", - [KEY_MP3] = "KEY_MP3", - [KEY_AUDIO] = "KEY_AUDIO", - [KEY_VIDEO] = "KEY_VIDEO", - [KEY_DIRECTORY] = "KEY_DIRECTORY", - [KEY_LIST] = "KEY_LIST", - [KEY_MEMO] = "KEY_MEMO", - [KEY_CALENDAR] = "KEY_CALENDAR", - [KEY_RED] = "KEY_RED", - [KEY_GREEN] = "KEY_GREEN", - [KEY_YELLOW] = "KEY_YELLOW", - [KEY_BLUE] = "KEY_BLUE", - [KEY_CHANNELUP] = "KEY_CHANNELUP", - [KEY_CHANNELDOWN] = "KEY_CHANNELDOWN", - [KEY_FIRST] = "KEY_FIRST", - [KEY_LAST] = "KEY_LAST", - [KEY_AB] = "KEY_AB", - [KEY_NEXT] = "KEY_NEXT", - [KEY_RESTART] = "KEY_RESTART", - [KEY_SLOW] = "KEY_SLOW", - [KEY_SHUFFLE] = "KEY_SHUFFLE", - [KEY_BREAK] = "KEY_BREAK", - [KEY_PREVIOUS] = "KEY_PREVIOUS", - [KEY_DIGITS] = "KEY_DIGITS", - [KEY_TEEN] = "KEY_TEEN", - [KEY_TWEN] = "KEY_TWEN", - [KEY_VIDEOPHONE] = "KEY_VIDEOPHONE", - [KEY_GAMES] = "KEY_GAMES", - [KEY_ZOOMIN] = "KEY_ZOOMIN", - [KEY_ZOOMOUT] = "KEY_ZOOMOUT", - [KEY_ZOOMRESET] = "KEY_ZOOMRESET", - [KEY_WORDPROCESSOR] = "KEY_WORDPROCESSOR", - [KEY_EDITOR] = "KEY_EDITOR", - [KEY_SPREADSHEET] = "KEY_SPREADSHEET", - [KEY_GRAPHICSEDITOR] = "KEY_GRAPHICSEDITOR", - [KEY_PRESENTATION] = "KEY_PRESENTATION", - [KEY_DATABASE] = "KEY_DATABASE", - [KEY_NEWS] = "KEY_NEWS", - [KEY_VOICEMAIL] = "KEY_VOICEMAIL", - [KEY_ADDRESSBOOK] = "KEY_ADDRESSBOOK", - [KEY_MESSENGER] = "KEY_MESSENGER", - [KEY_DISPLAYTOGGLE] = "KEY_DISPLAYTOGGLE", - [KEY_SPELLCHECK] = "KEY_SPELLCHECK", - [KEY_LOGOFF] = "KEY_LOGOFF", - [KEY_DOLLAR] = "KEY_DOLLAR", - [KEY_EURO] = "KEY_EURO", - [KEY_FRAMEBACK] = "KEY_FRAMEBACK", - [KEY_FRAMEFORWARD] = "KEY_FRAMEFORWARD", - [KEY_CONTEXT_MENU] = "KEY_CONTEXT_MENU", - [KEY_MEDIA_REPEAT] = "KEY_MEDIA_REPEAT", - [KEY_10CHANNELSUP] = "KEY_10CHANNELSUP", - [KEY_10CHANNELSDOWN] = "KEY_10CHANNELSDOWN", - [KEY_IMAGES] = "KEY_IMAGES", - [KEY_NOTIFICATION_CENTER] = "KEY_NOTIFICATION_CENTER", - [KEY_PICKUP_PHONE] = "KEY_PICKUP_PHONE", - [KEY_HANGUP_PHONE] = "KEY_HANGUP_PHONE", - [KEY_DEL_EOL] = "KEY_DEL_EOL", - [KEY_DEL_EOS] = "KEY_DEL_EOS", - [KEY_INS_LINE] = "KEY_INS_LINE", - [KEY_DEL_LINE] = "KEY_DEL_LINE", - [KEY_FN] = "KEY_FN", - [KEY_FN_ESC] = "KEY_FN_ESC", - [KEY_FN_F1] = "KEY_FN_F1", - [KEY_FN_F2] = "KEY_FN_F2", - [KEY_FN_F3] = "KEY_FN_F3", - [KEY_FN_F4] = "KEY_FN_F4", - [KEY_FN_F5] = "KEY_FN_F5", - [KEY_FN_F6] = "KEY_FN_F6", - [KEY_FN_F7] = "KEY_FN_F7", - [KEY_FN_F8] = "KEY_FN_F8", - [KEY_FN_F9] = "KEY_FN_F9", - [KEY_FN_F10] = "KEY_FN_F10", - [KEY_FN_F11] = "KEY_FN_F11", - [KEY_FN_F12] = "KEY_FN_F12", - [KEY_FN_1] = "KEY_FN_1", - [KEY_FN_2] = "KEY_FN_2", - [KEY_FN_D] = "KEY_FN_D", - [KEY_FN_E] = "KEY_FN_E", - [KEY_FN_F] = "KEY_FN_F", - [KEY_FN_S] = "KEY_FN_S", - [KEY_FN_B] = "KEY_FN_B", - [KEY_FN_RIGHT_SHIFT] = "KEY_FN_RIGHT_SHIFT", - [KEY_BRL_DOT1] = "KEY_BRL_DOT1", - [KEY_BRL_DOT2] = "KEY_BRL_DOT2", - [KEY_BRL_DOT3] = "KEY_BRL_DOT3", - [KEY_BRL_DOT4] = "KEY_BRL_DOT4", - [KEY_BRL_DOT5] = "KEY_BRL_DOT5", - [KEY_BRL_DOT6] = "KEY_BRL_DOT6", - [KEY_BRL_DOT7] = "KEY_BRL_DOT7", - [KEY_BRL_DOT8] = "KEY_BRL_DOT8", - [KEY_BRL_DOT9] = "KEY_BRL_DOT9", - [KEY_BRL_DOT10] = "KEY_BRL_DOT10", - [KEY_NUMERIC_0] = "KEY_NUMERIC_0", - [KEY_NUMERIC_1] = "KEY_NUMERIC_1", - [KEY_NUMERIC_2] = "KEY_NUMERIC_2", - [KEY_NUMERIC_3] = "KEY_NUMERIC_3", - [KEY_NUMERIC_4] = "KEY_NUMERIC_4", - [KEY_NUMERIC_5] = "KEY_NUMERIC_5", - [KEY_NUMERIC_6] = "KEY_NUMERIC_6", - [KEY_NUMERIC_7] = "KEY_NUMERIC_7", - [KEY_NUMERIC_8] = "KEY_NUMERIC_8", - [KEY_NUMERIC_9] = "KEY_NUMERIC_9", - [KEY_NUMERIC_STAR] = "KEY_NUMERIC_STAR", - [KEY_NUMERIC_POUND] = "KEY_NUMERIC_POUND", - [KEY_NUMERIC_A] = "KEY_NUMERIC_A", - [KEY_NUMERIC_B] = "KEY_NUMERIC_B", - [KEY_NUMERIC_C] = "KEY_NUMERIC_C", - [KEY_NUMERIC_D] = "KEY_NUMERIC_D", - [KEY_CAMERA_FOCUS] = "KEY_CAMERA_FOCUS", - [KEY_WPS_BUTTON] = "KEY_WPS_BUTTON", - [KEY_TOUCHPAD_TOGGLE] = "KEY_TOUCHPAD_TOGGLE", - [KEY_TOUCHPAD_ON] = "KEY_TOUCHPAD_ON", - [KEY_TOUCHPAD_OFF] = "KEY_TOUCHPAD_OFF", - [KEY_CAMERA_ZOOMIN] = "KEY_CAMERA_ZOOMIN", - [KEY_CAMERA_ZOOMOUT] = "KEY_CAMERA_ZOOMOUT", - [KEY_CAMERA_UP] = "KEY_CAMERA_UP", - [KEY_CAMERA_DOWN] = "KEY_CAMERA_DOWN", - [KEY_CAMERA_LEFT] = "KEY_CAMERA_LEFT", - [KEY_CAMERA_RIGHT] = "KEY_CAMERA_RIGHT", - [KEY_ATTENDANT_ON] = "KEY_ATTENDANT_ON", - [KEY_ATTENDANT_OFF] = "KEY_ATTENDANT_OFF", - [KEY_ATTENDANT_TOGGLE] = "KEY_ATTENDANT_TOGGLE", - [KEY_LIGHTS_TOGGLE] = "KEY_LIGHTS_TOGGLE", - [KEY_ALS_TOGGLE] = "KEY_ALS_TOGGLE", - [KEY_ROTATE_LOCK_TOGGLE] = "KEY_ROTATE_LOCK_TOGGLE", - [KEY_BUTTONCONFIG] = "KEY_BUTTONCONFIG", - [KEY_TASKMANAGER] = "KEY_TASKMANAGER", - [KEY_JOURNAL] = "KEY_JOURNAL", - [KEY_CONTROLPANEL] = "KEY_CONTROLPANEL", - [KEY_APPSELECT] = "KEY_APPSELECT", - [KEY_SCREENSAVER] = "KEY_SCREENSAVER", - [KEY_VOICECOMMAND] = "KEY_VOICECOMMAND", - [KEY_ASSISTANT] = "KEY_ASSISTANT", - [KEY_KBD_LAYOUT_NEXT] = "KEY_KBD_LAYOUT_NEXT", - [KEY_EMOJI_PICKER] = "KEY_EMOJI_PICKER", - [KEY_DICTATE] = "KEY_DICTATE", - [KEY_CAMERA_ACCESS_ENABLE] = "KEY_CAMERA_ACCESS_ENABLE", - [KEY_CAMERA_ACCESS_DISABLE] = "KEY_CAMERA_ACCESS_DISABLE", - [KEY_CAMERA_ACCESS_TOGGLE] = "KEY_CAMERA_ACCESS_TOGGLE", - [KEY_BRIGHTNESS_MIN] = "KEY_BRIGHTNESS_MIN", - [KEY_BRIGHTNESS_MAX] = "KEY_BRIGHTNESS_MAX", - [KEY_KBDINPUTASSIST_PREV] = "KEY_KBDINPUTASSIST_PREV", - [KEY_KBDINPUTASSIST_NEXT] = "KEY_KBDINPUTASSIST_NEXT", - [KEY_KBDINPUTASSIST_PREVGROUP] = "KEY_KBDINPUTASSIST_PREVGROUP", - [KEY_KBDINPUTASSIST_NEXTGROUP] = "KEY_KBDINPUTASSIST_NEXTGROUP", - [KEY_KBDINPUTASSIST_ACCEPT] = "KEY_KBDINPUTASSIST_ACCEPT", - [KEY_KBDINPUTASSIST_CANCEL] = "KEY_KBDINPUTASSIST_CANCEL", - [KEY_RIGHT_UP] = "KEY_RIGHT_UP", - [KEY_RIGHT_DOWN] = "KEY_RIGHT_DOWN", - [KEY_LEFT_UP] = "KEY_LEFT_UP", - [KEY_LEFT_DOWN] = "KEY_LEFT_DOWN", - [KEY_ROOT_MENU] = "KEY_ROOT_MENU", - [KEY_MEDIA_TOP_MENU] = "KEY_MEDIA_TOP_MENU", - [KEY_NUMERIC_11] = "KEY_NUMERIC_11", - [KEY_NUMERIC_12] = "KEY_NUMERIC_12", - [KEY_AUDIO_DESC] = "KEY_AUDIO_DESC", - [KEY_3D_MODE] = "KEY_3D_MODE", - [KEY_NEXT_FAVORITE] = "KEY_NEXT_FAVORITE", - [KEY_STOP_RECORD] = "KEY_STOP_RECORD", - [KEY_PAUSE_RECORD] = "KEY_PAUSE_RECORD", - [KEY_VOD] = "KEY_VOD", - [KEY_UNMUTE] = "KEY_UNMUTE", - [KEY_FASTREVERSE] = "KEY_FASTREVERSE", - [KEY_SLOWREVERSE] = "KEY_SLOWREVERSE", - [KEY_DATA] = "KEY_DATA", - [KEY_ONSCREEN_KEYBOARD] = "KEY_ONSCREEN_KEYBOARD", - [KEY_PRIVACY_SCREEN_TOGGLE] = "KEY_PRIVACY_SCREEN_TOGGLE", - [KEY_SELECTIVE_SCREENSHOT] = "KEY_SELECTIVE_SCREENSHOT", - [KEY_NEXT_ELEMENT] = "KEY_NEXT_ELEMENT", - [KEY_PREVIOUS_ELEMENT] = "KEY_PREVIOUS_ELEMENT", - [KEY_AUTOPILOT_ENGAGE_TOGGLE] = "KEY_AUTOPILOT_ENGAGE_TOGGLE", - [KEY_MARK_WAYPOINT] = "KEY_MARK_WAYPOINT", - [KEY_SOS] = "KEY_SOS", - [KEY_NAV_CHART] = "KEY_NAV_CHART", - [KEY_FISHING_CHART] = "KEY_FISHING_CHART", - [KEY_SINGLE_RANGE_RADAR] = "KEY_SINGLE_RANGE_RADAR", - [KEY_DUAL_RANGE_RADAR] = "KEY_DUAL_RANGE_RADAR", - [KEY_RADAR_OVERLAY] = "KEY_RADAR_OVERLAY", - [KEY_TRADITIONAL_SONAR] = "KEY_TRADITIONAL_SONAR", - [KEY_CLEARVU_SONAR] = "KEY_CLEARVU_SONAR", - [KEY_SIDEVU_SONAR] = "KEY_SIDEVU_SONAR", - [KEY_NAV_INFO] = "KEY_NAV_INFO", - [KEY_BRIGHTNESS_MENU] = "KEY_BRIGHTNESS_MENU", - [KEY_MACRO1] = "KEY_MACRO1", - [KEY_MACRO2] = "KEY_MACRO2", - [KEY_MACRO3] = "KEY_MACRO3", - [KEY_MACRO4] = "KEY_MACRO4", - [KEY_MACRO5] = "KEY_MACRO5", - [KEY_MACRO6] = "KEY_MACRO6", - [KEY_MACRO7] = "KEY_MACRO7", - [KEY_MACRO8] = "KEY_MACRO8", - [KEY_MACRO9] = "KEY_MACRO9", - [KEY_MACRO10] = "KEY_MACRO10", - [KEY_MACRO11] = "KEY_MACRO11", - [KEY_MACRO12] = "KEY_MACRO12", - [KEY_MACRO13] = "KEY_MACRO13", - [KEY_MACRO14] = "KEY_MACRO14", - [KEY_MACRO15] = "KEY_MACRO15", - [KEY_MACRO16] = "KEY_MACRO16", - [KEY_MACRO17] = "KEY_MACRO17", - [KEY_MACRO18] = "KEY_MACRO18", - [KEY_MACRO19] = "KEY_MACRO19", - [KEY_MACRO20] = "KEY_MACRO20", - [KEY_MACRO21] = "KEY_MACRO21", - [KEY_MACRO22] = "KEY_MACRO22", - [KEY_MACRO23] = "KEY_MACRO23", - [KEY_MACRO24] = "KEY_MACRO24", - [KEY_MACRO25] = "KEY_MACRO25", - [KEY_MACRO26] = "KEY_MACRO26", - [KEY_MACRO27] = "KEY_MACRO27", - [KEY_MACRO28] = "KEY_MACRO28", - [KEY_MACRO29] = "KEY_MACRO29", - [KEY_MACRO30] = "KEY_MACRO30", - [KEY_MACRO_RECORD_START] = "KEY_MACRO_RECORD_START", - [KEY_MACRO_RECORD_STOP] = "KEY_MACRO_RECORD_STOP", - [KEY_MACRO_PRESET_CYCLE] = "KEY_MACRO_PRESET_CYCLE", - [KEY_MACRO_PRESET1] = "KEY_MACRO_PRESET1", - [KEY_MACRO_PRESET2] = "KEY_MACRO_PRESET2", - [KEY_MACRO_PRESET3] = "KEY_MACRO_PRESET3", - [KEY_KBD_LCD_MENU1] = "KEY_KBD_LCD_MENU1", - [KEY_KBD_LCD_MENU2] = "KEY_KBD_LCD_MENU2", - [KEY_KBD_LCD_MENU3] = "KEY_KBD_LCD_MENU3", - [KEY_KBD_LCD_MENU4] = "KEY_KBD_LCD_MENU4", - [KEY_KBD_LCD_MENU5] = "KEY_KBD_LCD_MENU5", - [KEY_MAX] = "KEY_MAX", - [BTN_0] = "BTN_0", - [BTN_1] = "BTN_1", - [BTN_2] = "BTN_2", - [BTN_3] = "BTN_3", - [BTN_4] = "BTN_4", - [BTN_5] = "BTN_5", - [BTN_6] = "BTN_6", - [BTN_7] = "BTN_7", - [BTN_8] = "BTN_8", - [BTN_9] = "BTN_9", - [BTN_LEFT] = "BTN_LEFT", - [BTN_RIGHT] = "BTN_RIGHT", - [BTN_MIDDLE] = "BTN_MIDDLE", - [BTN_SIDE] = "BTN_SIDE", - [BTN_EXTRA] = "BTN_EXTRA", - [BTN_FORWARD] = "BTN_FORWARD", - [BTN_BACK] = "BTN_BACK", - [BTN_TASK] = "BTN_TASK", - [BTN_TRIGGER] = "BTN_TRIGGER", - [BTN_THUMB] = "BTN_THUMB", - [BTN_THUMB2] = "BTN_THUMB2", - [BTN_TOP] = "BTN_TOP", - [BTN_TOP2] = "BTN_TOP2", - [BTN_PINKIE] = "BTN_PINKIE", - [BTN_BASE] = "BTN_BASE", - [BTN_BASE2] = "BTN_BASE2", - [BTN_BASE3] = "BTN_BASE3", - [BTN_BASE4] = "BTN_BASE4", - [BTN_BASE5] = "BTN_BASE5", - [BTN_BASE6] = "BTN_BASE6", - [BTN_DEAD] = "BTN_DEAD", - [BTN_SOUTH] = "BTN_SOUTH", - [BTN_EAST] = "BTN_EAST", - [BTN_C] = "BTN_C", - [BTN_NORTH] = "BTN_NORTH", - [BTN_WEST] = "BTN_WEST", - [BTN_Z] = "BTN_Z", - [BTN_TL] = "BTN_TL", - [BTN_TR] = "BTN_TR", - [BTN_TL2] = "BTN_TL2", - [BTN_TR2] = "BTN_TR2", - [BTN_SELECT] = "BTN_SELECT", - [BTN_START] = "BTN_START", - [BTN_MODE] = "BTN_MODE", - [BTN_THUMBL] = "BTN_THUMBL", - [BTN_THUMBR] = "BTN_THUMBR", - [BTN_TOOL_PEN] = "BTN_TOOL_PEN", - [BTN_TOOL_RUBBER] = "BTN_TOOL_RUBBER", - [BTN_TOOL_BRUSH] = "BTN_TOOL_BRUSH", - [BTN_TOOL_PENCIL] = "BTN_TOOL_PENCIL", - [BTN_TOOL_AIRBRUSH] = "BTN_TOOL_AIRBRUSH", - [BTN_TOOL_FINGER] = "BTN_TOOL_FINGER", - [BTN_TOOL_MOUSE] = "BTN_TOOL_MOUSE", - [BTN_TOOL_LENS] = "BTN_TOOL_LENS", - [BTN_TOOL_QUINTTAP] = "BTN_TOOL_QUINTTAP", - [BTN_STYLUS3] = "BTN_STYLUS3", - [BTN_TOUCH] = "BTN_TOUCH", - [BTN_STYLUS] = "BTN_STYLUS", - [BTN_STYLUS2] = "BTN_STYLUS2", - [BTN_TOOL_DOUBLETAP] = "BTN_TOOL_DOUBLETAP", - [BTN_TOOL_TRIPLETAP] = "BTN_TOOL_TRIPLETAP", - [BTN_TOOL_QUADTAP] = "BTN_TOOL_QUADTAP", - [BTN_GEAR_DOWN] = "BTN_GEAR_DOWN", - [BTN_GEAR_UP] = "BTN_GEAR_UP", - [BTN_DPAD_UP] = "BTN_DPAD_UP", - [BTN_DPAD_DOWN] = "BTN_DPAD_DOWN", - [BTN_DPAD_LEFT] = "BTN_DPAD_LEFT", - [BTN_DPAD_RIGHT] = "BTN_DPAD_RIGHT", - [BTN_TRIGGER_HAPPY1] = "BTN_TRIGGER_HAPPY1", - [BTN_TRIGGER_HAPPY2] = "BTN_TRIGGER_HAPPY2", - [BTN_TRIGGER_HAPPY3] = "BTN_TRIGGER_HAPPY3", - [BTN_TRIGGER_HAPPY4] = "BTN_TRIGGER_HAPPY4", - [BTN_TRIGGER_HAPPY5] = "BTN_TRIGGER_HAPPY5", - [BTN_TRIGGER_HAPPY6] = "BTN_TRIGGER_HAPPY6", - [BTN_TRIGGER_HAPPY7] = "BTN_TRIGGER_HAPPY7", - [BTN_TRIGGER_HAPPY8] = "BTN_TRIGGER_HAPPY8", - [BTN_TRIGGER_HAPPY9] = "BTN_TRIGGER_HAPPY9", - [BTN_TRIGGER_HAPPY10] = "BTN_TRIGGER_HAPPY10", - [BTN_TRIGGER_HAPPY11] = "BTN_TRIGGER_HAPPY11", - [BTN_TRIGGER_HAPPY12] = "BTN_TRIGGER_HAPPY12", - [BTN_TRIGGER_HAPPY13] = "BTN_TRIGGER_HAPPY13", - [BTN_TRIGGER_HAPPY14] = "BTN_TRIGGER_HAPPY14", - [BTN_TRIGGER_HAPPY15] = "BTN_TRIGGER_HAPPY15", - [BTN_TRIGGER_HAPPY16] = "BTN_TRIGGER_HAPPY16", - [BTN_TRIGGER_HAPPY17] = "BTN_TRIGGER_HAPPY17", - [BTN_TRIGGER_HAPPY18] = "BTN_TRIGGER_HAPPY18", - [BTN_TRIGGER_HAPPY19] = "BTN_TRIGGER_HAPPY19", - [BTN_TRIGGER_HAPPY20] = "BTN_TRIGGER_HAPPY20", - [BTN_TRIGGER_HAPPY21] = "BTN_TRIGGER_HAPPY21", - [BTN_TRIGGER_HAPPY22] = "BTN_TRIGGER_HAPPY22", - [BTN_TRIGGER_HAPPY23] = "BTN_TRIGGER_HAPPY23", - [BTN_TRIGGER_HAPPY24] = "BTN_TRIGGER_HAPPY24", - [BTN_TRIGGER_HAPPY25] = "BTN_TRIGGER_HAPPY25", - [BTN_TRIGGER_HAPPY26] = "BTN_TRIGGER_HAPPY26", - [BTN_TRIGGER_HAPPY27] = "BTN_TRIGGER_HAPPY27", - [BTN_TRIGGER_HAPPY28] = "BTN_TRIGGER_HAPPY28", - [BTN_TRIGGER_HAPPY29] = "BTN_TRIGGER_HAPPY29", - [BTN_TRIGGER_HAPPY30] = "BTN_TRIGGER_HAPPY30", - [BTN_TRIGGER_HAPPY31] = "BTN_TRIGGER_HAPPY31", - [BTN_TRIGGER_HAPPY32] = "BTN_TRIGGER_HAPPY32", - [BTN_TRIGGER_HAPPY33] = "BTN_TRIGGER_HAPPY33", - [BTN_TRIGGER_HAPPY34] = "BTN_TRIGGER_HAPPY34", - [BTN_TRIGGER_HAPPY35] = "BTN_TRIGGER_HAPPY35", - [BTN_TRIGGER_HAPPY36] = "BTN_TRIGGER_HAPPY36", - [BTN_TRIGGER_HAPPY37] = "BTN_TRIGGER_HAPPY37", - [BTN_TRIGGER_HAPPY38] = "BTN_TRIGGER_HAPPY38", - [BTN_TRIGGER_HAPPY39] = "BTN_TRIGGER_HAPPY39", - [BTN_TRIGGER_HAPPY40] = "BTN_TRIGGER_HAPPY40", -}; - -static const char *const led_map[LED_MAX + 1] = { - [LED_NUML] = "LED_NUML", - [LED_CAPSL] = "LED_CAPSL", - [LED_SCROLLL] = "LED_SCROLLL", - [LED_COMPOSE] = "LED_COMPOSE", - [LED_KANA] = "LED_KANA", - [LED_SLEEP] = "LED_SLEEP", - [LED_SUSPEND] = "LED_SUSPEND", - [LED_MUTE] = "LED_MUTE", - [LED_MISC] = "LED_MISC", - [LED_MAIL] = "LED_MAIL", - [LED_CHARGING] = "LED_CHARGING", - [LED_MAX] = "LED_MAX", -}; - -static const char *const snd_map[SND_MAX + 1] = { - [SND_CLICK] = "SND_CLICK", - [SND_BELL] = "SND_BELL", - [SND_TONE] = "SND_TONE", - [SND_MAX] = "SND_MAX", -}; - -static const char *const msc_map[MSC_MAX + 1] = { - [MSC_SERIAL] = "MSC_SERIAL", - [MSC_PULSELED] = "MSC_PULSELED", - [MSC_GESTURE] = "MSC_GESTURE", - [MSC_RAW] = "MSC_RAW", - [MSC_SCAN] = "MSC_SCAN", - [MSC_TIMESTAMP] = "MSC_TIMESTAMP", - [MSC_MAX] = "MSC_MAX", -}; - -static const char *const sw_map[SW_MAX + 1] = { - [SW_LID] = "SW_LID", - [SW_TABLET_MODE] = "SW_TABLET_MODE", - [SW_HEADPHONE_INSERT] = "SW_HEADPHONE_INSERT", - [SW_RFKILL_ALL] = "SW_RFKILL_ALL", - [SW_MICROPHONE_INSERT] = "SW_MICROPHONE_INSERT", - [SW_DOCK] = "SW_DOCK", - [SW_LINEOUT_INSERT] = "SW_LINEOUT_INSERT", - [SW_JACK_PHYSICAL_INSERT] = "SW_JACK_PHYSICAL_INSERT", - [SW_VIDEOOUT_INSERT] = "SW_VIDEOOUT_INSERT", - [SW_CAMERA_LENS_COVER] = "SW_CAMERA_LENS_COVER", - [SW_KEYPAD_SLIDE] = "SW_KEYPAD_SLIDE", - [SW_FRONT_PROXIMITY] = "SW_FRONT_PROXIMITY", - [SW_ROTATE_LOCK] = "SW_ROTATE_LOCK", - [SW_LINEIN_INSERT] = "SW_LINEIN_INSERT", - [SW_MUTE_DEVICE] = "SW_MUTE_DEVICE", - [SW_PEN_INSERTED] = "SW_PEN_INSERTED", - [SW_MACHINE_COVER] = "SW_MACHINE_COVER", -}; - -static const char *const ff_map[FF_MAX + 1] = { - [FF_STATUS_STOPPED] = "FF_STATUS_STOPPED", - [FF_STATUS_MAX] = "FF_STATUS_MAX", - [FF_RUMBLE] = "FF_RUMBLE", - [FF_PERIODIC] = "FF_PERIODIC", - [FF_CONSTANT] = "FF_CONSTANT", - [FF_SPRING] = "FF_SPRING", - [FF_FRICTION] = "FF_FRICTION", - [FF_DAMPER] = "FF_DAMPER", - [FF_INERTIA] = "FF_INERTIA", - [FF_RAMP] = "FF_RAMP", - [FF_SQUARE] = "FF_SQUARE", - [FF_TRIANGLE] = "FF_TRIANGLE", - [FF_SINE] = "FF_SINE", - [FF_SAW_UP] = "FF_SAW_UP", - [FF_SAW_DOWN] = "FF_SAW_DOWN", - [FF_CUSTOM] = "FF_CUSTOM", - [FF_GAIN] = "FF_GAIN", - [FF_AUTOCENTER] = "FF_AUTOCENTER", - [FF_MAX] = "FF_MAX", -}; - -static const char *const syn_map[SYN_MAX + 1] = { - [SYN_REPORT] = "SYN_REPORT", - [SYN_CONFIG] = "SYN_CONFIG", - [SYN_MT_REPORT] = "SYN_MT_REPORT", - [SYN_DROPPED] = "SYN_DROPPED", - [SYN_MAX] = "SYN_MAX", -}; - -static const char *const rep_map[REP_MAX + 1] = { - [REP_DELAY] = "REP_DELAY", - [REP_PERIOD] = "REP_PERIOD", -}; - -static const char *const input_prop_map[INPUT_PROP_MAX + 1] = { - [INPUT_PROP_POINTER] = "INPUT_PROP_POINTER", - [INPUT_PROP_DIRECT] = "INPUT_PROP_DIRECT", - [INPUT_PROP_BUTTONPAD] = "INPUT_PROP_BUTTONPAD", - [INPUT_PROP_SEMI_MT] = "INPUT_PROP_SEMI_MT", - [INPUT_PROP_TOPBUTTONPAD] = "INPUT_PROP_TOPBUTTONPAD", - [INPUT_PROP_POINTING_STICK] = "INPUT_PROP_POINTING_STICK", - [INPUT_PROP_ACCELEROMETER] = "INPUT_PROP_ACCELEROMETER", - [INPUT_PROP_MAX] = "INPUT_PROP_MAX", -}; - -static const char *const mt_tool_map[MT_TOOL_MAX + 1] = { - [MT_TOOL_FINGER] = "MT_TOOL_FINGER", - [MT_TOOL_PEN] = "MT_TOOL_PEN", - [MT_TOOL_PALM] = "MT_TOOL_PALM", - [MT_TOOL_DIAL] = "MT_TOOL_DIAL", - [MT_TOOL_MAX] = "MT_TOOL_MAX", -}; - -static const char *const *const event_type_map[EV_MAX + 1] = { - [EV_REL] = rel_map, - [EV_ABS] = abs_map, - [EV_KEY] = key_map, - [EV_LED] = led_map, - [EV_SND] = snd_map, - [EV_MSC] = msc_map, - [EV_SW] = sw_map, - [EV_FF] = ff_map, - [EV_SYN] = syn_map, - [EV_REP] = rep_map, -}; - -#if __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Winitializer-overrides" -#elif __GNUC__ - #pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Woverride-init" -#endif -static const int ev_max[EV_MAX + 1] = { - SYN_MAX, - KEY_MAX, - REL_MAX, - ABS_MAX, - MSC_MAX, - SW_MAX, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - LED_MAX, - SND_MAX, - -1, - REP_MAX, - FF_MAX, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, -}; -#if __clang__ -#pragma clang diagnostic pop /* "-Winitializer-overrides" */ -#elif __GNUC__ -#pragma GCC diagnostic pop /* "-Woverride-init" */ -#endif - -struct name_entry { - const char *name; - unsigned int value; -}; - -static const struct name_entry tool_type_names[] = { - {.name = "MT_TOOL_DIAL", .value = MT_TOOL_DIAL}, - {.name = "MT_TOOL_FINGER", .value = MT_TOOL_FINGER}, - {.name = "MT_TOOL_MAX", .value = MT_TOOL_MAX}, - {.name = "MT_TOOL_PALM", .value = MT_TOOL_PALM}, - {.name = "MT_TOOL_PEN", .value = MT_TOOL_PEN}, -}; - -static const struct name_entry ev_names[] = { - {.name = "EV_ABS", .value = EV_ABS}, - {.name = "EV_FF", .value = EV_FF}, - {.name = "EV_FF_STATUS", .value = EV_FF_STATUS}, - {.name = "EV_KEY", .value = EV_KEY}, - {.name = "EV_LED", .value = EV_LED}, - {.name = "EV_MAX", .value = EV_MAX}, - {.name = "EV_MSC", .value = EV_MSC}, - {.name = "EV_PWR", .value = EV_PWR}, - {.name = "EV_REL", .value = EV_REL}, - {.name = "EV_REP", .value = EV_REP}, - {.name = "EV_SND", .value = EV_SND}, - {.name = "EV_SW", .value = EV_SW}, - {.name = "EV_SYN", .value = EV_SYN}, -}; - -static const struct name_entry code_names[] = { - {.name = "ABS_BRAKE", .value = ABS_BRAKE}, - {.name = "ABS_DISTANCE", .value = ABS_DISTANCE}, - {.name = "ABS_GAS", .value = ABS_GAS}, - {.name = "ABS_HAT0X", .value = ABS_HAT0X}, - {.name = "ABS_HAT0Y", .value = ABS_HAT0Y}, - {.name = "ABS_HAT1X", .value = ABS_HAT1X}, - {.name = "ABS_HAT1Y", .value = ABS_HAT1Y}, - {.name = "ABS_HAT2X", .value = ABS_HAT2X}, - {.name = "ABS_HAT2Y", .value = ABS_HAT2Y}, - {.name = "ABS_HAT3X", .value = ABS_HAT3X}, - {.name = "ABS_HAT3Y", .value = ABS_HAT3Y}, - {.name = "ABS_MAX", .value = ABS_MAX}, - {.name = "ABS_MISC", .value = ABS_MISC}, - {.name = "ABS_MT_BLOB_ID", .value = ABS_MT_BLOB_ID}, - {.name = "ABS_MT_DISTANCE", .value = ABS_MT_DISTANCE}, - {.name = "ABS_MT_ORIENTATION", .value = ABS_MT_ORIENTATION}, - {.name = "ABS_MT_POSITION_X", .value = ABS_MT_POSITION_X}, - {.name = "ABS_MT_POSITION_Y", .value = ABS_MT_POSITION_Y}, - {.name = "ABS_MT_PRESSURE", .value = ABS_MT_PRESSURE}, - {.name = "ABS_MT_SLOT", .value = ABS_MT_SLOT}, - {.name = "ABS_MT_TOOL_TYPE", .value = ABS_MT_TOOL_TYPE}, - {.name = "ABS_MT_TOOL_X", .value = ABS_MT_TOOL_X}, - {.name = "ABS_MT_TOOL_Y", .value = ABS_MT_TOOL_Y}, - {.name = "ABS_MT_TOUCH_MAJOR", .value = ABS_MT_TOUCH_MAJOR}, - {.name = "ABS_MT_TOUCH_MINOR", .value = ABS_MT_TOUCH_MINOR}, - {.name = "ABS_MT_TRACKING_ID", .value = ABS_MT_TRACKING_ID}, - {.name = "ABS_MT_WIDTH_MAJOR", .value = ABS_MT_WIDTH_MAJOR}, - {.name = "ABS_MT_WIDTH_MINOR", .value = ABS_MT_WIDTH_MINOR}, - {.name = "ABS_PRESSURE", .value = ABS_PRESSURE}, - {.name = "ABS_PROFILE", .value = ABS_PROFILE}, - {.name = "ABS_RESERVED", .value = ABS_RESERVED}, - {.name = "ABS_RUDDER", .value = ABS_RUDDER}, - {.name = "ABS_RX", .value = ABS_RX}, - {.name = "ABS_RY", .value = ABS_RY}, - {.name = "ABS_RZ", .value = ABS_RZ}, - {.name = "ABS_THROTTLE", .value = ABS_THROTTLE}, - {.name = "ABS_TILT_X", .value = ABS_TILT_X}, - {.name = "ABS_TILT_Y", .value = ABS_TILT_Y}, - {.name = "ABS_TOOL_WIDTH", .value = ABS_TOOL_WIDTH}, - {.name = "ABS_VOLUME", .value = ABS_VOLUME}, - {.name = "ABS_WHEEL", .value = ABS_WHEEL}, - {.name = "ABS_X", .value = ABS_X}, - {.name = "ABS_Y", .value = ABS_Y}, - {.name = "ABS_Z", .value = ABS_Z}, - {.name = "BTN_0", .value = BTN_0}, - {.name = "BTN_1", .value = BTN_1}, - {.name = "BTN_2", .value = BTN_2}, - {.name = "BTN_3", .value = BTN_3}, - {.name = "BTN_4", .value = BTN_4}, - {.name = "BTN_5", .value = BTN_5}, - {.name = "BTN_6", .value = BTN_6}, - {.name = "BTN_7", .value = BTN_7}, - {.name = "BTN_8", .value = BTN_8}, - {.name = "BTN_9", .value = BTN_9}, - {.name = "BTN_A", .value = BTN_A}, - {.name = "BTN_B", .value = BTN_B}, - {.name = "BTN_BACK", .value = BTN_BACK}, - {.name = "BTN_BASE", .value = BTN_BASE}, - {.name = "BTN_BASE2", .value = BTN_BASE2}, - {.name = "BTN_BASE3", .value = BTN_BASE3}, - {.name = "BTN_BASE4", .value = BTN_BASE4}, - {.name = "BTN_BASE5", .value = BTN_BASE5}, - {.name = "BTN_BASE6", .value = BTN_BASE6}, - {.name = "BTN_C", .value = BTN_C}, - {.name = "BTN_DEAD", .value = BTN_DEAD}, - {.name = "BTN_DPAD_DOWN", .value = BTN_DPAD_DOWN}, - {.name = "BTN_DPAD_LEFT", .value = BTN_DPAD_LEFT}, - {.name = "BTN_DPAD_RIGHT", .value = BTN_DPAD_RIGHT}, - {.name = "BTN_DPAD_UP", .value = BTN_DPAD_UP}, - {.name = "BTN_EAST", .value = BTN_EAST}, - {.name = "BTN_EXTRA", .value = BTN_EXTRA}, - {.name = "BTN_FORWARD", .value = BTN_FORWARD}, - {.name = "BTN_GEAR_DOWN", .value = BTN_GEAR_DOWN}, - {.name = "BTN_GEAR_UP", .value = BTN_GEAR_UP}, - {.name = "BTN_LEFT", .value = BTN_LEFT}, - {.name = "BTN_MIDDLE", .value = BTN_MIDDLE}, - {.name = "BTN_MODE", .value = BTN_MODE}, - {.name = "BTN_NORTH", .value = BTN_NORTH}, - {.name = "BTN_PINKIE", .value = BTN_PINKIE}, - {.name = "BTN_RIGHT", .value = BTN_RIGHT}, - {.name = "BTN_SELECT", .value = BTN_SELECT}, - {.name = "BTN_SIDE", .value = BTN_SIDE}, - {.name = "BTN_SOUTH", .value = BTN_SOUTH}, - {.name = "BTN_START", .value = BTN_START}, - {.name = "BTN_STYLUS", .value = BTN_STYLUS}, - {.name = "BTN_STYLUS2", .value = BTN_STYLUS2}, - {.name = "BTN_STYLUS3", .value = BTN_STYLUS3}, - {.name = "BTN_TASK", .value = BTN_TASK}, - {.name = "BTN_THUMB", .value = BTN_THUMB}, - {.name = "BTN_THUMB2", .value = BTN_THUMB2}, - {.name = "BTN_THUMBL", .value = BTN_THUMBL}, - {.name = "BTN_THUMBR", .value = BTN_THUMBR}, - {.name = "BTN_TL", .value = BTN_TL}, - {.name = "BTN_TL2", .value = BTN_TL2}, - {.name = "BTN_TOOL_AIRBRUSH", .value = BTN_TOOL_AIRBRUSH}, - {.name = "BTN_TOOL_BRUSH", .value = BTN_TOOL_BRUSH}, - {.name = "BTN_TOOL_DOUBLETAP", .value = BTN_TOOL_DOUBLETAP}, - {.name = "BTN_TOOL_FINGER", .value = BTN_TOOL_FINGER}, - {.name = "BTN_TOOL_LENS", .value = BTN_TOOL_LENS}, - {.name = "BTN_TOOL_MOUSE", .value = BTN_TOOL_MOUSE}, - {.name = "BTN_TOOL_PEN", .value = BTN_TOOL_PEN}, - {.name = "BTN_TOOL_PENCIL", .value = BTN_TOOL_PENCIL}, - {.name = "BTN_TOOL_QUADTAP", .value = BTN_TOOL_QUADTAP}, - {.name = "BTN_TOOL_QUINTTAP", .value = BTN_TOOL_QUINTTAP}, - {.name = "BTN_TOOL_RUBBER", .value = BTN_TOOL_RUBBER}, - {.name = "BTN_TOOL_TRIPLETAP", .value = BTN_TOOL_TRIPLETAP}, - {.name = "BTN_TOP", .value = BTN_TOP}, - {.name = "BTN_TOP2", .value = BTN_TOP2}, - {.name = "BTN_TOUCH", .value = BTN_TOUCH}, - {.name = "BTN_TR", .value = BTN_TR}, - {.name = "BTN_TR2", .value = BTN_TR2}, - {.name = "BTN_TRIGGER", .value = BTN_TRIGGER}, - {.name = "BTN_TRIGGER_HAPPY1", .value = BTN_TRIGGER_HAPPY1}, - {.name = "BTN_TRIGGER_HAPPY10", .value = BTN_TRIGGER_HAPPY10}, - {.name = "BTN_TRIGGER_HAPPY11", .value = BTN_TRIGGER_HAPPY11}, - {.name = "BTN_TRIGGER_HAPPY12", .value = BTN_TRIGGER_HAPPY12}, - {.name = "BTN_TRIGGER_HAPPY13", .value = BTN_TRIGGER_HAPPY13}, - {.name = "BTN_TRIGGER_HAPPY14", .value = BTN_TRIGGER_HAPPY14}, - {.name = "BTN_TRIGGER_HAPPY15", .value = BTN_TRIGGER_HAPPY15}, - {.name = "BTN_TRIGGER_HAPPY16", .value = BTN_TRIGGER_HAPPY16}, - {.name = "BTN_TRIGGER_HAPPY17", .value = BTN_TRIGGER_HAPPY17}, - {.name = "BTN_TRIGGER_HAPPY18", .value = BTN_TRIGGER_HAPPY18}, - {.name = "BTN_TRIGGER_HAPPY19", .value = BTN_TRIGGER_HAPPY19}, - {.name = "BTN_TRIGGER_HAPPY2", .value = BTN_TRIGGER_HAPPY2}, - {.name = "BTN_TRIGGER_HAPPY20", .value = BTN_TRIGGER_HAPPY20}, - {.name = "BTN_TRIGGER_HAPPY21", .value = BTN_TRIGGER_HAPPY21}, - {.name = "BTN_TRIGGER_HAPPY22", .value = BTN_TRIGGER_HAPPY22}, - {.name = "BTN_TRIGGER_HAPPY23", .value = BTN_TRIGGER_HAPPY23}, - {.name = "BTN_TRIGGER_HAPPY24", .value = BTN_TRIGGER_HAPPY24}, - {.name = "BTN_TRIGGER_HAPPY25", .value = BTN_TRIGGER_HAPPY25}, - {.name = "BTN_TRIGGER_HAPPY26", .value = BTN_TRIGGER_HAPPY26}, - {.name = "BTN_TRIGGER_HAPPY27", .value = BTN_TRIGGER_HAPPY27}, - {.name = "BTN_TRIGGER_HAPPY28", .value = BTN_TRIGGER_HAPPY28}, - {.name = "BTN_TRIGGER_HAPPY29", .value = BTN_TRIGGER_HAPPY29}, - {.name = "BTN_TRIGGER_HAPPY3", .value = BTN_TRIGGER_HAPPY3}, - {.name = "BTN_TRIGGER_HAPPY30", .value = BTN_TRIGGER_HAPPY30}, - {.name = "BTN_TRIGGER_HAPPY31", .value = BTN_TRIGGER_HAPPY31}, - {.name = "BTN_TRIGGER_HAPPY32", .value = BTN_TRIGGER_HAPPY32}, - {.name = "BTN_TRIGGER_HAPPY33", .value = BTN_TRIGGER_HAPPY33}, - {.name = "BTN_TRIGGER_HAPPY34", .value = BTN_TRIGGER_HAPPY34}, - {.name = "BTN_TRIGGER_HAPPY35", .value = BTN_TRIGGER_HAPPY35}, - {.name = "BTN_TRIGGER_HAPPY36", .value = BTN_TRIGGER_HAPPY36}, - {.name = "BTN_TRIGGER_HAPPY37", .value = BTN_TRIGGER_HAPPY37}, - {.name = "BTN_TRIGGER_HAPPY38", .value = BTN_TRIGGER_HAPPY38}, - {.name = "BTN_TRIGGER_HAPPY39", .value = BTN_TRIGGER_HAPPY39}, - {.name = "BTN_TRIGGER_HAPPY4", .value = BTN_TRIGGER_HAPPY4}, - {.name = "BTN_TRIGGER_HAPPY40", .value = BTN_TRIGGER_HAPPY40}, - {.name = "BTN_TRIGGER_HAPPY5", .value = BTN_TRIGGER_HAPPY5}, - {.name = "BTN_TRIGGER_HAPPY6", .value = BTN_TRIGGER_HAPPY6}, - {.name = "BTN_TRIGGER_HAPPY7", .value = BTN_TRIGGER_HAPPY7}, - {.name = "BTN_TRIGGER_HAPPY8", .value = BTN_TRIGGER_HAPPY8}, - {.name = "BTN_TRIGGER_HAPPY9", .value = BTN_TRIGGER_HAPPY9}, - {.name = "BTN_WEST", .value = BTN_WEST}, - {.name = "BTN_X", .value = BTN_X}, - {.name = "BTN_Y", .value = BTN_Y}, - {.name = "BTN_Z", .value = BTN_Z}, - {.name = "FF_AUTOCENTER", .value = FF_AUTOCENTER}, - {.name = "FF_CONSTANT", .value = FF_CONSTANT}, - {.name = "FF_CUSTOM", .value = FF_CUSTOM}, - {.name = "FF_DAMPER", .value = FF_DAMPER}, - {.name = "FF_FRICTION", .value = FF_FRICTION}, - {.name = "FF_GAIN", .value = FF_GAIN}, - {.name = "FF_INERTIA", .value = FF_INERTIA}, - {.name = "FF_MAX", .value = FF_MAX}, - {.name = "FF_PERIODIC", .value = FF_PERIODIC}, - {.name = "FF_RAMP", .value = FF_RAMP}, - {.name = "FF_RUMBLE", .value = FF_RUMBLE}, - {.name = "FF_SAW_DOWN", .value = FF_SAW_DOWN}, - {.name = "FF_SAW_UP", .value = FF_SAW_UP}, - {.name = "FF_SINE", .value = FF_SINE}, - {.name = "FF_SPRING", .value = FF_SPRING}, - {.name = "FF_SQUARE", .value = FF_SQUARE}, - {.name = "FF_STATUS_MAX", .value = FF_STATUS_MAX}, - {.name = "FF_STATUS_STOPPED", .value = FF_STATUS_STOPPED}, - {.name = "FF_TRIANGLE", .value = FF_TRIANGLE}, - {.name = "KEY_0", .value = KEY_0}, - {.name = "KEY_1", .value = KEY_1}, - {.name = "KEY_102ND", .value = KEY_102ND}, - {.name = "KEY_10CHANNELSDOWN", .value = KEY_10CHANNELSDOWN}, - {.name = "KEY_10CHANNELSUP", .value = KEY_10CHANNELSUP}, - {.name = "KEY_2", .value = KEY_2}, - {.name = "KEY_3", .value = KEY_3}, - {.name = "KEY_3D_MODE", .value = KEY_3D_MODE}, - {.name = "KEY_4", .value = KEY_4}, - {.name = "KEY_5", .value = KEY_5}, - {.name = "KEY_6", .value = KEY_6}, - {.name = "KEY_7", .value = KEY_7}, - {.name = "KEY_8", .value = KEY_8}, - {.name = "KEY_9", .value = KEY_9}, - {.name = "KEY_A", .value = KEY_A}, - {.name = "KEY_AB", .value = KEY_AB}, - {.name = "KEY_ADDRESSBOOK", .value = KEY_ADDRESSBOOK}, - {.name = "KEY_AGAIN", .value = KEY_AGAIN}, - {.name = "KEY_ALL_APPLICATIONS", .value = KEY_ALL_APPLICATIONS}, - {.name = "KEY_ALS_TOGGLE", .value = KEY_ALS_TOGGLE}, - {.name = "KEY_ALTERASE", .value = KEY_ALTERASE}, - {.name = "KEY_ANGLE", .value = KEY_ANGLE}, - {.name = "KEY_APOSTROPHE", .value = KEY_APOSTROPHE}, - {.name = "KEY_APPSELECT", .value = KEY_APPSELECT}, - {.name = "KEY_ARCHIVE", .value = KEY_ARCHIVE}, - {.name = "KEY_ASPECT_RATIO", .value = KEY_ASPECT_RATIO}, - {.name = "KEY_ASSISTANT", .value = KEY_ASSISTANT}, - {.name = "KEY_ATTENDANT_OFF", .value = KEY_ATTENDANT_OFF}, - {.name = "KEY_ATTENDANT_ON", .value = KEY_ATTENDANT_ON}, - {.name = "KEY_ATTENDANT_TOGGLE", .value = KEY_ATTENDANT_TOGGLE}, - {.name = "KEY_AUDIO", .value = KEY_AUDIO}, - {.name = "KEY_AUDIO_DESC", .value = KEY_AUDIO_DESC}, - {.name = "KEY_AUTOPILOT_ENGAGE_TOGGLE", .value = KEY_AUTOPILOT_ENGAGE_TOGGLE}, - {.name = "KEY_AUX", .value = KEY_AUX}, - {.name = "KEY_B", .value = KEY_B}, - {.name = "KEY_BACK", .value = KEY_BACK}, - {.name = "KEY_BACKSLASH", .value = KEY_BACKSLASH}, - {.name = "KEY_BACKSPACE", .value = KEY_BACKSPACE}, - {.name = "KEY_BASSBOOST", .value = KEY_BASSBOOST}, - {.name = "KEY_BATTERY", .value = KEY_BATTERY}, - {.name = "KEY_BLUE", .value = KEY_BLUE}, - {.name = "KEY_BLUETOOTH", .value = KEY_BLUETOOTH}, - {.name = "KEY_BOOKMARKS", .value = KEY_BOOKMARKS}, - {.name = "KEY_BREAK", .value = KEY_BREAK}, - {.name = "KEY_BRIGHTNESSDOWN", .value = KEY_BRIGHTNESSDOWN}, - {.name = "KEY_BRIGHTNESSUP", .value = KEY_BRIGHTNESSUP}, - {.name = "KEY_BRIGHTNESS_AUTO", .value = KEY_BRIGHTNESS_AUTO}, - {.name = "KEY_BRIGHTNESS_CYCLE", .value = KEY_BRIGHTNESS_CYCLE}, - {.name = "KEY_BRIGHTNESS_MAX", .value = KEY_BRIGHTNESS_MAX}, - {.name = "KEY_BRIGHTNESS_MENU", .value = KEY_BRIGHTNESS_MENU}, - {.name = "KEY_BRIGHTNESS_MIN", .value = KEY_BRIGHTNESS_MIN}, - {.name = "KEY_BRL_DOT1", .value = KEY_BRL_DOT1}, - {.name = "KEY_BRL_DOT10", .value = KEY_BRL_DOT10}, - {.name = "KEY_BRL_DOT2", .value = KEY_BRL_DOT2}, - {.name = "KEY_BRL_DOT3", .value = KEY_BRL_DOT3}, - {.name = "KEY_BRL_DOT4", .value = KEY_BRL_DOT4}, - {.name = "KEY_BRL_DOT5", .value = KEY_BRL_DOT5}, - {.name = "KEY_BRL_DOT6", .value = KEY_BRL_DOT6}, - {.name = "KEY_BRL_DOT7", .value = KEY_BRL_DOT7}, - {.name = "KEY_BRL_DOT8", .value = KEY_BRL_DOT8}, - {.name = "KEY_BRL_DOT9", .value = KEY_BRL_DOT9}, - {.name = "KEY_BUTTONCONFIG", .value = KEY_BUTTONCONFIG}, - {.name = "KEY_C", .value = KEY_C}, - {.name = "KEY_CALC", .value = KEY_CALC}, - {.name = "KEY_CALENDAR", .value = KEY_CALENDAR}, - {.name = "KEY_CAMERA", .value = KEY_CAMERA}, - {.name = "KEY_CAMERA_ACCESS_DISABLE", .value = KEY_CAMERA_ACCESS_DISABLE}, - {.name = "KEY_CAMERA_ACCESS_ENABLE", .value = KEY_CAMERA_ACCESS_ENABLE}, - {.name = "KEY_CAMERA_ACCESS_TOGGLE", .value = KEY_CAMERA_ACCESS_TOGGLE}, - {.name = "KEY_CAMERA_DOWN", .value = KEY_CAMERA_DOWN}, - {.name = "KEY_CAMERA_FOCUS", .value = KEY_CAMERA_FOCUS}, - {.name = "KEY_CAMERA_LEFT", .value = KEY_CAMERA_LEFT}, - {.name = "KEY_CAMERA_RIGHT", .value = KEY_CAMERA_RIGHT}, - {.name = "KEY_CAMERA_UP", .value = KEY_CAMERA_UP}, - {.name = "KEY_CAMERA_ZOOMIN", .value = KEY_CAMERA_ZOOMIN}, - {.name = "KEY_CAMERA_ZOOMOUT", .value = KEY_CAMERA_ZOOMOUT}, - {.name = "KEY_CANCEL", .value = KEY_CANCEL}, - {.name = "KEY_CAPSLOCK", .value = KEY_CAPSLOCK}, - {.name = "KEY_CD", .value = KEY_CD}, - {.name = "KEY_CHANNEL", .value = KEY_CHANNEL}, - {.name = "KEY_CHANNELDOWN", .value = KEY_CHANNELDOWN}, - {.name = "KEY_CHANNELUP", .value = KEY_CHANNELUP}, - {.name = "KEY_CHAT", .value = KEY_CHAT}, - {.name = "KEY_CLEAR", .value = KEY_CLEAR}, - {.name = "KEY_CLEARVU_SONAR", .value = KEY_CLEARVU_SONAR}, - {.name = "KEY_CLOSE", .value = KEY_CLOSE}, - {.name = "KEY_CLOSECD", .value = KEY_CLOSECD}, - {.name = "KEY_COFFEE", .value = KEY_COFFEE}, - {.name = "KEY_COMMA", .value = KEY_COMMA}, - {.name = "KEY_COMPOSE", .value = KEY_COMPOSE}, - {.name = "KEY_COMPUTER", .value = KEY_COMPUTER}, - {.name = "KEY_CONFIG", .value = KEY_CONFIG}, - {.name = "KEY_CONNECT", .value = KEY_CONNECT}, - {.name = "KEY_CONTEXT_MENU", .value = KEY_CONTEXT_MENU}, - {.name = "KEY_CONTROLPANEL", .value = KEY_CONTROLPANEL}, - {.name = "KEY_COPY", .value = KEY_COPY}, - {.name = "KEY_CUT", .value = KEY_CUT}, - {.name = "KEY_CYCLEWINDOWS", .value = KEY_CYCLEWINDOWS}, - {.name = "KEY_D", .value = KEY_D}, - {.name = "KEY_DATA", .value = KEY_DATA}, - {.name = "KEY_DATABASE", .value = KEY_DATABASE}, - {.name = "KEY_DELETE", .value = KEY_DELETE}, - {.name = "KEY_DELETEFILE", .value = KEY_DELETEFILE}, - {.name = "KEY_DEL_EOL", .value = KEY_DEL_EOL}, - {.name = "KEY_DEL_EOS", .value = KEY_DEL_EOS}, - {.name = "KEY_DEL_LINE", .value = KEY_DEL_LINE}, - {.name = "KEY_DICTATE", .value = KEY_DICTATE}, - {.name = "KEY_DIGITS", .value = KEY_DIGITS}, - {.name = "KEY_DIRECTORY", .value = KEY_DIRECTORY}, - {.name = "KEY_DISPLAYTOGGLE", .value = KEY_DISPLAYTOGGLE}, - {.name = "KEY_DISPLAY_OFF", .value = KEY_DISPLAY_OFF}, - {.name = "KEY_DOCUMENTS", .value = KEY_DOCUMENTS}, - {.name = "KEY_DOLLAR", .value = KEY_DOLLAR}, - {.name = "KEY_DOT", .value = KEY_DOT}, - {.name = "KEY_DOWN", .value = KEY_DOWN}, - {.name = "KEY_DUAL_RANGE_RADAR", .value = KEY_DUAL_RANGE_RADAR}, - {.name = "KEY_DVD", .value = KEY_DVD}, - {.name = "KEY_E", .value = KEY_E}, - {.name = "KEY_EDIT", .value = KEY_EDIT}, - {.name = "KEY_EDITOR", .value = KEY_EDITOR}, - {.name = "KEY_EJECTCD", .value = KEY_EJECTCD}, - {.name = "KEY_EJECTCLOSECD", .value = KEY_EJECTCLOSECD}, - {.name = "KEY_EMAIL", .value = KEY_EMAIL}, - {.name = "KEY_EMOJI_PICKER", .value = KEY_EMOJI_PICKER}, - {.name = "KEY_END", .value = KEY_END}, - {.name = "KEY_ENTER", .value = KEY_ENTER}, - {.name = "KEY_EPG", .value = KEY_EPG}, - {.name = "KEY_EQUAL", .value = KEY_EQUAL}, - {.name = "KEY_ESC", .value = KEY_ESC}, - {.name = "KEY_EURO", .value = KEY_EURO}, - {.name = "KEY_EXIT", .value = KEY_EXIT}, - {.name = "KEY_F", .value = KEY_F}, - {.name = "KEY_F1", .value = KEY_F1}, - {.name = "KEY_F10", .value = KEY_F10}, - {.name = "KEY_F11", .value = KEY_F11}, - {.name = "KEY_F12", .value = KEY_F12}, - {.name = "KEY_F13", .value = KEY_F13}, - {.name = "KEY_F14", .value = KEY_F14}, - {.name = "KEY_F15", .value = KEY_F15}, - {.name = "KEY_F16", .value = KEY_F16}, - {.name = "KEY_F17", .value = KEY_F17}, - {.name = "KEY_F18", .value = KEY_F18}, - {.name = "KEY_F19", .value = KEY_F19}, - {.name = "KEY_F2", .value = KEY_F2}, - {.name = "KEY_F20", .value = KEY_F20}, - {.name = "KEY_F21", .value = KEY_F21}, - {.name = "KEY_F22", .value = KEY_F22}, - {.name = "KEY_F23", .value = KEY_F23}, - {.name = "KEY_F24", .value = KEY_F24}, - {.name = "KEY_F3", .value = KEY_F3}, - {.name = "KEY_F4", .value = KEY_F4}, - {.name = "KEY_F5", .value = KEY_F5}, - {.name = "KEY_F6", .value = KEY_F6}, - {.name = "KEY_F7", .value = KEY_F7}, - {.name = "KEY_F8", .value = KEY_F8}, - {.name = "KEY_F9", .value = KEY_F9}, - {.name = "KEY_FASTFORWARD", .value = KEY_FASTFORWARD}, - {.name = "KEY_FASTREVERSE", .value = KEY_FASTREVERSE}, - {.name = "KEY_FAVORITES", .value = KEY_FAVORITES}, - {.name = "KEY_FILE", .value = KEY_FILE}, - {.name = "KEY_FINANCE", .value = KEY_FINANCE}, - {.name = "KEY_FIND", .value = KEY_FIND}, - {.name = "KEY_FIRST", .value = KEY_FIRST}, - {.name = "KEY_FISHING_CHART", .value = KEY_FISHING_CHART}, - {.name = "KEY_FN", .value = KEY_FN}, - {.name = "KEY_FN_1", .value = KEY_FN_1}, - {.name = "KEY_FN_2", .value = KEY_FN_2}, - {.name = "KEY_FN_B", .value = KEY_FN_B}, - {.name = "KEY_FN_D", .value = KEY_FN_D}, - {.name = "KEY_FN_E", .value = KEY_FN_E}, - {.name = "KEY_FN_ESC", .value = KEY_FN_ESC}, - {.name = "KEY_FN_F", .value = KEY_FN_F}, - {.name = "KEY_FN_F1", .value = KEY_FN_F1}, - {.name = "KEY_FN_F10", .value = KEY_FN_F10}, - {.name = "KEY_FN_F11", .value = KEY_FN_F11}, - {.name = "KEY_FN_F12", .value = KEY_FN_F12}, - {.name = "KEY_FN_F2", .value = KEY_FN_F2}, - {.name = "KEY_FN_F3", .value = KEY_FN_F3}, - {.name = "KEY_FN_F4", .value = KEY_FN_F4}, - {.name = "KEY_FN_F5", .value = KEY_FN_F5}, - {.name = "KEY_FN_F6", .value = KEY_FN_F6}, - {.name = "KEY_FN_F7", .value = KEY_FN_F7}, - {.name = "KEY_FN_F8", .value = KEY_FN_F8}, - {.name = "KEY_FN_F9", .value = KEY_FN_F9}, - {.name = "KEY_FN_RIGHT_SHIFT", .value = KEY_FN_RIGHT_SHIFT}, - {.name = "KEY_FN_S", .value = KEY_FN_S}, - {.name = "KEY_FORWARD", .value = KEY_FORWARD}, - {.name = "KEY_FORWARDMAIL", .value = KEY_FORWARDMAIL}, - {.name = "KEY_FRAMEBACK", .value = KEY_FRAMEBACK}, - {.name = "KEY_FRAMEFORWARD", .value = KEY_FRAMEFORWARD}, - {.name = "KEY_FRONT", .value = KEY_FRONT}, - {.name = "KEY_FULL_SCREEN", .value = KEY_FULL_SCREEN}, - {.name = "KEY_G", .value = KEY_G}, - {.name = "KEY_GAMES", .value = KEY_GAMES}, - {.name = "KEY_GOTO", .value = KEY_GOTO}, - {.name = "KEY_GRAPHICSEDITOR", .value = KEY_GRAPHICSEDITOR}, - {.name = "KEY_GRAVE", .value = KEY_GRAVE}, - {.name = "KEY_GREEN", .value = KEY_GREEN}, - {.name = "KEY_H", .value = KEY_H}, - {.name = "KEY_HANGEUL", .value = KEY_HANGEUL}, - {.name = "KEY_HANGUP_PHONE", .value = KEY_HANGUP_PHONE}, - {.name = "KEY_HANJA", .value = KEY_HANJA}, - {.name = "KEY_HELP", .value = KEY_HELP}, - {.name = "KEY_HENKAN", .value = KEY_HENKAN}, - {.name = "KEY_HIRAGANA", .value = KEY_HIRAGANA}, - {.name = "KEY_HOME", .value = KEY_HOME}, - {.name = "KEY_HOMEPAGE", .value = KEY_HOMEPAGE}, - {.name = "KEY_HP", .value = KEY_HP}, - {.name = "KEY_I", .value = KEY_I}, - {.name = "KEY_IMAGES", .value = KEY_IMAGES}, - {.name = "KEY_INFO", .value = KEY_INFO}, - {.name = "KEY_INSERT", .value = KEY_INSERT}, - {.name = "KEY_INS_LINE", .value = KEY_INS_LINE}, - {.name = "KEY_ISO", .value = KEY_ISO}, - {.name = "KEY_J", .value = KEY_J}, - {.name = "KEY_JOURNAL", .value = KEY_JOURNAL}, - {.name = "KEY_K", .value = KEY_K}, - {.name = "KEY_KATAKANA", .value = KEY_KATAKANA}, - {.name = "KEY_KATAKANAHIRAGANA", .value = KEY_KATAKANAHIRAGANA}, - {.name = "KEY_KBDILLUMDOWN", .value = KEY_KBDILLUMDOWN}, - {.name = "KEY_KBDILLUMTOGGLE", .value = KEY_KBDILLUMTOGGLE}, - {.name = "KEY_KBDILLUMUP", .value = KEY_KBDILLUMUP}, - {.name = "KEY_KBDINPUTASSIST_ACCEPT", .value = KEY_KBDINPUTASSIST_ACCEPT}, - {.name = "KEY_KBDINPUTASSIST_CANCEL", .value = KEY_KBDINPUTASSIST_CANCEL}, - {.name = "KEY_KBDINPUTASSIST_NEXT", .value = KEY_KBDINPUTASSIST_NEXT}, - {.name = "KEY_KBDINPUTASSIST_NEXTGROUP", .value = KEY_KBDINPUTASSIST_NEXTGROUP}, - {.name = "KEY_KBDINPUTASSIST_PREV", .value = KEY_KBDINPUTASSIST_PREV}, - {.name = "KEY_KBDINPUTASSIST_PREVGROUP", .value = KEY_KBDINPUTASSIST_PREVGROUP}, - {.name = "KEY_KBD_LAYOUT_NEXT", .value = KEY_KBD_LAYOUT_NEXT}, - {.name = "KEY_KBD_LCD_MENU1", .value = KEY_KBD_LCD_MENU1}, - {.name = "KEY_KBD_LCD_MENU2", .value = KEY_KBD_LCD_MENU2}, - {.name = "KEY_KBD_LCD_MENU3", .value = KEY_KBD_LCD_MENU3}, - {.name = "KEY_KBD_LCD_MENU4", .value = KEY_KBD_LCD_MENU4}, - {.name = "KEY_KBD_LCD_MENU5", .value = KEY_KBD_LCD_MENU5}, - {.name = "KEY_KEYBOARD", .value = KEY_KEYBOARD}, - {.name = "KEY_KP0", .value = KEY_KP0}, - {.name = "KEY_KP1", .value = KEY_KP1}, - {.name = "KEY_KP2", .value = KEY_KP2}, - {.name = "KEY_KP3", .value = KEY_KP3}, - {.name = "KEY_KP4", .value = KEY_KP4}, - {.name = "KEY_KP5", .value = KEY_KP5}, - {.name = "KEY_KP6", .value = KEY_KP6}, - {.name = "KEY_KP7", .value = KEY_KP7}, - {.name = "KEY_KP8", .value = KEY_KP8}, - {.name = "KEY_KP9", .value = KEY_KP9}, - {.name = "KEY_KPASTERISK", .value = KEY_KPASTERISK}, - {.name = "KEY_KPCOMMA", .value = KEY_KPCOMMA}, - {.name = "KEY_KPDOT", .value = KEY_KPDOT}, - {.name = "KEY_KPENTER", .value = KEY_KPENTER}, - {.name = "KEY_KPEQUAL", .value = KEY_KPEQUAL}, - {.name = "KEY_KPJPCOMMA", .value = KEY_KPJPCOMMA}, - {.name = "KEY_KPLEFTPAREN", .value = KEY_KPLEFTPAREN}, - {.name = "KEY_KPMINUS", .value = KEY_KPMINUS}, - {.name = "KEY_KPPLUS", .value = KEY_KPPLUS}, - {.name = "KEY_KPPLUSMINUS", .value = KEY_KPPLUSMINUS}, - {.name = "KEY_KPRIGHTPAREN", .value = KEY_KPRIGHTPAREN}, - {.name = "KEY_KPSLASH", .value = KEY_KPSLASH}, - {.name = "KEY_L", .value = KEY_L}, - {.name = "KEY_LANGUAGE", .value = KEY_LANGUAGE}, - {.name = "KEY_LAST", .value = KEY_LAST}, - {.name = "KEY_LEFT", .value = KEY_LEFT}, - {.name = "KEY_LEFTALT", .value = KEY_LEFTALT}, - {.name = "KEY_LEFTBRACE", .value = KEY_LEFTBRACE}, - {.name = "KEY_LEFTCTRL", .value = KEY_LEFTCTRL}, - {.name = "KEY_LEFTMETA", .value = KEY_LEFTMETA}, - {.name = "KEY_LEFTSHIFT", .value = KEY_LEFTSHIFT}, - {.name = "KEY_LEFT_DOWN", .value = KEY_LEFT_DOWN}, - {.name = "KEY_LEFT_UP", .value = KEY_LEFT_UP}, - {.name = "KEY_LIGHTS_TOGGLE", .value = KEY_LIGHTS_TOGGLE}, - {.name = "KEY_LINEFEED", .value = KEY_LINEFEED}, - {.name = "KEY_LIST", .value = KEY_LIST}, - {.name = "KEY_LOGOFF", .value = KEY_LOGOFF}, - {.name = "KEY_M", .value = KEY_M}, - {.name = "KEY_MACRO", .value = KEY_MACRO}, - {.name = "KEY_MACRO1", .value = KEY_MACRO1}, - {.name = "KEY_MACRO10", .value = KEY_MACRO10}, - {.name = "KEY_MACRO11", .value = KEY_MACRO11}, - {.name = "KEY_MACRO12", .value = KEY_MACRO12}, - {.name = "KEY_MACRO13", .value = KEY_MACRO13}, - {.name = "KEY_MACRO14", .value = KEY_MACRO14}, - {.name = "KEY_MACRO15", .value = KEY_MACRO15}, - {.name = "KEY_MACRO16", .value = KEY_MACRO16}, - {.name = "KEY_MACRO17", .value = KEY_MACRO17}, - {.name = "KEY_MACRO18", .value = KEY_MACRO18}, - {.name = "KEY_MACRO19", .value = KEY_MACRO19}, - {.name = "KEY_MACRO2", .value = KEY_MACRO2}, - {.name = "KEY_MACRO20", .value = KEY_MACRO20}, - {.name = "KEY_MACRO21", .value = KEY_MACRO21}, - {.name = "KEY_MACRO22", .value = KEY_MACRO22}, - {.name = "KEY_MACRO23", .value = KEY_MACRO23}, - {.name = "KEY_MACRO24", .value = KEY_MACRO24}, - {.name = "KEY_MACRO25", .value = KEY_MACRO25}, - {.name = "KEY_MACRO26", .value = KEY_MACRO26}, - {.name = "KEY_MACRO27", .value = KEY_MACRO27}, - {.name = "KEY_MACRO28", .value = KEY_MACRO28}, - {.name = "KEY_MACRO29", .value = KEY_MACRO29}, - {.name = "KEY_MACRO3", .value = KEY_MACRO3}, - {.name = "KEY_MACRO30", .value = KEY_MACRO30}, - {.name = "KEY_MACRO4", .value = KEY_MACRO4}, - {.name = "KEY_MACRO5", .value = KEY_MACRO5}, - {.name = "KEY_MACRO6", .value = KEY_MACRO6}, - {.name = "KEY_MACRO7", .value = KEY_MACRO7}, - {.name = "KEY_MACRO8", .value = KEY_MACRO8}, - {.name = "KEY_MACRO9", .value = KEY_MACRO9}, - {.name = "KEY_MACRO_PRESET1", .value = KEY_MACRO_PRESET1}, - {.name = "KEY_MACRO_PRESET2", .value = KEY_MACRO_PRESET2}, - {.name = "KEY_MACRO_PRESET3", .value = KEY_MACRO_PRESET3}, - {.name = "KEY_MACRO_PRESET_CYCLE", .value = KEY_MACRO_PRESET_CYCLE}, - {.name = "KEY_MACRO_RECORD_START", .value = KEY_MACRO_RECORD_START}, - {.name = "KEY_MACRO_RECORD_STOP", .value = KEY_MACRO_RECORD_STOP}, - {.name = "KEY_MAIL", .value = KEY_MAIL}, - {.name = "KEY_MARK_WAYPOINT", .value = KEY_MARK_WAYPOINT}, - {.name = "KEY_MAX", .value = KEY_MAX}, - {.name = "KEY_MEDIA", .value = KEY_MEDIA}, - {.name = "KEY_MEDIA_REPEAT", .value = KEY_MEDIA_REPEAT}, - {.name = "KEY_MEDIA_TOP_MENU", .value = KEY_MEDIA_TOP_MENU}, - {.name = "KEY_MEMO", .value = KEY_MEMO}, - {.name = "KEY_MENU", .value = KEY_MENU}, - {.name = "KEY_MESSENGER", .value = KEY_MESSENGER}, - {.name = "KEY_MHP", .value = KEY_MHP}, - {.name = "KEY_MICMUTE", .value = KEY_MICMUTE}, - {.name = "KEY_MINUS", .value = KEY_MINUS}, - {.name = "KEY_MODE", .value = KEY_MODE}, - {.name = "KEY_MOVE", .value = KEY_MOVE}, - {.name = "KEY_MP3", .value = KEY_MP3}, - {.name = "KEY_MSDOS", .value = KEY_MSDOS}, - {.name = "KEY_MUHENKAN", .value = KEY_MUHENKAN}, - {.name = "KEY_MUTE", .value = KEY_MUTE}, - {.name = "KEY_N", .value = KEY_N}, - {.name = "KEY_NAV_CHART", .value = KEY_NAV_CHART}, - {.name = "KEY_NAV_INFO", .value = KEY_NAV_INFO}, - {.name = "KEY_NEW", .value = KEY_NEW}, - {.name = "KEY_NEWS", .value = KEY_NEWS}, - {.name = "KEY_NEXT", .value = KEY_NEXT}, - {.name = "KEY_NEXTSONG", .value = KEY_NEXTSONG}, - {.name = "KEY_NEXT_ELEMENT", .value = KEY_NEXT_ELEMENT}, - {.name = "KEY_NEXT_FAVORITE", .value = KEY_NEXT_FAVORITE}, - {.name = "KEY_NOTIFICATION_CENTER", .value = KEY_NOTIFICATION_CENTER}, - {.name = "KEY_NUMERIC_0", .value = KEY_NUMERIC_0}, - {.name = "KEY_NUMERIC_1", .value = KEY_NUMERIC_1}, - {.name = "KEY_NUMERIC_11", .value = KEY_NUMERIC_11}, - {.name = "KEY_NUMERIC_12", .value = KEY_NUMERIC_12}, - {.name = "KEY_NUMERIC_2", .value = KEY_NUMERIC_2}, - {.name = "KEY_NUMERIC_3", .value = KEY_NUMERIC_3}, - {.name = "KEY_NUMERIC_4", .value = KEY_NUMERIC_4}, - {.name = "KEY_NUMERIC_5", .value = KEY_NUMERIC_5}, - {.name = "KEY_NUMERIC_6", .value = KEY_NUMERIC_6}, - {.name = "KEY_NUMERIC_7", .value = KEY_NUMERIC_7}, - {.name = "KEY_NUMERIC_8", .value = KEY_NUMERIC_8}, - {.name = "KEY_NUMERIC_9", .value = KEY_NUMERIC_9}, - {.name = "KEY_NUMERIC_A", .value = KEY_NUMERIC_A}, - {.name = "KEY_NUMERIC_B", .value = KEY_NUMERIC_B}, - {.name = "KEY_NUMERIC_C", .value = KEY_NUMERIC_C}, - {.name = "KEY_NUMERIC_D", .value = KEY_NUMERIC_D}, - {.name = "KEY_NUMERIC_POUND", .value = KEY_NUMERIC_POUND}, - {.name = "KEY_NUMERIC_STAR", .value = KEY_NUMERIC_STAR}, - {.name = "KEY_NUMLOCK", .value = KEY_NUMLOCK}, - {.name = "KEY_O", .value = KEY_O}, - {.name = "KEY_OK", .value = KEY_OK}, - {.name = "KEY_ONSCREEN_KEYBOARD", .value = KEY_ONSCREEN_KEYBOARD}, - {.name = "KEY_OPEN", .value = KEY_OPEN}, - {.name = "KEY_OPTION", .value = KEY_OPTION}, - {.name = "KEY_P", .value = KEY_P}, - {.name = "KEY_PAGEDOWN", .value = KEY_PAGEDOWN}, - {.name = "KEY_PAGEUP", .value = KEY_PAGEUP}, - {.name = "KEY_PASTE", .value = KEY_PASTE}, - {.name = "KEY_PAUSE", .value = KEY_PAUSE}, - {.name = "KEY_PAUSECD", .value = KEY_PAUSECD}, - {.name = "KEY_PAUSE_RECORD", .value = KEY_PAUSE_RECORD}, - {.name = "KEY_PC", .value = KEY_PC}, - {.name = "KEY_PHONE", .value = KEY_PHONE}, - {.name = "KEY_PICKUP_PHONE", .value = KEY_PICKUP_PHONE}, - {.name = "KEY_PLAY", .value = KEY_PLAY}, - {.name = "KEY_PLAYCD", .value = KEY_PLAYCD}, - {.name = "KEY_PLAYER", .value = KEY_PLAYER}, - {.name = "KEY_PLAYPAUSE", .value = KEY_PLAYPAUSE}, - {.name = "KEY_POWER", .value = KEY_POWER}, - {.name = "KEY_POWER2", .value = KEY_POWER2}, - {.name = "KEY_PRESENTATION", .value = KEY_PRESENTATION}, - {.name = "KEY_PREVIOUS", .value = KEY_PREVIOUS}, - {.name = "KEY_PREVIOUSSONG", .value = KEY_PREVIOUSSONG}, - {.name = "KEY_PREVIOUS_ELEMENT", .value = KEY_PREVIOUS_ELEMENT}, - {.name = "KEY_PRINT", .value = KEY_PRINT}, - {.name = "KEY_PRIVACY_SCREEN_TOGGLE", .value = KEY_PRIVACY_SCREEN_TOGGLE}, - {.name = "KEY_PROG1", .value = KEY_PROG1}, - {.name = "KEY_PROG2", .value = KEY_PROG2}, - {.name = "KEY_PROG3", .value = KEY_PROG3}, - {.name = "KEY_PROG4", .value = KEY_PROG4}, - {.name = "KEY_PROGRAM", .value = KEY_PROGRAM}, - {.name = "KEY_PROPS", .value = KEY_PROPS}, - {.name = "KEY_PVR", .value = KEY_PVR}, - {.name = "KEY_Q", .value = KEY_Q}, - {.name = "KEY_QUESTION", .value = KEY_QUESTION}, - {.name = "KEY_R", .value = KEY_R}, - {.name = "KEY_RADAR_OVERLAY", .value = KEY_RADAR_OVERLAY}, - {.name = "KEY_RADIO", .value = KEY_RADIO}, - {.name = "KEY_RECORD", .value = KEY_RECORD}, - {.name = "KEY_RED", .value = KEY_RED}, - {.name = "KEY_REDO", .value = KEY_REDO}, - {.name = "KEY_REFRESH", .value = KEY_REFRESH}, - {.name = "KEY_REPLY", .value = KEY_REPLY}, - {.name = "KEY_RESERVED", .value = KEY_RESERVED}, - {.name = "KEY_RESTART", .value = KEY_RESTART}, - {.name = "KEY_REWIND", .value = KEY_REWIND}, - {.name = "KEY_RFKILL", .value = KEY_RFKILL}, - {.name = "KEY_RIGHT", .value = KEY_RIGHT}, - {.name = "KEY_RIGHTALT", .value = KEY_RIGHTALT}, - {.name = "KEY_RIGHTBRACE", .value = KEY_RIGHTBRACE}, - {.name = "KEY_RIGHTCTRL", .value = KEY_RIGHTCTRL}, - {.name = "KEY_RIGHTMETA", .value = KEY_RIGHTMETA}, - {.name = "KEY_RIGHTSHIFT", .value = KEY_RIGHTSHIFT}, - {.name = "KEY_RIGHT_DOWN", .value = KEY_RIGHT_DOWN}, - {.name = "KEY_RIGHT_UP", .value = KEY_RIGHT_UP}, - {.name = "KEY_RO", .value = KEY_RO}, - {.name = "KEY_ROOT_MENU", .value = KEY_ROOT_MENU}, - {.name = "KEY_ROTATE_DISPLAY", .value = KEY_ROTATE_DISPLAY}, - {.name = "KEY_ROTATE_LOCK_TOGGLE", .value = KEY_ROTATE_LOCK_TOGGLE}, - {.name = "KEY_S", .value = KEY_S}, - {.name = "KEY_SAT", .value = KEY_SAT}, - {.name = "KEY_SAT2", .value = KEY_SAT2}, - {.name = "KEY_SAVE", .value = KEY_SAVE}, - {.name = "KEY_SCALE", .value = KEY_SCALE}, - {.name = "KEY_SCREENSAVER", .value = KEY_SCREENSAVER}, - {.name = "KEY_SCROLLDOWN", .value = KEY_SCROLLDOWN}, - {.name = "KEY_SCROLLLOCK", .value = KEY_SCROLLLOCK}, - {.name = "KEY_SCROLLUP", .value = KEY_SCROLLUP}, - {.name = "KEY_SEARCH", .value = KEY_SEARCH}, - {.name = "KEY_SELECT", .value = KEY_SELECT}, - {.name = "KEY_SELECTIVE_SCREENSHOT", .value = KEY_SELECTIVE_SCREENSHOT}, - {.name = "KEY_SEMICOLON", .value = KEY_SEMICOLON}, - {.name = "KEY_SEND", .value = KEY_SEND}, - {.name = "KEY_SENDFILE", .value = KEY_SENDFILE}, - {.name = "KEY_SETUP", .value = KEY_SETUP}, - {.name = "KEY_SHOP", .value = KEY_SHOP}, - {.name = "KEY_SHUFFLE", .value = KEY_SHUFFLE}, - {.name = "KEY_SIDEVU_SONAR", .value = KEY_SIDEVU_SONAR}, - {.name = "KEY_SINGLE_RANGE_RADAR", .value = KEY_SINGLE_RANGE_RADAR}, - {.name = "KEY_SLASH", .value = KEY_SLASH}, - {.name = "KEY_SLEEP", .value = KEY_SLEEP}, - {.name = "KEY_SLOW", .value = KEY_SLOW}, - {.name = "KEY_SLOWREVERSE", .value = KEY_SLOWREVERSE}, - {.name = "KEY_SOS", .value = KEY_SOS}, - {.name = "KEY_SOUND", .value = KEY_SOUND}, - {.name = "KEY_SPACE", .value = KEY_SPACE}, - {.name = "KEY_SPELLCHECK", .value = KEY_SPELLCHECK}, - {.name = "KEY_SPORT", .value = KEY_SPORT}, - {.name = "KEY_SPREADSHEET", .value = KEY_SPREADSHEET}, - {.name = "KEY_STOP", .value = KEY_STOP}, - {.name = "KEY_STOPCD", .value = KEY_STOPCD}, - {.name = "KEY_STOP_RECORD", .value = KEY_STOP_RECORD}, - {.name = "KEY_SUBTITLE", .value = KEY_SUBTITLE}, - {.name = "KEY_SUSPEND", .value = KEY_SUSPEND}, - {.name = "KEY_SWITCHVIDEOMODE", .value = KEY_SWITCHVIDEOMODE}, - {.name = "KEY_SYSRQ", .value = KEY_SYSRQ}, - {.name = "KEY_T", .value = KEY_T}, - {.name = "KEY_TAB", .value = KEY_TAB}, - {.name = "KEY_TAPE", .value = KEY_TAPE}, - {.name = "KEY_TASKMANAGER", .value = KEY_TASKMANAGER}, - {.name = "KEY_TEEN", .value = KEY_TEEN}, - {.name = "KEY_TEXT", .value = KEY_TEXT}, - {.name = "KEY_TIME", .value = KEY_TIME}, - {.name = "KEY_TITLE", .value = KEY_TITLE}, - {.name = "KEY_TOUCHPAD_OFF", .value = KEY_TOUCHPAD_OFF}, - {.name = "KEY_TOUCHPAD_ON", .value = KEY_TOUCHPAD_ON}, - {.name = "KEY_TOUCHPAD_TOGGLE", .value = KEY_TOUCHPAD_TOGGLE}, - {.name = "KEY_TRADITIONAL_SONAR", .value = KEY_TRADITIONAL_SONAR}, - {.name = "KEY_TUNER", .value = KEY_TUNER}, - {.name = "KEY_TV", .value = KEY_TV}, - {.name = "KEY_TV2", .value = KEY_TV2}, - {.name = "KEY_TWEN", .value = KEY_TWEN}, - {.name = "KEY_U", .value = KEY_U}, - {.name = "KEY_UNDO", .value = KEY_UNDO}, - {.name = "KEY_UNKNOWN", .value = KEY_UNKNOWN}, - {.name = "KEY_UNMUTE", .value = KEY_UNMUTE}, - {.name = "KEY_UP", .value = KEY_UP}, - {.name = "KEY_UWB", .value = KEY_UWB}, - {.name = "KEY_V", .value = KEY_V}, - {.name = "KEY_VCR", .value = KEY_VCR}, - {.name = "KEY_VCR2", .value = KEY_VCR2}, - {.name = "KEY_VENDOR", .value = KEY_VENDOR}, - {.name = "KEY_VIDEO", .value = KEY_VIDEO}, - {.name = "KEY_VIDEOPHONE", .value = KEY_VIDEOPHONE}, - {.name = "KEY_VIDEO_NEXT", .value = KEY_VIDEO_NEXT}, - {.name = "KEY_VIDEO_PREV", .value = KEY_VIDEO_PREV}, - {.name = "KEY_VOD", .value = KEY_VOD}, - {.name = "KEY_VOICECOMMAND", .value = KEY_VOICECOMMAND}, - {.name = "KEY_VOICEMAIL", .value = KEY_VOICEMAIL}, - {.name = "KEY_VOLUMEDOWN", .value = KEY_VOLUMEDOWN}, - {.name = "KEY_VOLUMEUP", .value = KEY_VOLUMEUP}, - {.name = "KEY_W", .value = KEY_W}, - {.name = "KEY_WAKEUP", .value = KEY_WAKEUP}, - {.name = "KEY_WLAN", .value = KEY_WLAN}, - {.name = "KEY_WORDPROCESSOR", .value = KEY_WORDPROCESSOR}, - {.name = "KEY_WPS_BUTTON", .value = KEY_WPS_BUTTON}, - {.name = "KEY_WWAN", .value = KEY_WWAN}, - {.name = "KEY_WWW", .value = KEY_WWW}, - {.name = "KEY_X", .value = KEY_X}, - {.name = "KEY_XFER", .value = KEY_XFER}, - {.name = "KEY_Y", .value = KEY_Y}, - {.name = "KEY_YELLOW", .value = KEY_YELLOW}, - {.name = "KEY_YEN", .value = KEY_YEN}, - {.name = "KEY_Z", .value = KEY_Z}, - {.name = "KEY_ZENKAKUHANKAKU", .value = KEY_ZENKAKUHANKAKU}, - {.name = "KEY_ZOOMIN", .value = KEY_ZOOMIN}, - {.name = "KEY_ZOOMOUT", .value = KEY_ZOOMOUT}, - {.name = "KEY_ZOOMRESET", .value = KEY_ZOOMRESET}, - {.name = "LED_CAPSL", .value = LED_CAPSL}, - {.name = "LED_CHARGING", .value = LED_CHARGING}, - {.name = "LED_COMPOSE", .value = LED_COMPOSE}, - {.name = "LED_KANA", .value = LED_KANA}, - {.name = "LED_MAIL", .value = LED_MAIL}, - {.name = "LED_MAX", .value = LED_MAX}, - {.name = "LED_MISC", .value = LED_MISC}, - {.name = "LED_MUTE", .value = LED_MUTE}, - {.name = "LED_NUML", .value = LED_NUML}, - {.name = "LED_SCROLLL", .value = LED_SCROLLL}, - {.name = "LED_SLEEP", .value = LED_SLEEP}, - {.name = "LED_SUSPEND", .value = LED_SUSPEND}, - {.name = "MSC_GESTURE", .value = MSC_GESTURE}, - {.name = "MSC_MAX", .value = MSC_MAX}, - {.name = "MSC_PULSELED", .value = MSC_PULSELED}, - {.name = "MSC_RAW", .value = MSC_RAW}, - {.name = "MSC_SCAN", .value = MSC_SCAN}, - {.name = "MSC_SERIAL", .value = MSC_SERIAL}, - {.name = "MSC_TIMESTAMP", .value = MSC_TIMESTAMP}, - {.name = "REL_DIAL", .value = REL_DIAL}, - {.name = "REL_HWHEEL", .value = REL_HWHEEL}, - {.name = "REL_HWHEEL_HI_RES", .value = REL_HWHEEL_HI_RES}, - {.name = "REL_MAX", .value = REL_MAX}, - {.name = "REL_MISC", .value = REL_MISC}, - {.name = "REL_RESERVED", .value = REL_RESERVED}, - {.name = "REL_RX", .value = REL_RX}, - {.name = "REL_RY", .value = REL_RY}, - {.name = "REL_RZ", .value = REL_RZ}, - {.name = "REL_WHEEL", .value = REL_WHEEL}, - {.name = "REL_WHEEL_HI_RES", .value = REL_WHEEL_HI_RES}, - {.name = "REL_X", .value = REL_X}, - {.name = "REL_Y", .value = REL_Y}, - {.name = "REL_Z", .value = REL_Z}, - {.name = "REP_DELAY", .value = REP_DELAY}, - {.name = "REP_MAX", .value = REP_MAX}, - {.name = "REP_PERIOD", .value = REP_PERIOD}, - {.name = "SND_BELL", .value = SND_BELL}, - {.name = "SND_CLICK", .value = SND_CLICK}, - {.name = "SND_MAX", .value = SND_MAX}, - {.name = "SND_TONE", .value = SND_TONE}, - {.name = "SW_CAMERA_LENS_COVER", .value = SW_CAMERA_LENS_COVER}, - {.name = "SW_DOCK", .value = SW_DOCK}, - {.name = "SW_FRONT_PROXIMITY", .value = SW_FRONT_PROXIMITY}, - {.name = "SW_HEADPHONE_INSERT", .value = SW_HEADPHONE_INSERT}, - {.name = "SW_JACK_PHYSICAL_INSERT", .value = SW_JACK_PHYSICAL_INSERT}, - {.name = "SW_KEYPAD_SLIDE", .value = SW_KEYPAD_SLIDE}, - {.name = "SW_LID", .value = SW_LID}, - {.name = "SW_LINEIN_INSERT", .value = SW_LINEIN_INSERT}, - {.name = "SW_LINEOUT_INSERT", .value = SW_LINEOUT_INSERT}, - {.name = "SW_MACHINE_COVER", .value = SW_MACHINE_COVER}, - {.name = "SW_MAX", .value = SW_MAX}, - {.name = "SW_MICROPHONE_INSERT", .value = SW_MICROPHONE_INSERT}, - {.name = "SW_MUTE_DEVICE", .value = SW_MUTE_DEVICE}, - {.name = "SW_PEN_INSERTED", .value = SW_PEN_INSERTED}, - {.name = "SW_RFKILL_ALL", .value = SW_RFKILL_ALL}, - {.name = "SW_ROTATE_LOCK", .value = SW_ROTATE_LOCK}, - {.name = "SW_TABLET_MODE", .value = SW_TABLET_MODE}, - {.name = "SW_VIDEOOUT_INSERT", .value = SW_VIDEOOUT_INSERT}, - {.name = "SYN_CONFIG", .value = SYN_CONFIG}, - {.name = "SYN_DROPPED", .value = SYN_DROPPED}, - {.name = "SYN_MAX", .value = SYN_MAX}, - {.name = "SYN_MT_REPORT", .value = SYN_MT_REPORT}, - {.name = "SYN_REPORT", .value = SYN_REPORT}, -}; - -static const struct name_entry prop_names[] = { - {.name = "INPUT_PROP_ACCELEROMETER", .value = INPUT_PROP_ACCELEROMETER}, - {.name = "INPUT_PROP_BUTTONPAD", .value = INPUT_PROP_BUTTONPAD}, - {.name = "INPUT_PROP_DIRECT", .value = INPUT_PROP_DIRECT}, - {.name = "INPUT_PROP_MAX", .value = INPUT_PROP_MAX}, - {.name = "INPUT_PROP_POINTER", .value = INPUT_PROP_POINTER}, - {.name = "INPUT_PROP_POINTING_STICK", .value = INPUT_PROP_POINTING_STICK}, - {.name = "INPUT_PROP_SEMI_MT", .value = INPUT_PROP_SEMI_MT}, - {.name = "INPUT_PROP_TOPBUTTONPAD", .value = INPUT_PROP_TOPBUTTONPAD}, -}; - -#endif /* EVENT_NAMES_H */ From 3ef0a4737f726730ff19f4bf8d4525d03d8509da Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 21:44:18 +0200 Subject: [PATCH 009/215] #1394 gitignore event-names.h --- priv/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/priv/.gitignore b/priv/.gitignore index c591fdeb45..131b793244 100644 --- a/priv/.gitignore +++ b/priv/.gitignore @@ -1,2 +1,3 @@ /build -.cxx \ No newline at end of file +.cxx +/src/main/cpp/libevdev/event-names.h From 913cb62461e1b26a69f68b7acbc4957ae40e55c0 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 21:56:06 +0200 Subject: [PATCH 010/215] #1394 delete unnecessary ADB native files and other tweaks --- priv/src/main/cpp/CMakeLists.txt | 3 +- priv/src/main/cpp/helper.cpp | 73 ------------------- priv/src/main/cpp/privstarter.cpp | 9 --- priv/src/main/cpp/starter.cpp | 4 +- .../keymapper/priv/adb/AdbPairingService.kt | 2 +- .../service/PrivServiceSetupController.kt | 2 +- 6 files changed, 5 insertions(+), 88 deletions(-) delete mode 100644 priv/src/main/cpp/helper.cpp delete mode 100644 priv/src/main/cpp/privstarter.cpp diff --git a/priv/src/main/cpp/CMakeLists.txt b/priv/src/main/cpp/CMakeLists.txt index 4a9b59944e..563da15e89 100644 --- a/priv/src/main/cpp/CMakeLists.txt +++ b/priv/src/main/cpp/CMakeLists.txt @@ -9,7 +9,7 @@ cmake_minimum_required(VERSION 3.22.1) # 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") +project("priv") # FROM SHIZUKU set(CMAKE_CXX_STANDARD 17) @@ -74,7 +74,6 @@ endif () # 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 privservice.cpp libevdev/libevdev.c libevdev/libevdev-names.c diff --git a/priv/src/main/cpp/helper.cpp b/priv/src/main/cpp/helper.cpp deleted file mode 100644 index 1001f8316d..0000000000 --- a/priv/src/main/cpp/helper.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#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/priv/src/main/cpp/privstarter.cpp b/priv/src/main/cpp/privstarter.cpp deleted file mode 100644 index 303a24597b..0000000000 --- a/priv/src/main/cpp/privstarter.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#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/priv/src/main/cpp/starter.cpp b/priv/src/main/cpp/starter.cpp index 91689b8a29..a10d1d9edd 100644 --- a/priv/src/main/cpp/starter.cpp +++ b/priv/src/main/cpp/starter.cpp @@ -32,8 +32,8 @@ // TODO take package name as argument #define PACKAGE_NAME "io.github.sds100.keymapper.debug" -#define SERVER_NAME "shizuku_server" -#define SERVER_CLASS_PATH "io.github.sds100.keymapper.nativelib.EvdevService" +#define SERVER_NAME "keymapper_priv" +#define SERVER_CLASS_PATH "io.github.sds100.keymapper.priv.service.PrivService" #if defined(__arm__) #define ABI "armeabi-v7a" diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt index ac9abc71d0..1c6735ea55 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt @@ -138,7 +138,7 @@ internal class AdbPairingService : Service() { val key = try { AdbKey( PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(this@AdbPairingService)), - "shizuku", + "keymapper", ) } catch (e: Throwable) { e.printStackTrace() diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt index d8983fc673..2fa4d82f7a 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt @@ -122,7 +122,7 @@ class PrivServiceSetupControllerImpl @Inject constructor( val key = try { AdbKey( PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), - "shizuku", + "keymapper", ) } catch (e: Throwable) { e.printStackTrace() From 0e6a96e95a4df70eb44a013e804ecc65deb49c1e Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Jul 2025 22:41:30 +0200 Subject: [PATCH 011/215] #1394 detecting ADB connection port works, and starting the priv service --- .../BaseAccessibilityServiceController.kt | 13 +- .../sds100/keymapper/priv/adb/AdbMdns.kt | 140 ++++---- .../keymapper/priv/adb/AdbPairingService.kt | 326 ------------------ .../keymapper/priv/adb/AdbServiceType.kt | 5 + .../keymapper/priv/service/PrivService.kt | 2 +- .../service/PrivServiceSetupController.kt | 35 +- 6 files changed, 113 insertions(+), 408 deletions(-) delete mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbServiceType.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 6397c0274c..9faa7dd2e4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -396,6 +396,13 @@ abstract class BaseAccessibilityServiceController( event: MyKeyEvent, detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ): Boolean { + //TODO remove + if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && event.action == KeyEvent.ACTION_DOWN) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + privServiceSetupController.startWithAdb() + } + } + val detailedLogInfo = event.toString() if (recordingTrigger) { @@ -554,7 +561,11 @@ abstract class BaseAccessibilityServiceController( if (pairingCode != null && port != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - privServiceSetupController.pairWirelessAdb(port, pairingCode) + service.lifecycleScope.launch { + privServiceSetupController.pairWirelessAdb(port, pairingCode) + delay(1000) + privServiceSetupController.startWithAdb() + } } } } diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt index 76636c7ec8..c85fb45db5 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt @@ -4,56 +4,97 @@ 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 kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber import java.io.IOException import java.net.InetSocketAddress import java.net.NetworkInterface import java.net.ServerSocket +/** + * This uses mDNS to scan for the ADB pairing and connection ports. + */ @RequiresApi(Build.VERSION_CODES.R) internal class AdbMdns( - context: Context, private val serviceType: String, - private val port: MutableLiveData + ctx: Context, + private val serviceType: AdbServiceType, ) { 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) + private val nsdManager: NsdManager = ctx.getSystemService(NsdManager::class.java) + + private val _port: MutableStateFlow = MutableStateFlow(null) + val port: StateFlow = _port.asStateFlow() + + private val discoveryListener: NsdManager.DiscoveryListener = + object : NsdManager.DiscoveryListener { + override fun onDiscoveryStarted(serviceType: String) { + Timber.d("onDiscoveryStarted: $serviceType") + + registered = true + } + + override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { + Timber.d("onStartDiscoveryFailed: $serviceType, $errorCode") + } + + override fun onDiscoveryStopped(serviceType: String) { + Timber.d("onDiscoveryStopped: $serviceType") + + registered = false + } + + override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { + Timber.d("onStopDiscoveryFailed: $serviceType, $errorCode") + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo) { + Timber.d("onServiceFound: ${serviceInfo.serviceName}") + + nsdManager.resolveService(serviceInfo, ResolveListener(this@AdbMdns)) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo) { + Timber.d("onServiceLost: ${serviceInfo.serviceName}") + + if (serviceInfo.serviceName == serviceName) { + _port.update { null } + } + } + } fun start() { - if (running) return + if (running) { + return + } + running = true + if (!registered) { - nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener) + nsdManager.discoverServices( + serviceType.id, + NsdManager.PROTOCOL_DNS_SD, + discoveryListener + ) } } fun stop() { - if (!running) return - running = false - if (registered) { - nsdManager.stopServiceDiscovery(listener) + if (!running) { + return } - } - private fun onDiscoveryStart() { - registered = true - } - - private fun onDiscoveryStop() { - registered = false - } - - private fun onServiceFound(info: NsdServiceInfo) { - nsdManager.resolveService(info, ResolveListener(this)) - } + running = false - private fun onServiceLost(info: NsdServiceInfo) { - if (info.serviceName == serviceName) port.postValue(-1) + if (registered) { + nsdManager.stopServiceDiscovery(discoveryListener) + } } private fun onServiceResolved(resolvedService: NsdServiceInfo) { @@ -67,7 +108,7 @@ internal class AdbMdns( && isPortAvailable(resolvedService.port) ) { serviceName = resolvedService.serviceName - port.postValue(resolvedService.port) + _port.update { resolvedService.port } } } @@ -80,56 +121,11 @@ internal class AdbMdns( 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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt deleted file mode 100644 index 1c6735ea55..0000000000 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingService.kt +++ /dev/null @@ -1,326 +0,0 @@ -package io.github.sds100.keymapper.priv.adb - -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.annotation.RequiresApi -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import io.github.sds100.keymapper.priv.R -import io.github.sds100.keymapper.priv.ktx.TAG -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import rikka.core.ktx.unsafeLazy -import java.net.ConnectException - -@RequiresApi(Build.VERSION_CODES.R) -internal 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 { - - 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)), - "keymapper", - ) - } 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(me.zhanghai.android.appiconloader.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(me.zhanghai.android.appiconloader.R.drawable.ic_instant_app_badge) - .setContentTitle("Searching") - .addAction(stopNotificationAction) - .build() - } - - private fun createInputNotification(port: Int): Notification { - return Notification.Builder(this, notificationChannel) - .setSmallIcon(me.zhanghai.android.appiconloader.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(me.zhanghai.android.appiconloader.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/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbServiceType.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbServiceType.kt new file mode 100644 index 0000000000..8f5fad9faa --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbServiceType.kt @@ -0,0 +1,5 @@ +package io.github.sds100.keymapper.priv.adb + +enum class AdbServiceType(val id: String) { + TLS_CONNECT("_adb-tls-connect._tcp"), TLS_PAIR("_adb-tls-pairing._tcp") +} \ No newline at end of file diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt index 9a4f7306bd..9f37730022 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt @@ -28,7 +28,7 @@ class PrivService : IPrivService.Stub() { init { @SuppressLint("UnsafeDynamicallyLoadedCode") // TODO can we change "shizuku.library.path" property? - System.load("${System.getProperty("shizuku.library.path")}/libevdev.so") + System.load("${System.getProperty("shizuku.library.path")}/libpriv.so") stringFromJNI() } diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt index 2fa4d82f7a..6dff4bf847 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt @@ -9,12 +9,16 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.priv.adb.AdbClient import io.github.sds100.keymapper.priv.adb.AdbKey import io.github.sds100.keymapper.priv.adb.AdbKeyException +import io.github.sds100.keymapper.priv.adb.AdbMdns import io.github.sds100.keymapper.priv.adb.AdbPairingClient +import io.github.sds100.keymapper.priv.adb.AdbServiceType import io.github.sds100.keymapper.priv.adb.PreferenceAdbKeyStore import io.github.sds100.keymapper.priv.starter.Starter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -31,18 +35,33 @@ class PrivServiceSetupControllerImpl @Inject constructor( private val sb = StringBuilder() @RequiresApi(Build.VERSION_CODES.R) - override fun startWithAdb(host: String, port: Int) { - writeStarterFiles() - - sb.append("Starting with wireless adb...").append('\n').append('\n') - postResult() + private var adbConnectMdns: AdbMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) + // TODO clean up + // TODO have lock so can only launch one start job at a time + @RequiresApi(Build.VERSION_CODES.R) + override fun startWithAdb() { coroutineScope.launch(Dispatchers.IO) { + adbConnectMdns.start() + + val host = "127.0.0.1" + val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } + + if (port == null) { + return@launch + } + + writeStarterFiles() + + sb.append("Starting with wireless adb...").append('\n').append('\n') + postResult() + val key = try { - AdbKey( + val adbKey = AdbKey( PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), - "shizuku", + "keymapper", ) + adbKey } catch (e: Throwable) { e.printStackTrace() sb.append('\n').append(Log.getStackTraceString(e)) @@ -175,5 +194,5 @@ interface PrivServiceSetupController { fun pairWirelessAdb(port: Int, code: Int) @RequiresApi(Build.VERSION_CODES.R) - fun startWithAdb(host: String, port: Int) + fun startWithAdb() } \ No newline at end of file From 96c904ebabcf100d599f2274c48af1e2e3af181d Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 8 Jul 2025 23:25:22 +0200 Subject: [PATCH 012/215] #1394 bunch of changes --- priv/build.gradle.kts | 2 +- .../keymapper/priv/service/PrivService.kt | 19 +++++++------ .../service/PrivServiceSetupController.kt | 28 ++++++++++++++++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/priv/build.gradle.kts b/priv/build.gradle.kts index 0f18f173b3..7dd2babd92 100644 --- a/priv/build.gradle.kts +++ b/priv/build.gradle.kts @@ -65,7 +65,7 @@ android { } dependencies { - implementation(project(":systemstubs")) + compileOnly(project(":systemstubs")) implementation(libs.jakewharton.timber) diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt index 9f37730022..51d6efcfe9 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt @@ -3,16 +3,15 @@ package io.github.sds100.keymapper.priv.service import android.annotation.SuppressLint import android.ddm.DdmHandleAppName import android.system.Os +import android.util.Log import io.github.sds100.keymapper.priv.IPrivService -import timber.log.Timber import kotlin.system.exitProcess +@SuppressLint("LogNotTimber") class PrivService : IPrivService.Stub() { - /** - * A native method that is implemented by the 'nativelib' native library, - * which is packaged with this application. - */ + // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. + external fun stringFromJNI(): String companion object { @@ -20,7 +19,7 @@ class PrivService : IPrivService.Stub() { @JvmStatic fun main(args: Array) { - DdmHandleAppName.setAppName("keymapper_evdev", 0) + DdmHandleAppName.setAppName("keymapper_priv", 0) PrivService() } } @@ -29,16 +28,18 @@ class PrivService : IPrivService.Stub() { @SuppressLint("UnsafeDynamicallyLoadedCode") // TODO can we change "shizuku.library.path" property? System.load("${System.getProperty("shizuku.library.path")}/libpriv.so") - stringFromJNI() + Log.d(TAG, "PrivService started") } + // TODO ungrab all evdev devices + // TODO ungrab all evdev devices if no key mapper app is bound to the service override fun destroy() { - Timber.d("Destroy PrivService") + Log.d(TAG, "PrivService destroyed") exitProcess(0) } override fun sendEvent(): String? { - Timber.e("UID = ${Os.getuid()}") + Log.d(TAG, "UID = ${Os.getuid()}") return stringFromJNI() } } \ No newline at end of file diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt index 6dff4bf847..990b1bc9eb 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt @@ -1,11 +1,16 @@ package io.github.sds100.keymapper.priv.service +import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.os.Build +import android.os.IBinder import android.preference.PreferenceManager import android.util.Log import androidx.annotation.RequiresApi import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.priv.IPrivService import io.github.sds100.keymapper.priv.adb.AdbClient import io.github.sds100.keymapper.priv.adb.AdbKey import io.github.sds100.keymapper.priv.adb.AdbKeyException @@ -35,7 +40,21 @@ class PrivServiceSetupControllerImpl @Inject constructor( private val sb = StringBuilder() @RequiresApi(Build.VERSION_CODES.R) - private var adbConnectMdns: AdbMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) + private val adbConnectMdns: AdbMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected( + name: ComponentName?, + service: IBinder? + ) { + Timber.d("priv service connected") + IPrivService.Stub.asInterface(service).sendEvent() + } + + override fun onServiceDisconnected(name: ComponentName?) { + Timber.d("priv service disconnected") + } + } // TODO clean up // TODO have lock so can only launch one start job at a time @@ -111,6 +130,13 @@ class PrivServiceSetupControllerImpl @Inject constructor( postResult(it) } } + + adbConnectMdns.stop() + + val serviceIntent = Intent(ctx, PrivService::class.java) + + Timber.d("BINDING TO SERVICE") + ctx.bindService(serviceIntent, serviceConnection, 0) } } From 1b3abbe65b5b1e61d24129973b3ae3d9fabd94c8 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 10 Jul 2025 13:01:02 -0400 Subject: [PATCH 013/215] #1394 add PRO Mode settings landing page --- .../keymapper/base/BaseViewModelHiltModule.kt | 6 + .../keymapper/base/compose/ComposeColors.kt | 10 +- .../base/compose/ComposeCustomColors.kt | 29 ++ .../keymapper/base/promode/ProModeFragment.kt | 55 +++ .../keymapper/base/promode/ProModeScreen.kt | 344 ++++++++++++++++++ .../base/promode/ProModeSetupUseCase.kt | 25 ++ .../base/promode/ProModeViewModel.kt | 71 ++++ .../base/settings/MainSettingsFragment.kt | 15 + .../base/settings/SettingsViewModel.kt | 15 +- .../base/utils/navigation/NavDestination.kt | 11 +- .../utils/navigation/NavigationProvider.kt | 2 + base/src/main/res/navigation/nav_base_app.xml | 13 + base/src/main/res/values/strings.xml | 26 ++ .../io/github/sds100/keymapper/data/Keys.kt | 3 + 14 files changed, 620 insertions(+), 5 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupUseCase.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt index 526b59f6e7..1d2befbf9c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt @@ -27,6 +27,8 @@ import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.logging.DisplayLogUseCase import io.github.sds100.keymapper.base.logging.DisplayLogUseCaseImpl +import io.github.sds100.keymapper.base.promode.ProModeSetupUseCase +import io.github.sds100.keymapper.base.promode.ProModeSetupUseCaseImpl import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCase import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCaseImpl import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase @@ -110,4 +112,8 @@ abstract class BaseViewModelHiltModule { @Binds @ViewModelScoped abstract fun bindCreateConstraintUseCase(impl: CreateConstraintUseCaseImpl): CreateConstraintUseCase + + @Binds + @ViewModelScoped + abstract fun bindProModeSetupUseCase(impl: ProModeSetupUseCaseImpl): ProModeSetupUseCase } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt index 7746f70135..a8c1ae2390 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/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) +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt index 49d7638df8..81eb41e2c9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt @@ -1,6 +1,10 @@ package io.github.sds100.keymapper.base.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,10 @@ 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 +47,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/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt new file mode 100644 index 0000000000..877269d203 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt @@ -0,0 +1,55 @@ +package io.github.sds100.keymapper.base.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.hilt.navigation.compose.hiltViewModel +import androidx.navigation.findNavController +import dagger.hilt.android.AndroidEntryPoint +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.databinding.FragmentComposeBinding + +@AndroidEntryPoint +class ProModeFragment : Fragment() { + + 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 = hiltViewModel(), + onNavigateBack = findNavController()::navigateUp, + ) + } + } + } + return this.root + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt new file mode 100644 index 0000000000..381110bff4 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -0,0 +1,344 @@ +package io.github.sds100.keymapper.base.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.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette +import io.github.sds100.keymapper.base.utils.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/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupUseCase.kt new file mode 100644 index 0000000000..d5e0368f0e --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupUseCase.kt @@ -0,0 +1,25 @@ +package io.github.sds100.keymapper.base.promode + +import dagger.hilt.android.scopes.ViewModelScoped +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 +import javax.inject.Inject + +@ViewModelScoped +class ProModeSetupUseCaseImpl @Inject constructor( + 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/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt new file mode 100644 index 0000000000..0cf890cdc3 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -0,0 +1,71 @@ +package io.github.sds100.keymapper.base.promode + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.ui.DialogProvider +import io.github.sds100.keymapper.base.utils.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 +import javax.inject.Inject + +@HiltViewModel +class ProModeViewModel @Inject constructor( + private val useCase: ProModeSetupUseCase, + resourceProvider: ResourceProvider, + dialogProvider: DialogProvider, + navigationProvider: NavigationProvider +) : ViewModel(), + ResourceProvider by resourceProvider, + DialogProvider by dialogProvider, + NavigationProvider by navigationProvider { + + companion object { + private const val WARNING_COUNT_DOWN_SECONDS = 5 + } + + @OptIn(ExperimentalCoroutinesApi::class) + val proModeWarningState: StateFlow = + useCase.isWarningUnderstood.flatMapLatest { isUnderstood -> + if (isUnderstood) { + flowOf(ProModeWarningState.Understood) + } else { + flow { + repeat(WARNING_COUNT_DOWN_SECONDS) { + emit(ProModeWarningState.CountingDown(WARNING_COUNT_DOWN_SECONDS - it)) + delay(1000L) + } + + emit(ProModeWarningState.Idle) + } + } + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, + ProModeWarningState.CountingDown( + WARNING_COUNT_DOWN_SECONDS, + ), + ) + + fun onWarningButtonClick() { + useCase.onUnderstoodWarning() + } +} + +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/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt index 072e8cedb7..7d3e17b36f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt @@ -124,6 +124,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 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 34d20d304a..c1da1588c1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -6,6 +6,9 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.utils.getFullMessage +import io.github.sds100.keymapper.base.utils.navigation.NavDestination +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogModel import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.DialogResponse @@ -29,11 +32,13 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( private val useCase: ConfigSettingsUseCase, private val resourceProvider: ResourceProvider, - dialogProvider: DialogProvider, val sharedPrefsDataStoreWrapper: SharedPrefsDataStoreWrapper, + dialogProvider: DialogProvider, + navigationProvider: NavigationProvider ) : ViewModel(), DialogProvider by dialogProvider, - ResourceProvider by resourceProvider { + ResourceProvider by resourceProvider, + NavigationProvider by navigationProvider { val automaticBackupLocation = useCase.automaticBackupLocation @@ -209,4 +214,10 @@ class SettingsViewModel @Inject constructor( } } } + + fun onProModeClick() { + viewModelScope.launch { + navigate("pro_mode_settings", NavDestination.ProMode) + } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt index 8f6c870c49..92237ab8e1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt @@ -35,6 +35,7 @@ abstract class NavDestination(val isCompose: Boolean = false) { const val ID_CONFIG_KEY_MAP = "config_key_map" const val ID_SHIZUKU_SETTINGS = "shizuku_settings" const val ID_INTERACT_UI_ELEMENT_ACTION = "interact_ui_element_action" + const val ID_PRO_MODE = "pro_mode" } @Serializable @@ -69,7 +70,8 @@ abstract class NavDestination(val isCompose: Boolean = false) { } @Serializable - data class PickCoordinate(val result: PickCoordinateResult? = null) : NavDestination() { + data class PickCoordinate(val result: PickCoordinateResult? = null) : + NavDestination() { override val id: String = ID_PICK_COORDINATE } @@ -86,7 +88,8 @@ abstract class NavDestination(val isCompose: Boolean = false) { } @Serializable - data class ConfigIntent(val result: ConfigIntentResult? = null) : NavDestination() { + data class ConfigIntent(val result: ConfigIntentResult? = null) : + NavDestination() { override val id: String = ID_CONFIG_INTENT } @@ -150,4 +153,8 @@ abstract class NavDestination(val isCompose: Boolean = false) { NavDestination(isCompose = true) { override val id: String = ID_INTERACT_UI_ELEMENT_ACTION } + + data object ProMode : NavDestination(isCompose = false) { + override val id: String = ID_PRO_MODE + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt index 0cea5a666a..4d2e4e7a53 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt @@ -355,6 +355,8 @@ private fun getDirection(destination: NavDestination<*>, requestKey: String): Na NavDestination.ShizukuSettings -> NavBaseAppDirections.toShizukuSettingsFragment() + NavDestination.ProMode -> NavBaseAppDirections.toProModeFragment() + else -> throw IllegalArgumentException("Can not find a direction for this destination: $destination") } } diff --git a/base/src/main/res/navigation/nav_base_app.xml b/base/src/main/res/navigation/nav_base_app.xml index e3f43da0f9..936a5d993e 100644 --- a/base/src/main/res/navigation/nav_base_app.xml +++ b/base/src/main/res/navigation/nav_base_app.xml @@ -276,4 +276,17 @@ android:name="io.github.sds100.keymapper.base.settings.ShizukuSettingsFragment" android:label="ShizukuSettingsFragment" /> + + + + \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 032e8d07ce..59101cd75c 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -720,6 +720,9 @@ Logging This may add latency to your key maps so only turn this on if you are trying to debug the app or have been asked to by the developer. + + PRO mode + Advanced detection of key events and more. @@ -1574,4 +1577,27 @@ +%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/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 681c7eaba9..2c719d34cd 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -109,4 +109,7 @@ object Keys { val skipTapTargetTutorial = booleanPreferencesKey("key_skip_tap_target_tutorial") + + val isProModeWarningUnderstood = + booleanPreferencesKey("key_is_pro_mode_warning_understood") } From dfd9c67c12ebcf5c9b03716eaa6cc79f5f624be5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 15 May 2025 18:59:00 +0200 Subject: [PATCH 014/215] #1394 add libsu code from old branch --- CREDITS.md | 2 + .../sds100/keymapper/base/BaseMainActivity.kt | 5 + .../keymaps/detection/DetectKeyMapsUseCase.kt | 2 +- .../DetectScreenOffKeyEventsController.kt | 99 +------------------ .../base/settings/ConfigSettingsUseCase.kt | 15 ++- .../base/settings/MainSettingsFragment.kt | 10 +- .../base/settings/SettingsViewModel.kt | 4 + .../base/system/navigation/OpenMenuHelper.kt | 2 +- .../ManageNotificationsUseCase.kt | 2 +- base/src/main/res/values/strings.xml | 5 +- .../base/actions/PerformActionsUseCaseTest.kt | 2 +- .../io/github/sds100/keymapper/data/Keys.kt | 3 + gradle/libs.versions.toml | 3 + system/build.gradle.kts | 3 +- .../keymapper/system/SystemHiltModule.kt | 3 +- .../permissions/AndroidPermissionAdapter.kt | 4 +- .../sds100/keymapper/system/root/SuAdapter.kt | 59 ++++------- .../keymapper/system/shell/ShellAdapter.kt | 2 - .../system/{Shell.kt => shell/SimpleShell.kt} | 20 +--- 19 files changed, 67 insertions(+), 178 deletions(-) rename system/src/main/java/io/github/sds100/keymapper/system/{Shell.kt => shell/SimpleShell.kt} (58%) diff --git a/CREDITS.md b/CREDITS.md index 8d7b22f638..874d1a432c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -13,6 +13,8 @@ Many thanks to... - @[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/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 9ab375dec4..4ae4144a7d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -38,6 +38,7 @@ import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter +import io.github.sds100.keymapper.system.root.SuAdapterImpl import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -86,6 +87,9 @@ abstract class BaseMainActivity : AppCompatActivity() { @Inject lateinit var privServiceSetup: PrivServiceSetupController + @Inject + lateinit var suAdapter: SuAdapterImpl + private lateinit var requestPermissionDelegate: RequestPermissionDelegate private val currentNightMode: Int @@ -197,6 +201,7 @@ abstract class BaseMainActivity : AppCompatActivity() { // the activities have not necessarily resumed at that point. permissionAdapter.onPermissionsChanged() serviceAdapter.invalidateState() + suAdapter.invalidateIsRooted() } override fun onDestroy() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt index fb3fc72e1f..deddfd064d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt @@ -151,7 +151,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( override val detectScreenOffTriggers: Flow = combine( allKeyMapList, - suAdapter.isGranted, + suAdapter.isRooted, ) { keyMapList, isRootPermissionGranted -> keyMapList.any { it.keyMap.trigger.screenOffTrigger } && isRootPermissionGranted }.flowOn(Dispatchers.Default) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt index bd56ca6ef0..3b79f41c78 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt @@ -1,22 +1,12 @@ package io.github.sds100.keymapper.base.keymaps.detection -import android.view.InputDevice -import android.view.KeyEvent -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.MyKeyEvent import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import timber.log.Timber +// TODO delete class DetectScreenOffKeyEventsController( private val suAdapter: SuAdapter, private val devicesAdapter: DevicesAdapter, @@ -34,92 +24,7 @@ class DetectScreenOffKeyEventsController( * @return whether it successfully started listening. */ fun startListening(scope: CoroutineScope): Boolean { - try { - job = scope.launch(Dispatchers.IO) { - val devicesInputStream = - suAdapter.getCommandOutput("getevent -i").valueOrNull() ?: return@launch - - val getEventDevices: String = devicesInputStream.bufferedReader().readText() - devicesInputStream.close() - - val deviceLocationToDeviceMap = mutableMapOf() - - val inputDevices = - devicesAdapter.connectedInputDevices.first { it is State.Data } as State.Data - - inputDevices.data.forEach { device -> - val deviceLocation = - getDeviceLocation(getEventDevices, device.name) ?: return@forEach - - deviceLocationToDeviceMap[deviceLocation] = device - } - - val deviceLocationRegex = Regex(REGEX_GET_DEVICE_LOCATION) - val actionRegex = Regex(REGEX_KEY_EVENT_ACTION) - - // use -q option to not initially output the list of devices - val inputStream = - suAdapter.getCommandOutput("getevent -lq").valueOrNull() ?: return@launch - - var line: String? - - while (inputStream.bufferedReader().readLine() - .also { line = it } != null && - isActive - ) { - line ?: continue - - InputEventUtils.GET_EVENT_LABEL_TO_KEYCODE.forEach { (label, keyCode) -> - if (line!!.contains(label)) { - val deviceLocation = - deviceLocationRegex.find(line!!)?.value ?: return@forEach - - val device = deviceLocationToDeviceMap[deviceLocation] ?: return@forEach - - val actionString = actionRegex.find(line!!)?.value ?: return@forEach - - when (actionString) { - "UP" -> { - onKeyEvent.invoke( - MyKeyEvent( - keyCode = keyCode, - action = KeyEvent.ACTION_UP, - device = device, - scanCode = 0, - metaState = 0, - repeatCount = 0, - source = InputDevice.SOURCE_UNKNOWN, - ), - ) - } - - "DOWN" -> { - onKeyEvent.invoke( - MyKeyEvent( - keyCode = keyCode, - action = KeyEvent.ACTION_DOWN, - device = device, - scanCode = 0, - metaState = 0, - repeatCount = 0, - source = InputDevice.SOURCE_UNKNOWN, - ), - ) - } - } - } - } - } - - inputStream.close() - } - } catch (e: Exception) { - Timber.e(e) - job?.cancel() - return false - } - - return true + return false } fun stopListening() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index bc67b9be08..aac013a856 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -46,7 +46,7 @@ class ConfigSettingsUseCaseImpl @Inject constructor( ) } - override val isRootGranted: Flow = suAdapter.isGranted + override val isRootGranted: Flow = suAdapter.isRooted override val isWriteSecureSettingsGranted: Flow = channelFlow { send(permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)) @@ -162,6 +162,10 @@ class ConfigSettingsUseCaseImpl @Inject constructor( 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 @@ -184,14 +188,15 @@ 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 @@ -204,17 +209,17 @@ interface ConfigSettingsUseCase { 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/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt index 7d3e17b36f..05a86246bd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt @@ -502,14 +502,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/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index c1da1588c1..0dbbf59b92 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -215,6 +215,10 @@ class SettingsViewModel @Inject constructor( } } + fun onRequestRootClick() { + useCase.requestRootPermission() + } + fun onProModeClick() { viewModelScope.launch { navigate("pro_mode_settings", NavDestination.ProMode) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt index 78120f505d..3d0d14e61c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt @@ -43,7 +43,7 @@ class OpenMenuHelper( return success() } - suAdapter.isGranted.firstBlocking() -> + suAdapter.isRooted.firstBlocking() -> return suAdapter.execute("input keyevent ${KeyEvent.KEYCODE_MENU}\n") else -> { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt index 3d699686ef..8178027873 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt @@ -23,7 +23,7 @@ class ManageNotificationsUseCaseImpl @Inject constructor( override val showImePickerNotification: Flow = combine( - suAdapter.isGranted, + suAdapter.isRooted, preferences.get(Keys.showImePickerNotification), ) { hasRootPermission, show -> when { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 59101cd75c..52b07396e5 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -634,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 diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index 5dee3c53b7..19d4068092 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -55,7 +55,7 @@ class PerformActionsUseCaseTest { inputMethodAdapter = mock(), fileAdapter = mock(), suAdapter = mock { - on { isGranted }.then { MutableStateFlow(false) } + on { isRooted }.then { MutableStateFlow(false) } }, shell = mock(), intentAdapter = mock(), diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 2c719d34cd..f4f760ad78 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -7,7 +7,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") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c4942c902..423d83e61b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,6 +69,8 @@ room-testing-legacy = "1.1.1" espresso-core = "3.6.1" ui-tooling = "1.8.1" # android.arch.persistence.room:testing +libsu-core = "6.0.0" + [libraries] # Kotlin androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" } @@ -169,6 +171,7 @@ jakewharton-timber = { group = "com.jakewharton.timber", name = "timber", versio kotson = { group = "com.github.salomonbrys.kotson", name = "kotson", version.ref = "kotson" } lsposed-hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" } net-lingala-zip4j = { group = "net.lingala.zip4j", name = "zip4j", version.ref = "lingala-zip4j" } +github-topjohnwu-libsu = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu-core" } # Gradle Plugins - Aliases for buildscript dependencies / plugins block # These are referenced in build.gradle.kts files' plugins blocks by their ID. diff --git a/system/build.gradle.kts b/system/build.gradle.kts index 6c133fa974..d38a35332e 100644 --- a/system/build.gradle.kts +++ b/system/build.gradle.kts @@ -41,10 +41,8 @@ dependencies { implementation(project(":data")) implementation(project(":systemstubs")) - // kotlin stuff implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) - implementation(libs.androidx.core.ktx) implementation(libs.jakewharton.timber) implementation(libs.dagger.hilt.android) @@ -58,4 +56,5 @@ dependencies { implementation(libs.rikka.shizuku.provider) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.preference.ktx) + implementation(libs.github.topjohnwu.libsu) } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt b/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt index 08d09239a9..39a66a2ee4 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt @@ -52,6 +52,7 @@ import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.root.SuAdapterImpl import io.github.sds100.keymapper.system.shell.ShellAdapter +import io.github.sds100.keymapper.system.shell.SimpleShell import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuAdapterImpl import io.github.sds100.keymapper.system.url.AndroidOpenUrlAdapter @@ -167,7 +168,7 @@ abstract class SystemHiltModule { @Singleton @Binds - abstract fun provideShellAdapter(impl: Shell): ShellAdapter + abstract fun provideShellAdapter(impl: SimpleShell): ShellAdapter @Singleton @Binds diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt index 61a23cea99..3b9343f3c5 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt @@ -103,7 +103,7 @@ class AndroidPermissionAdapter @Inject constructor( .stateIn(coroutineScope, SharingStarted.Eagerly, false) init { - suAdapter.isGranted + suAdapter.isRooted .drop(1) .onEach { onPermissionsChanged() } .launchIn(coroutineScope) @@ -281,7 +281,7 @@ class AndroidPermissionAdapter @Inject constructor( 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/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt index 081f1deafb..eef9730338 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt @@ -1,63 +1,49 @@ package io.github.sds100.keymapper.system.root +import com.topjohnwu.superuser.Shell import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.firstBlocking -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import io.github.sds100.keymapper.system.shell.ShellAdapter 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 java.io.IOException -import java.io.InputStream +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import javax.inject.Inject import javax.inject.Singleton @Singleton class SuAdapterImpl @Inject constructor( coroutineScope: CoroutineScope, - private val shell: ShellAdapter, - 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): KMResult<*> { - if (!isGranted.firstBlocking()) { + if (!isRooted.firstBlocking()) { return SystemError.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,27 +52,18 @@ class SuAdapterImpl @Inject constructor( } } - override fun getCommandOutput(command: String): KMResult { - if (!isGranted.firstBlocking()) { - return SystemError.PermissionDenied(Permission.ROOT) - } - - try { - val inputStream = shell.getShellCommandStdOut("su", "-c", command) - return Success(inputStream) - } catch (e: IOException) { - return KMError.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 */ fun requestPermission(): Boolean fun execute(command: String, block: Boolean = false): KMResult<*> - fun getCommandOutput(command: String): KMResult } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt index f22b677ec6..56c8b9f083 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt @@ -1,10 +1,8 @@ package io.github.sds100.keymapper.system.shell import io.github.sds100.keymapper.common.utils.KMResult -import java.io.InputStream interface ShellAdapter { fun run(vararg command: String, waitFor: Boolean = false): Boolean fun execute(command: String): KMResult<*> - fun getShellCommandStdOut(vararg command: String): InputStream } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/Shell.kt b/system/src/main/java/io/github/sds100/keymapper/system/shell/SimpleShell.kt similarity index 58% rename from system/src/main/java/io/github/sds100/keymapper/system/Shell.kt rename to system/src/main/java/io/github/sds100/keymapper/system/shell/SimpleShell.kt index 1b9d0b7535..14a280b4e1 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/Shell.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shell/SimpleShell.kt @@ -1,16 +1,14 @@ -package io.github.sds100.keymapper.system +package io.github.sds100.keymapper.system.shell import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success -import io.github.sds100.keymapper.system.shell.ShellAdapter import java.io.IOException -import java.io.InputStream import javax.inject.Inject import javax.inject.Singleton @Singleton -class Shell @Inject constructor() : ShellAdapter { +class SimpleShell @Inject constructor() : ShellAdapter { /** * @return whether the command was executed successfully */ @@ -26,18 +24,6 @@ class Shell @Inject constructor() : ShellAdapter { false } - /** - * Remember to close it after using it. - */ - @Throws(IOException::class) - override fun getShellCommandStdOut(vararg command: String): InputStream = Runtime.getRuntime().exec(command).inputStream - - /** - * Remember to close it after using it. - */ - @Throws(IOException::class) - fun getShellCommandStdErr(vararg command: String): InputStream = Runtime.getRuntime().exec(command).errorStream - override fun execute(command: String): KMResult<*> { try { Runtime.getRuntime().exec(command) @@ -47,4 +33,4 @@ class Shell @Inject constructor() : ShellAdapter { return KMError.Exception(e) } } -} +} \ No newline at end of file From 91bef905d3fbb4766addaf9f3a60a919c50a7569 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 11 Jul 2025 15:33:00 -0600 Subject: [PATCH 015/215] #1394 WIP: add the binder content provider (ShizukuProvider) --- priv/src/main/AndroidManifest.xml | 14 + .../priv/provider/PrivBinderProvider.kt | 274 ++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/provider/PrivBinderProvider.kt diff --git a/priv/src/main/AndroidManifest.xml b/priv/src/main/AndroidManifest.xml index a5918e68ab..2165f224e3 100644 --- a/priv/src/main/AndroidManifest.xml +++ b/priv/src/main/AndroidManifest.xml @@ -1,4 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/provider/PrivBinderProvider.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/provider/PrivBinderProvider.kt new file mode 100644 index 0000000000..7617668cfd --- /dev/null +++ b/priv/src/main/java/io/github/sds100/keymapper/priv/provider/PrivBinderProvider.kt @@ -0,0 +1,274 @@ +package io.github.sds100.keymapper.priv.provider + +import android.content.ContentProvider +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Log +import moe.shizuku.api.BinderContainer + +/** + * + * + * This provider receives binder from Shizuku server. When app process starts, + * Shizuku server (it runs under adb/root) will send the binder to client apps with this provider. + * + * + * + * Add the provider to your manifest like this: + * + *
<manifest>
+ * ...
+ * <application>
+ * ...
+ * <provider
+ * android:name="rikka.shizuku.ShizukuProvider"
+ * android:authorities="${applicationId}.shizuku"
+ * android:exported="true"
+ * android:multiprocess="false"
+ * android:permission="android.permission.INTERACT_ACROSS_USERS_FULL"
+ * </provider>
+ * ...
+ * </application>
+ * </manifest>
+ * + * + * + * There are something needs you attention: + * + * + * 1. `android:permission` shoule be a permission that granted to Shell (com.android.shell) + * but not normal apps (e.g., android.permission.INTERACT_ACROSS_USERS_FULL), so that it can only + * be used by the app itself and Shizuku server. + * 1. `android:exported` must be `true` so that the provider can be accessed + * from Shizuku server runs under adb. + * 1. `android:multiprocess` must be `false` + * since Shizuku server only gets uid when app starts. + * + * + * + * If your app runs in multiple processes, this provider also provides the functionality of sharing + * the binder across processes. See [.enableMultiProcessSupport]. + * + */ +class PrivBinderProvider : ContentProvider() { + override fun attachInfo(context: Context?, info: ProviderInfo) { + super.attachInfo(context, info) + + check(!info.multiprocess) { "android:multiprocess must be false" } + + check(info.exported) { "android:exported must be true" } + + isProviderProcess = true + } + + override fun onCreate(): Boolean { + if (enableSuiInitialization && !Sui.isSui()) { + val result: Boolean = Sui.init(context!!.packageName) + Log.d(TAG, "Initialize Sui: " + result) + } + return true + } + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + if (Sui.isSui()) { + Log.w( + TAG, + "Provider called when Sui is available. Are you using Shizuku and Sui at the same time?" + ) + return Bundle() + } + + if (extras == null) { + return null + } + + extras.classLoader = BinderContainer::class.java.getClassLoader() + + val reply = Bundle() + when (method) { + METHOD_SEND_BINDER -> { + handleSendBinder(extras) + } + + METHOD_GET_BINDER -> { + if (!handleGetBinder(reply)) { + return null + } + } + } + return reply + } + + private fun handleSendBinder(extras: Bundle) { + if (Shizuku.pingBinder()) { + Log.d(TAG, "sendBinder is called when already a living binder") + return + } + + val container: BinderContainer? = extras.getParcelable(EXTRA_BINDER) + if (container != null && container.binder != null) { + Log.d(TAG, "binder received") + + Shizuku.onBinderReceived(container.binder, context!!.packageName) + + if (enableMultiProcess) { + Log.d(TAG, "broadcast binder") + + val intent: Intent = Intent(ACTION_BINDER_RECEIVED) + .putExtra(EXTRA_BINDER, container) + .setPackage(context!!.packageName) + context!!.sendBroadcast(intent) + } + } + } + + private fun handleGetBinder(reply: Bundle): Boolean { + // Other processes in the same app can read the provider without permission + val binder: IBinder? = Shizuku.getBinder() + if (binder == null || !binder.pingBinder()) return false + + reply.putParcelable(EXTRA_BINDER, BinderContainer(binder)) + return true + } + + // no other provider methods + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + return null + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + return 0 + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + return 0 + } + + companion object { + private const val TAG = "ShizukuProvider" + + // For receive Binder from Shizuku + const val METHOD_SEND_BINDER: String = "sendBinder" + + // For share Binder between processes + const val METHOD_GET_BINDER: String = "getBinder" + + const val ACTION_BINDER_RECEIVED: String = "moe.shizuku.api.action.BINDER_RECEIVED" + + private const val EXTRA_BINDER = "moe.shizuku.privileged.api.intent.extra.BINDER" + + const val PERMISSION: String = "moe.shizuku.manager.permission.API_V23" + + const val MANAGER_APPLICATION_ID: String = "moe.shizuku.privileged.api" + + private const val enableMultiProcess = false + + private var isProviderProcess = false + + private const val enableSuiInitialization = true + + fun setIsProviderProcess(isProviderProcess: Boolean) { + ShizukuProvider.isProviderProcess = isProviderProcess + } + + /** + * Enables built-in multi-process support. + * + * + * This method MUST be called as early as possible (e.g., static block in Application). + */ + fun enableMultiProcessSupport(isProviderProcess: Boolean) { + Log.d( + TAG, + "Enable built-in multi-process support (from " + (if (isProviderProcess) "provider process" else "non-provider process") + ")" + ) + + ShizukuProvider.isProviderProcess = isProviderProcess + ShizukuProvider.enableMultiProcess = true + } + + /** + * Disable automatic Sui initialization. + */ + fun disableAutomaticSuiInitialization() { + ShizukuProvider.enableSuiInitialization = false + } + + /** + * Require binder for non-provider process, should have [.enableMultiProcessSupport] called first. + * + * @param context Context + */ + fun requestBinderForNonProviderProcess(context: Context) { + if (isProviderProcess) { + return + } + + Log.d(TAG, "request binder in non-provider process") + + val receiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val container: BinderContainer? = intent.getParcelableExtra(EXTRA_BINDER) + if (container != null && container.binder != null) { + Log.i(TAG, "binder received from broadcast") + Shizuku.onBinderReceived(container.binder, context.packageName) + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver( + receiver, + IntentFilter(ACTION_BINDER_RECEIVED), + Context.RECEIVER_NOT_EXPORTED + ) + } else { + context.registerReceiver(receiver, IntentFilter(ACTION_BINDER_RECEIVED)) + } + + var reply: Bundle? + try { + reply = context.contentResolver.call( + Uri.parse("content://" + context.packageName + ".shizuku"), + ShizukuProvider.METHOD_GET_BINDER, null, Bundle() + ) + } catch (tr: Throwable) { + reply = null + } + + if (reply != null) { + reply.classLoader = BinderContainer::class.java.getClassLoader() + + val container: BinderContainer? = reply.getParcelable(EXTRA_BINDER) + if (container != null && container.binder != null) { + Log.i(TAG, "Binder received from other process") + Shizuku.onBinderReceived(container.binder, context.packageName) + } + } + } + } +} From 295a90542738e54ac17b6b94a394df0f40569dfe Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 11 Jul 2025 15:52:27 -0600 Subject: [PATCH 016/215] #1394 rename priv service to system bridge --- app/build.gradle.kts | 2 +- .../github/sds100/keymapper/MainActivity.kt | 1 - .../AccessibilityServiceController.kt | 6 +- base/build.gradle.kts | 2 +- .../sds100/keymapper/base/BaseMainActivity.kt | 4 +- .../BaseAccessibilityServiceController.kt | 10 +- priv/src/main/AndroidManifest.xml | 18 -- .../sds100/keymapper/priv/PrivHiltModule.kt | 18 -- .../priv/provider/PrivBinderProvider.kt | 274 ------------------ settings.gradle.kts | 2 +- {priv => sysbridge}/.gitignore | 0 {priv => sysbridge}/build.gradle.kts | 2 +- {priv => sysbridge}/consumer-rules.pro | 0 {priv => sysbridge}/proguard-rules.pro | 0 sysbridge/src/main/AndroidManifest.xml | 7 + .../keymapper/sysbridge/ISystemBridge.aidl | 4 +- .../src/main/cpp/CMakeLists.txt | 2 +- .../src/main/cpp/adb_pairing.cpp | 2 +- .../src/main/cpp/adb_pairing.h | 0 {priv => sysbridge}/src/main/cpp/android.cpp | 0 {priv => sysbridge}/src/main/cpp/android.h | 0 {priv => sysbridge}/src/main/cpp/cgroup.cpp | 0 {priv => sysbridge}/src/main/cpp/cgroup.h | 0 .../src/main/cpp/libevdev/Makefile.am | 0 .../src/main/cpp/libevdev/libevdev-int.h | 0 .../src/main/cpp/libevdev/libevdev-names.c | 0 .../main/cpp/libevdev/libevdev-uinput-int.h | 0 .../src/main/cpp/libevdev/libevdev-uinput.c | 0 .../src/main/cpp/libevdev/libevdev-uinput.h | 0 .../src/main/cpp/libevdev/libevdev-util.h | 0 .../src/main/cpp/libevdev/libevdev.c | 0 .../src/main/cpp/libevdev/libevdev.h | 0 .../src/main/cpp/libevdev/libevdev.sym | 0 .../src/main/cpp/libevdev/make-event-names.py | 0 {priv => sysbridge}/src/main/cpp/logging.h | 0 {priv => sysbridge}/src/main/cpp/misc.cpp | 0 {priv => sysbridge}/src/main/cpp/misc.h | 0 .../src/main/cpp/privservice.cpp | 0 {priv => sysbridge}/src/main/cpp/selinux.cpp | 0 {priv => sysbridge}/src/main/cpp/selinux.h | 0 {priv => sysbridge}/src/main/cpp/starter.cpp | 2 +- .../sysbridge/SysBridgeHiltModule.kt | 18 ++ .../keymapper/sysbridge}/adb/AdbClient.kt | 28 +- .../keymapper/sysbridge}/adb/AdbException.kt | 2 +- .../sds100/keymapper/sysbridge}/adb/AdbKey.kt | 2 +- .../keymapper/sysbridge}/adb/AdbMdns.kt | 2 +- .../keymapper/sysbridge}/adb/AdbMessage.kt | 20 +- .../sysbridge}/adb/AdbPairingClient.kt | 2 +- .../keymapper/sysbridge}/adb/AdbProtocol.kt | 2 +- .../sysbridge}/adb/AdbServiceType.kt | 2 +- .../keymapper/sysbridge}/ktx/Context.kt | 2 +- .../sds100/keymapper/sysbridge}/ktx/Log.kt | 2 +- .../sysbridge/service/SystemBridge.kt | 8 +- .../service/SystemBridgeSetupController.kt | 34 +-- .../keymapper/sysbridge}/starter/Starter.kt | 10 +- {priv => sysbridge}/src/main/res/raw/start.sh | 0 .../src/main/res/values/strings.xml | 0 57 files changed, 102 insertions(+), 388 deletions(-) delete mode 100644 priv/src/main/AndroidManifest.xml delete mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/PrivHiltModule.kt delete mode 100644 priv/src/main/java/io/github/sds100/keymapper/priv/provider/PrivBinderProvider.kt rename {priv => sysbridge}/.gitignore (100%) rename {priv => sysbridge}/build.gradle.kts (98%) rename {priv => sysbridge}/consumer-rules.pro (100%) rename {priv => sysbridge}/proguard-rules.pro (100%) create mode 100644 sysbridge/src/main/AndroidManifest.xml rename priv/src/main/aidl/io/github/sds100/keymapper/priv/IPrivService.aidl => sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl (76%) rename {priv => sysbridge}/src/main/cpp/CMakeLists.txt (99%) rename {priv => sysbridge}/src/main/cpp/adb_pairing.cpp (99%) rename {priv => sysbridge}/src/main/cpp/adb_pairing.h (100%) rename {priv => sysbridge}/src/main/cpp/android.cpp (100%) rename {priv => sysbridge}/src/main/cpp/android.h (100%) rename {priv => sysbridge}/src/main/cpp/cgroup.cpp (100%) rename {priv => sysbridge}/src/main/cpp/cgroup.h (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/Makefile.am (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev-int.h (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev-names.c (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev-uinput-int.h (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev-uinput.c (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev-uinput.h (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev-util.h (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev.c (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev.h (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/libevdev.sym (100%) rename {priv => sysbridge}/src/main/cpp/libevdev/make-event-names.py (100%) rename {priv => sysbridge}/src/main/cpp/logging.h (100%) rename {priv => sysbridge}/src/main/cpp/misc.cpp (100%) rename {priv => sysbridge}/src/main/cpp/misc.h (100%) rename {priv => sysbridge}/src/main/cpp/privservice.cpp (100%) rename {priv => sysbridge}/src/main/cpp/selinux.cpp (100%) rename {priv => sysbridge}/src/main/cpp/selinux.h (100%) rename {priv => sysbridge}/src/main/cpp/starter.cpp (99%) create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/adb/AdbClient.kt (85%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/adb/AdbException.kt (91%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/adb/AdbKey.kt (99%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/adb/AdbMdns.kt (98%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/adb/AdbMessage.kt (85%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/adb/AdbPairingClient.kt (99%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/adb/AdbProtocol.kt (91%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/adb/AdbServiceType.kt (71%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/ktx/Context.kt (84%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/ktx/Log.kt (96%) rename priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt (86%) rename priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt (87%) rename {priv/src/main/java/io/github/sds100/keymapper/priv => sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge}/starter/Starter.kt (93%) rename {priv => sysbridge}/src/main/res/raw/start.sh (100%) rename {priv => sysbridge}/src/main/res/values/strings.xml (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c71b4a3782..77c8450f1b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,7 +141,7 @@ dependencies { implementation(project(":base")) implementation(project(":api")) implementation(project(":data")) - implementation(project(":priv")) + implementation(project(":sysbridge")) implementation(project(":system")) compileOnly(project(":systemstubs")) diff --git a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt b/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt index 0fe4632810..d48551a3dc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt @@ -10,7 +10,6 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.databinding.ActivityMainBinding import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.showDialogs -import io.github.sds100.keymapper.priv.service.PrivServiceSetupController import javax.inject.Inject @AndroidEntryPoint diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 3002c68927..d5a9ebd1f3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -12,7 +12,7 @@ import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsControll import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeRecorder import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.priv.service.PrivServiceSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.root.SuAdapter @@ -29,7 +29,7 @@ class AccessibilityServiceController @AssistedInject constructor( devicesAdapter: DevicesAdapter, suAdapter: SuAdapter, settingsRepository: PreferenceRepository, - privServiceSetupController: PrivServiceSetupController + systemBridgeSetupController: SystemBridgeSetupController ) : BaseAccessibilityServiceController( service = service, rerouteKeyEventsControllerFactory = rerouteKeyEventsControllerFactory, @@ -42,7 +42,7 @@ class AccessibilityServiceController @AssistedInject constructor( devicesAdapter = devicesAdapter, suAdapter = suAdapter, settingsRepository = settingsRepository, - privServiceSetupController = privServiceSetupController + systemBridgeSetupController = systemBridgeSetupController ) { @AssistedFactory diff --git a/base/build.gradle.kts b/base/build.gradle.kts index 2d0d758eed..d2813ccb33 100644 --- a/base/build.gradle.kts +++ b/base/build.gradle.kts @@ -73,7 +73,7 @@ android { dependencies { implementation(project(":common")) implementation(project(":data")) - implementation(project(":priv")) + implementation(project(":sysbridge")) implementation(project(":system")) implementation(project(":systemstubs")) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 4ae4144a7d..fc480bd5c8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -33,7 +33,7 @@ import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider -import io.github.sds100.keymapper.priv.service.PrivServiceSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl @@ -85,7 +85,7 @@ abstract class BaseMainActivity : AppCompatActivity() { lateinit var buildConfigProvider: BuildConfigProvider @Inject - lateinit var privServiceSetup: PrivServiceSetupController + lateinit var privServiceSetup: SystemBridgeSetupController @Inject lateinit var suAdapter: SuAdapterImpl diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 9faa7dd2e4..0e8e8e9803 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -29,7 +29,7 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.priv.service.PrivServiceSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils @@ -71,7 +71,7 @@ abstract class BaseAccessibilityServiceController( private val devicesAdapter: DevicesAdapter, private val suAdapter: SuAdapter, private val settingsRepository: PreferenceRepository, - private val privServiceSetupController: PrivServiceSetupController + private val systemBridgeSetupController: SystemBridgeSetupController ) { companion object { @@ -399,7 +399,7 @@ abstract class BaseAccessibilityServiceController( //TODO remove if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && event.action == KeyEvent.ACTION_DOWN) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - privServiceSetupController.startWithAdb() + systemBridgeSetupController.startWithAdb() } } @@ -562,9 +562,9 @@ abstract class BaseAccessibilityServiceController( if (pairingCode != null && port != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { service.lifecycleScope.launch { - privServiceSetupController.pairWirelessAdb(port, pairingCode) + systemBridgeSetupController.pairWirelessAdb(port, pairingCode) delay(1000) - privServiceSetupController.startWithAdb() + systemBridgeSetupController.startWithAdb() } } } diff --git a/priv/src/main/AndroidManifest.xml b/priv/src/main/AndroidManifest.xml deleted file mode 100644 index 2165f224e3..0000000000 --- a/priv/src/main/AndroidManifest.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/PrivHiltModule.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/PrivHiltModule.kt deleted file mode 100644 index 004044ce35..0000000000 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/PrivHiltModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.sds100.keymapper.priv - -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import io.github.sds100.keymapper.priv.service.PrivServiceSetupController -import io.github.sds100.keymapper.priv.service.PrivServiceSetupControllerImpl -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -abstract class PrivHiltModule { - - @Singleton - @Binds - abstract fun bindPrivServiceSetupController(impl: PrivServiceSetupControllerImpl): PrivServiceSetupController -} \ No newline at end of file diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/provider/PrivBinderProvider.kt b/priv/src/main/java/io/github/sds100/keymapper/priv/provider/PrivBinderProvider.kt deleted file mode 100644 index 7617668cfd..0000000000 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/provider/PrivBinderProvider.kt +++ /dev/null @@ -1,274 +0,0 @@ -package io.github.sds100.keymapper.priv.provider - -import android.content.ContentProvider -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.database.Cursor -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.util.Log -import moe.shizuku.api.BinderContainer - -/** - * - * - * This provider receives binder from Shizuku server. When app process starts, - * Shizuku server (it runs under adb/root) will send the binder to client apps with this provider. - * - * - * - * Add the provider to your manifest like this: - * - *
<manifest>
- * ...
- * <application>
- * ...
- * <provider
- * android:name="rikka.shizuku.ShizukuProvider"
- * android:authorities="${applicationId}.shizuku"
- * android:exported="true"
- * android:multiprocess="false"
- * android:permission="android.permission.INTERACT_ACROSS_USERS_FULL"
- * </provider>
- * ...
- * </application>
- * </manifest>
- * - * - * - * There are something needs you attention: - * - * - * 1. `android:permission` shoule be a permission that granted to Shell (com.android.shell) - * but not normal apps (e.g., android.permission.INTERACT_ACROSS_USERS_FULL), so that it can only - * be used by the app itself and Shizuku server. - * 1. `android:exported` must be `true` so that the provider can be accessed - * from Shizuku server runs under adb. - * 1. `android:multiprocess` must be `false` - * since Shizuku server only gets uid when app starts. - * - * - * - * If your app runs in multiple processes, this provider also provides the functionality of sharing - * the binder across processes. See [.enableMultiProcessSupport]. - * - */ -class PrivBinderProvider : ContentProvider() { - override fun attachInfo(context: Context?, info: ProviderInfo) { - super.attachInfo(context, info) - - check(!info.multiprocess) { "android:multiprocess must be false" } - - check(info.exported) { "android:exported must be true" } - - isProviderProcess = true - } - - override fun onCreate(): Boolean { - if (enableSuiInitialization && !Sui.isSui()) { - val result: Boolean = Sui.init(context!!.packageName) - Log.d(TAG, "Initialize Sui: " + result) - } - return true - } - - override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { - if (Sui.isSui()) { - Log.w( - TAG, - "Provider called when Sui is available. Are you using Shizuku and Sui at the same time?" - ) - return Bundle() - } - - if (extras == null) { - return null - } - - extras.classLoader = BinderContainer::class.java.getClassLoader() - - val reply = Bundle() - when (method) { - METHOD_SEND_BINDER -> { - handleSendBinder(extras) - } - - METHOD_GET_BINDER -> { - if (!handleGetBinder(reply)) { - return null - } - } - } - return reply - } - - private fun handleSendBinder(extras: Bundle) { - if (Shizuku.pingBinder()) { - Log.d(TAG, "sendBinder is called when already a living binder") - return - } - - val container: BinderContainer? = extras.getParcelable(EXTRA_BINDER) - if (container != null && container.binder != null) { - Log.d(TAG, "binder received") - - Shizuku.onBinderReceived(container.binder, context!!.packageName) - - if (enableMultiProcess) { - Log.d(TAG, "broadcast binder") - - val intent: Intent = Intent(ACTION_BINDER_RECEIVED) - .putExtra(EXTRA_BINDER, container) - .setPackage(context!!.packageName) - context!!.sendBroadcast(intent) - } - } - } - - private fun handleGetBinder(reply: Bundle): Boolean { - // Other processes in the same app can read the provider without permission - val binder: IBinder? = Shizuku.getBinder() - if (binder == null || !binder.pingBinder()) return false - - reply.putParcelable(EXTRA_BINDER, BinderContainer(binder)) - return true - } - - // no other provider methods - override fun query( - uri: Uri, - projection: Array?, - selection: String?, - selectionArgs: Array?, - sortOrder: String? - ): Cursor? { - return null - } - - override fun getType(uri: Uri): String? { - return null - } - - override fun insert(uri: Uri, values: ContentValues?): Uri? { - return null - } - - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - return 0 - } - - override fun update( - uri: Uri, - values: ContentValues?, - selection: String?, - selectionArgs: Array? - ): Int { - return 0 - } - - companion object { - private const val TAG = "ShizukuProvider" - - // For receive Binder from Shizuku - const val METHOD_SEND_BINDER: String = "sendBinder" - - // For share Binder between processes - const val METHOD_GET_BINDER: String = "getBinder" - - const val ACTION_BINDER_RECEIVED: String = "moe.shizuku.api.action.BINDER_RECEIVED" - - private const val EXTRA_BINDER = "moe.shizuku.privileged.api.intent.extra.BINDER" - - const val PERMISSION: String = "moe.shizuku.manager.permission.API_V23" - - const val MANAGER_APPLICATION_ID: String = "moe.shizuku.privileged.api" - - private const val enableMultiProcess = false - - private var isProviderProcess = false - - private const val enableSuiInitialization = true - - fun setIsProviderProcess(isProviderProcess: Boolean) { - ShizukuProvider.isProviderProcess = isProviderProcess - } - - /** - * Enables built-in multi-process support. - * - * - * This method MUST be called as early as possible (e.g., static block in Application). - */ - fun enableMultiProcessSupport(isProviderProcess: Boolean) { - Log.d( - TAG, - "Enable built-in multi-process support (from " + (if (isProviderProcess) "provider process" else "non-provider process") + ")" - ) - - ShizukuProvider.isProviderProcess = isProviderProcess - ShizukuProvider.enableMultiProcess = true - } - - /** - * Disable automatic Sui initialization. - */ - fun disableAutomaticSuiInitialization() { - ShizukuProvider.enableSuiInitialization = false - } - - /** - * Require binder for non-provider process, should have [.enableMultiProcessSupport] called first. - * - * @param context Context - */ - fun requestBinderForNonProviderProcess(context: Context) { - if (isProviderProcess) { - return - } - - Log.d(TAG, "request binder in non-provider process") - - val receiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val container: BinderContainer? = intent.getParcelableExtra(EXTRA_BINDER) - if (container != null && container.binder != null) { - Log.i(TAG, "binder received from broadcast") - Shizuku.onBinderReceived(container.binder, context.packageName) - } - } - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver( - receiver, - IntentFilter(ACTION_BINDER_RECEIVED), - Context.RECEIVER_NOT_EXPORTED - ) - } else { - context.registerReceiver(receiver, IntentFilter(ACTION_BINDER_RECEIVED)) - } - - var reply: Bundle? - try { - reply = context.contentResolver.call( - Uri.parse("content://" + context.packageName + ".shizuku"), - ShizukuProvider.METHOD_GET_BINDER, null, Bundle() - ) - } catch (tr: Throwable) { - reply = null - } - - if (reply != null) { - reply.classLoader = BinderContainer::class.java.getClassLoader() - - val container: BinderContainer? = reply.getParcelable(EXTRA_BINDER) - if (container != null && container.binder != null) { - Log.i(TAG, "Binder received from other process") - Shizuku.onBinderReceived(container.binder, context.packageName) - } - } - } - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0779070117..f035ff415c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,4 +30,4 @@ include(":api") include(":system") include(":common") include(":data") -include(":priv") +include(":sysbridge") diff --git a/priv/.gitignore b/sysbridge/.gitignore similarity index 100% rename from priv/.gitignore rename to sysbridge/.gitignore diff --git a/priv/build.gradle.kts b/sysbridge/build.gradle.kts similarity index 98% rename from priv/build.gradle.kts rename to sysbridge/build.gradle.kts index 7dd2babd92..a19bdee905 100644 --- a/priv/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } android { - namespace = "io.github.sds100.keymapper.priv" + namespace = "io.github.sds100.keymapper.sysbridge" compileSdk = libs.versions.compile.sdk.get().toInt() defaultConfig { diff --git a/priv/consumer-rules.pro b/sysbridge/consumer-rules.pro similarity index 100% rename from priv/consumer-rules.pro rename to sysbridge/consumer-rules.pro diff --git a/priv/proguard-rules.pro b/sysbridge/proguard-rules.pro similarity index 100% rename from priv/proguard-rules.pro rename to sysbridge/proguard-rules.pro diff --git a/sysbridge/src/main/AndroidManifest.xml b/sysbridge/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..65b230d0de --- /dev/null +++ b/sysbridge/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/priv/src/main/aidl/io/github/sds100/keymapper/priv/IPrivService.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl similarity index 76% rename from priv/src/main/aidl/io/github/sds100/keymapper/priv/IPrivService.aidl rename to sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 12931cb84c..d8f20ff56c 100644 --- a/priv/src/main/aidl/io/github/sds100/keymapper/priv/IPrivService.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -1,6 +1,6 @@ -package io.github.sds100.keymapper.priv; +package io.github.sds100.keymapper.sysbridge; -interface IPrivService { +interface ISystemBridge { // Destroy method defined by Shizuku server. This is required // for Shizuku user services. // See demo/service/UserService.java in the Shizuku-API repository. diff --git a/priv/src/main/cpp/CMakeLists.txt b/sysbridge/src/main/cpp/CMakeLists.txt similarity index 99% rename from priv/src/main/cpp/CMakeLists.txt rename to sysbridge/src/main/cpp/CMakeLists.txt index 563da15e89..b1b35a3c67 100644 --- a/priv/src/main/cpp/CMakeLists.txt +++ b/sysbridge/src/main/cpp/CMakeLists.txt @@ -9,7 +9,7 @@ cmake_minimum_required(VERSION 3.22.1) # 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("priv") +project("sysbridge") # FROM SHIZUKU set(CMAKE_CXX_STANDARD 17) diff --git a/priv/src/main/cpp/adb_pairing.cpp b/sysbridge/src/main/cpp/adb_pairing.cpp similarity index 99% rename from priv/src/main/cpp/adb_pairing.cpp rename to sysbridge/src/main/cpp/adb_pairing.cpp index 80d10f45f9..a7651671c0 100644 --- a/priv/src/main/cpp/adb_pairing.cpp +++ b/sysbridge/src/main/cpp/adb_pairing.cpp @@ -223,7 +223,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { {"nativeDestroy", "(J)V", (void *) PairingContext_Destroy}, }; - env->RegisterNatives(env->FindClass("io/github/sds100/keymapper/priv/adb/PairingContext"), + env->RegisterNatives(env->FindClass("io/github/sds100/keymapper/sysbridge/adb/PairingContext"), methods_PairingContext, sizeof(methods_PairingContext) / sizeof(JNINativeMethod)); diff --git a/priv/src/main/cpp/adb_pairing.h b/sysbridge/src/main/cpp/adb_pairing.h similarity index 100% rename from priv/src/main/cpp/adb_pairing.h rename to sysbridge/src/main/cpp/adb_pairing.h diff --git a/priv/src/main/cpp/android.cpp b/sysbridge/src/main/cpp/android.cpp similarity index 100% rename from priv/src/main/cpp/android.cpp rename to sysbridge/src/main/cpp/android.cpp diff --git a/priv/src/main/cpp/android.h b/sysbridge/src/main/cpp/android.h similarity index 100% rename from priv/src/main/cpp/android.h rename to sysbridge/src/main/cpp/android.h diff --git a/priv/src/main/cpp/cgroup.cpp b/sysbridge/src/main/cpp/cgroup.cpp similarity index 100% rename from priv/src/main/cpp/cgroup.cpp rename to sysbridge/src/main/cpp/cgroup.cpp diff --git a/priv/src/main/cpp/cgroup.h b/sysbridge/src/main/cpp/cgroup.h similarity index 100% rename from priv/src/main/cpp/cgroup.h rename to sysbridge/src/main/cpp/cgroup.h diff --git a/priv/src/main/cpp/libevdev/Makefile.am b/sysbridge/src/main/cpp/libevdev/Makefile.am similarity index 100% rename from priv/src/main/cpp/libevdev/Makefile.am rename to sysbridge/src/main/cpp/libevdev/Makefile.am diff --git a/priv/src/main/cpp/libevdev/libevdev-int.h b/sysbridge/src/main/cpp/libevdev/libevdev-int.h similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev-int.h rename to sysbridge/src/main/cpp/libevdev/libevdev-int.h diff --git a/priv/src/main/cpp/libevdev/libevdev-names.c b/sysbridge/src/main/cpp/libevdev/libevdev-names.c similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev-names.c rename to sysbridge/src/main/cpp/libevdev/libevdev-names.c diff --git a/priv/src/main/cpp/libevdev/libevdev-uinput-int.h b/sysbridge/src/main/cpp/libevdev/libevdev-uinput-int.h similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev-uinput-int.h rename to sysbridge/src/main/cpp/libevdev/libevdev-uinput-int.h diff --git a/priv/src/main/cpp/libevdev/libevdev-uinput.c b/sysbridge/src/main/cpp/libevdev/libevdev-uinput.c similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev-uinput.c rename to sysbridge/src/main/cpp/libevdev/libevdev-uinput.c diff --git a/priv/src/main/cpp/libevdev/libevdev-uinput.h b/sysbridge/src/main/cpp/libevdev/libevdev-uinput.h similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev-uinput.h rename to sysbridge/src/main/cpp/libevdev/libevdev-uinput.h diff --git a/priv/src/main/cpp/libevdev/libevdev-util.h b/sysbridge/src/main/cpp/libevdev/libevdev-util.h similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev-util.h rename to sysbridge/src/main/cpp/libevdev/libevdev-util.h diff --git a/priv/src/main/cpp/libevdev/libevdev.c b/sysbridge/src/main/cpp/libevdev/libevdev.c similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev.c rename to sysbridge/src/main/cpp/libevdev/libevdev.c diff --git a/priv/src/main/cpp/libevdev/libevdev.h b/sysbridge/src/main/cpp/libevdev/libevdev.h similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev.h rename to sysbridge/src/main/cpp/libevdev/libevdev.h diff --git a/priv/src/main/cpp/libevdev/libevdev.sym b/sysbridge/src/main/cpp/libevdev/libevdev.sym similarity index 100% rename from priv/src/main/cpp/libevdev/libevdev.sym rename to sysbridge/src/main/cpp/libevdev/libevdev.sym diff --git a/priv/src/main/cpp/libevdev/make-event-names.py b/sysbridge/src/main/cpp/libevdev/make-event-names.py similarity index 100% rename from priv/src/main/cpp/libevdev/make-event-names.py rename to sysbridge/src/main/cpp/libevdev/make-event-names.py diff --git a/priv/src/main/cpp/logging.h b/sysbridge/src/main/cpp/logging.h similarity index 100% rename from priv/src/main/cpp/logging.h rename to sysbridge/src/main/cpp/logging.h diff --git a/priv/src/main/cpp/misc.cpp b/sysbridge/src/main/cpp/misc.cpp similarity index 100% rename from priv/src/main/cpp/misc.cpp rename to sysbridge/src/main/cpp/misc.cpp diff --git a/priv/src/main/cpp/misc.h b/sysbridge/src/main/cpp/misc.h similarity index 100% rename from priv/src/main/cpp/misc.h rename to sysbridge/src/main/cpp/misc.h diff --git a/priv/src/main/cpp/privservice.cpp b/sysbridge/src/main/cpp/privservice.cpp similarity index 100% rename from priv/src/main/cpp/privservice.cpp rename to sysbridge/src/main/cpp/privservice.cpp diff --git a/priv/src/main/cpp/selinux.cpp b/sysbridge/src/main/cpp/selinux.cpp similarity index 100% rename from priv/src/main/cpp/selinux.cpp rename to sysbridge/src/main/cpp/selinux.cpp diff --git a/priv/src/main/cpp/selinux.h b/sysbridge/src/main/cpp/selinux.h similarity index 100% rename from priv/src/main/cpp/selinux.h rename to sysbridge/src/main/cpp/selinux.h diff --git a/priv/src/main/cpp/starter.cpp b/sysbridge/src/main/cpp/starter.cpp similarity index 99% rename from priv/src/main/cpp/starter.cpp rename to sysbridge/src/main/cpp/starter.cpp index a10d1d9edd..bc9e8bdbd4 100644 --- a/priv/src/main/cpp/starter.cpp +++ b/sysbridge/src/main/cpp/starter.cpp @@ -33,7 +33,7 @@ // TODO take package name as argument #define PACKAGE_NAME "io.github.sds100.keymapper.debug" #define SERVER_NAME "keymapper_priv" -#define SERVER_CLASS_PATH "io.github.sds100.keymapper.priv.service.PrivService" +#define SERVER_CLASS_PATH "io.github.sds100.keymapper.sysbridge.service.PrivService" #if defined(__arm__) #define ABI "armeabi-v7a" diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt new file mode 100644 index 0000000000..1cdd30c30c --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt @@ -0,0 +1,18 @@ +package io.github.sds100.keymapper.sysbridge + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class SysBridgeHiltModule { + + @Singleton + @Binds + abstract fun bindPrivServiceSetupController(impl: SystemBridgeSetupControllerImpl): SystemBridgeSetupController +} \ No newline at end of file diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt similarity index 85% rename from priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt index f6545c6894..21574c8b6c 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbClient.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt @@ -1,21 +1,21 @@ -package io.github.sds100.keymapper.priv.adb +package io.github.sds100.keymapper.sysbridge.adb import android.os.Build import android.util.Log import androidx.annotation.RequiresApi -import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY -import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_SIGNATURE -import io.github.sds100.keymapper.priv.adb.AdbProtocol.ADB_AUTH_TOKEN -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_AUTH -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_CLSE -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_CNXN -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_MAXDATA -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_OKAY -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_OPEN -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_STLS -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_STLS_VERSION -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_VERSION -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_WRTE +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_SIGNATURE +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_TOKEN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_AUTH +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CLSE +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CNXN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_MAXDATA +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_OKAY +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_OPEN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS_VERSION +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_VERSION +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_WRTE import java.io.Closeable import java.io.DataInputStream import java.io.DataOutputStream diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbException.kt similarity index 91% rename from priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbException.kt index 097ab350f4..486118ad5a 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbException.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbException.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.adb +package io.github.sds100.keymapper.sysbridge.adb @Suppress("NOTHING_TO_INLINE") internal inline fun adbError(message: Any): Nothing = throw AdbException(message.toString()) diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbKey.kt similarity index 99% rename from priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbKey.kt index 185435b05e..64867d4c08 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbKey.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbKey.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.adb +package io.github.sds100.keymapper.sysbridge.adb import android.annotation.SuppressLint import android.content.SharedPreferences diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt similarity index 98% rename from priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt index c85fb45db5..fed0ac66a7 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMdns.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.adb +package io.github.sds100.keymapper.sysbridge.adb import android.content.Context import android.net.nsd.NsdManager diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMessage.kt similarity index 85% rename from priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMessage.kt index bfae9db26c..19a1d40d1b 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbMessage.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMessage.kt @@ -1,13 +1,13 @@ -package io.github.sds100.keymapper.priv.adb - -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_AUTH -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_CLSE -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_CNXN -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_OKAY -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_OPEN -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_STLS -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_SYNC -import io.github.sds100.keymapper.priv.adb.AdbProtocol.A_WRTE +package io.github.sds100.keymapper.sysbridge.adb + +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_AUTH +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CLSE +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CNXN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_OKAY +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_OPEN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_SYNC +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_WRTE import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbPairingClient.kt similarity index 99% rename from priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbPairingClient.kt index 7750c86262..faaef4aa40 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbPairingClient.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbPairingClient.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.adb +package io.github.sds100.keymapper.sysbridge.adb import android.os.Build import android.util.Log diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbProtocol.kt similarity index 91% rename from priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbProtocol.kt index 1c312cac0d..d3d142156c 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbProtocol.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbProtocol.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.adb +package io.github.sds100.keymapper.sysbridge.adb internal object AdbProtocol { diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbServiceType.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbServiceType.kt similarity index 71% rename from priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbServiceType.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbServiceType.kt index 8f5fad9faa..ee3a49347f 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/adb/AdbServiceType.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbServiceType.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.adb +package io.github.sds100.keymapper.sysbridge.adb enum class AdbServiceType(val id: String) { TLS_CONNECT("_adb-tls-connect._tcp"), TLS_PAIR("_adb-tls-pairing._tcp") diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Context.kt similarity index 84% rename from priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Context.kt index 459f0b0e1a..201818b49a 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Context.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Context.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.ktx +package io.github.sds100.keymapper.sysbridge.ktx import android.content.Context import android.os.Build diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt similarity index 96% rename from priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt index 918398e294..7f6a358f6a 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/ktx/Log.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt @@ -1,6 +1,6 @@ @file:Suppress("NOTHING_TO_INLINE") -package io.github.sds100.keymapper.priv.ktx +package io.github.sds100.keymapper.sysbridge.ktx import android.util.Log diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt similarity index 86% rename from priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 51d6efcfe9..5fa00716a1 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivService.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -1,14 +1,14 @@ -package io.github.sds100.keymapper.priv.service +package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint import android.ddm.DdmHandleAppName import android.system.Os import android.util.Log -import io.github.sds100.keymapper.priv.IPrivService +import io.github.sds100.keymapper.sysbridge.ISystemBridge import kotlin.system.exitProcess @SuppressLint("LogNotTimber") -class PrivService : IPrivService.Stub() { +class SystemBridge : ISystemBridge.Stub() { // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. @@ -20,7 +20,7 @@ class PrivService : IPrivService.Stub() { @JvmStatic fun main(args: Array) { DdmHandleAppName.setAppName("keymapper_priv", 0) - PrivService() + SystemBridge() } } diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt similarity index 87% rename from priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 990b1bc9eb..7a9f18a7ef 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/service/PrivServiceSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.service +package io.github.sds100.keymapper.sysbridge.service import android.content.ComponentName import android.content.Context @@ -10,15 +10,15 @@ import android.preference.PreferenceManager import android.util.Log import androidx.annotation.RequiresApi import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.priv.IPrivService -import io.github.sds100.keymapper.priv.adb.AdbClient -import io.github.sds100.keymapper.priv.adb.AdbKey -import io.github.sds100.keymapper.priv.adb.AdbKeyException -import io.github.sds100.keymapper.priv.adb.AdbMdns -import io.github.sds100.keymapper.priv.adb.AdbPairingClient -import io.github.sds100.keymapper.priv.adb.AdbServiceType -import io.github.sds100.keymapper.priv.adb.PreferenceAdbKeyStore -import io.github.sds100.keymapper.priv.starter.Starter +import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.adb.AdbClient +import io.github.sds100.keymapper.sysbridge.adb.AdbKey +import io.github.sds100.keymapper.sysbridge.adb.AdbKeyException +import io.github.sds100.keymapper.sysbridge.adb.AdbMdns +import io.github.sds100.keymapper.sysbridge.adb.AdbPairingClient +import io.github.sds100.keymapper.sysbridge.adb.AdbServiceType +import io.github.sds100.keymapper.sysbridge.adb.PreferenceAdbKeyStore +import io.github.sds100.keymapper.sysbridge.starter.Starter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first @@ -32,10 +32,10 @@ import javax.inject.Singleton * This starter code is taken from the Shizuku project. */ @Singleton -class PrivServiceSetupControllerImpl @Inject constructor( +class SystemBridgeSetupControllerImpl @Inject constructor( @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope -) : PrivServiceSetupController { +) : SystemBridgeSetupController { private val sb = StringBuilder() @@ -47,12 +47,12 @@ class PrivServiceSetupControllerImpl @Inject constructor( name: ComponentName?, service: IBinder? ) { - Timber.d("priv service connected") - IPrivService.Stub.asInterface(service).sendEvent() + Timber.d("sysbridge service connected") + ISystemBridge.Stub.asInterface(service).sendEvent() } override fun onServiceDisconnected(name: ComponentName?) { - Timber.d("priv service disconnected") + Timber.d("sysbridge service disconnected") } } @@ -133,7 +133,7 @@ class PrivServiceSetupControllerImpl @Inject constructor( adbConnectMdns.stop() - val serviceIntent = Intent(ctx, PrivService::class.java) + val serviceIntent = Intent(ctx, SystemBridge::class.java) Timber.d("BINDING TO SERVICE") ctx.bindService(serviceIntent, serviceConnection, 0) @@ -215,7 +215,7 @@ class PrivServiceSetupControllerImpl @Inject constructor( } } -interface PrivServiceSetupController { +interface SystemBridgeSetupController { @RequiresApi(Build.VERSION_CODES.R) fun pairWirelessAdb(port: Int, code: Int) diff --git a/priv/src/main/java/io/github/sds100/keymapper/priv/starter/Starter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt similarity index 93% rename from priv/src/main/java/io/github/sds100/keymapper/priv/starter/Starter.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt index cb5cc94352..b82c70de03 100644 --- a/priv/src/main/java/io/github/sds100/keymapper/priv/starter/Starter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.priv.starter +package io.github.sds100.keymapper.sysbridge.starter import android.content.Context import android.os.Build @@ -6,10 +6,10 @@ import android.os.UserManager import android.system.ErrnoException import android.system.Os import androidx.annotation.RequiresApi -import io.github.sds100.keymapper.priv.R -import io.github.sds100.keymapper.priv.ktx.createDeviceProtectedStorageContextCompat -import io.github.sds100.keymapper.priv.ktx.logd -import io.github.sds100.keymapper.priv.ktx.loge +import io.github.sds100.keymapper.sysbridge.R +import io.github.sds100.keymapper.sysbridge.ktx.createDeviceProtectedStorageContextCompat +import io.github.sds100.keymapper.sysbridge.ktx.logd +import io.github.sds100.keymapper.sysbridge.ktx.loge import rikka.core.os.FileUtils import java.io.BufferedReader import java.io.ByteArrayInputStream diff --git a/priv/src/main/res/raw/start.sh b/sysbridge/src/main/res/raw/start.sh similarity index 100% rename from priv/src/main/res/raw/start.sh rename to sysbridge/src/main/res/raw/start.sh diff --git a/priv/src/main/res/values/strings.xml b/sysbridge/src/main/res/values/strings.xml similarity index 100% rename from priv/src/main/res/values/strings.xml rename to sysbridge/src/main/res/values/strings.xml From 4a9bf041e32fc973af1d3ed67ae4e852063f1f00 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 11 Jul 2025 16:24:59 -0600 Subject: [PATCH 017/215] #1394 WIP: create content provider to receive system bridge Binder --- sysbridge/src/main/AndroidManifest.xml | 12 +- .../sysbridge/SysBridgeHiltModule.kt | 6 + .../sysbridge/manager/SystemBridgeManager.kt | 26 ++++ .../sysbridge/provider/BinderContainer.java | 44 +++++++ .../provider/SystemBridgeBinderProvider.kt | 116 ++++++++++++++++++ 5 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/BinderContainer.java create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt diff --git a/sysbridge/src/main/AndroidManifest.xml b/sysbridge/src/main/AndroidManifest.xml index 65b230d0de..0b0d37149b 100644 --- a/sysbridge/src/main/AndroidManifest.xml +++ b/sysbridge/src/main/AndroidManifest.xml @@ -1,7 +1,17 @@ - + + + \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt index 1cdd30c30c..11adfd9381 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt @@ -4,6 +4,8 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManagerImpl import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl import javax.inject.Singleton @@ -15,4 +17,8 @@ abstract class SysBridgeHiltModule { @Singleton @Binds abstract fun bindPrivServiceSetupController(impl: SystemBridgeSetupControllerImpl): SystemBridgeSetupController + + @Singleton + @Binds + abstract fun bindSystemBridgeManager(impl: SystemBridgeManagerImpl): SystemBridgeManager } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt new file mode 100644 index 0000000000..6a0ae1cbbf --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -0,0 +1,26 @@ +package io.github.sds100.keymapper.sysbridge.manager + +import android.content.Context +import android.os.IBinder +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This class handles starting, stopping, (dis)connecting to the system bridge + */ +@Singleton +class SystemBridgeManagerImpl @Inject constructor( + @ApplicationContext private val ctx: Context +) : SystemBridgeManager { + + fun pingBinder(): Boolean { + return false + } + + fun onBinderReceived(binder: IBinder) { + + } +} + +interface SystemBridgeManager \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/BinderContainer.java b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/BinderContainer.java new file mode 100644 index 0000000000..b14a24670e --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/BinderContainer.java @@ -0,0 +1,44 @@ +package io.github.sds100.keymapper.sysbridge.provider; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.RestrictTo; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +@RestrictTo(LIBRARY_GROUP_PREFIX) +public class BinderContainer implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public BinderContainer createFromParcel(Parcel source) { + return new BinderContainer(source); + } + + @Override + public BinderContainer[] newArray(int size) { + return new BinderContainer[size]; + } + }; + public IBinder binder; + + public BinderContainer(IBinder binder) { + this.binder = binder; + } + + protected BinderContainer(Parcel in) { + this.binder = in.readStrongBinder(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(this.binder); + } +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt new file mode 100644 index 0000000000..5ff130c829 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt @@ -0,0 +1,116 @@ +package io.github.sds100.keymapper.sysbridge.provider + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import android.os.Bundle +import androidx.core.os.BundleCompat +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManagerImpl +import timber.log.Timber + +/** + * Taken from the ShizukuProvider class. + * + * This provider receives the Binder from the system bridge. When app process starts, + * the system bridge (it runs under adb/root) will send the binder to client apps with this provider. + */ +class SystemBridgeBinderProvider : ContentProvider() { + companion object { + // For receive Binder from Shizuku + const val METHOD_SEND_BINDER: String = "sendBinder" + + private const val EXTRA_BINDER = "moe.shizuku.privileged.api.intent.extra.BINDER" + } + + private val systemBridgeManager: SystemBridgeManagerImpl by lazy { + val appContext = context?.applicationContext ?: throw IllegalStateException() + val hiltEntryPoint = + EntryPointAccessors.fromApplication( + appContext, + SystemBridgeProviderEntryPoint::class.java + ) + + hiltEntryPoint.systemBridgeManager() + } + + override fun onCreate(): Boolean { + return true + } + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + if (extras == null) { + return null + } + + extras.classLoader = BinderContainer::class.java.getClassLoader() + + val reply = Bundle() + when (method) { + METHOD_SEND_BINDER -> { + handleSendBinder(extras) + } + } + return reply + } + + private fun handleSendBinder(extras: Bundle) { + if (systemBridgeManager.pingBinder()) { + Timber.d("sendBinder is called when there is already a Binder from the system bridge.") + return + } + + val container: BinderContainer? = BundleCompat.getParcelable( + extras, EXTRA_BINDER, + BinderContainer::class.java + ) + + if (container != null && container.binder != null) { + Timber.d("binder received") + + systemBridgeManager.onBinderReceived(container.binder) + } + } + + // no other provider methods + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + return null + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + return 0 + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + return 0 + } + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface SystemBridgeProviderEntryPoint { + fun systemBridgeManager(): SystemBridgeManagerImpl + } +} \ No newline at end of file From d6f73022e02c08c47200978be08dbb55872bd909 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 11 Jul 2025 21:02:24 -0600 Subject: [PATCH 018/215] #1394 sending system bridge binder to app works --- gradle/libs.versions.toml | 4 + sysbridge/build.gradle.kts | 3 + sysbridge/src/main/AndroidManifest.xml | 2 +- sysbridge/src/main/cpp/CMakeLists.txt | 2 +- sysbridge/src/main/cpp/starter.cpp | 2 +- .../cpp/{privservice.cpp => sysbridge.cpp} | 4 +- .../sysbridge/manager/SystemBridgeManager.kt | 8 ++ .../provider/SystemBridgeBinderProvider.kt | 2 +- .../sysbridge/service/SystemBridge.kt | 133 +++++++++++++++++- .../service/SystemBridgeSetupController.kt | 23 --- .../sysbridge/utils/IContentProviderUtils.kt | 41 ++++++ 11 files changed, 190 insertions(+), 34 deletions(-) rename sysbridge/src/main/cpp/{privservice.cpp => sysbridge.cpp} (93%) create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 423d83e61b..545d299ec1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,6 +70,7 @@ espresso-core = "3.6.1" ui-tooling = "1.8.1" # android.arch.persistence.room:testing libsu-core = "6.0.0" +rikka-hidden = "4.3.3" [libraries] # Kotlin @@ -151,6 +152,8 @@ splitties-toast = { group = "com.louiscad.splitties", name = "splitties-toast", # Rikka Shizuku rikka-shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" } rikka-shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" } +rikka-hidden-compat = { group = "dev.rikka.hidden", name = "compat", version.ref = "rikka-hidden" } +rikka-hidden-stub = { group = "dev.rikka.hidden", name = "stub", version.ref = "rikka-hidden" } # Testing junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -173,6 +176,7 @@ lsposed-hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hidde net-lingala-zip4j = { group = "net.lingala.zip4j", name = "zip4j", version.ref = "lingala-zip4j" } github-topjohnwu-libsu = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu-core" } + # Gradle Plugins - Aliases for buildscript dependencies / plugins block # These are referenced in build.gradle.kts files' plugins blocks by their ID. # Versions defined here can be used by plugins {} block in settings.gradle.kts if needed. diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index a19bdee905..e7db6690a3 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -76,6 +76,9 @@ dependencies { implementation(libs.dagger.hilt.android) ksp(libs.dagger.hilt.android.compiler) + implementation(libs.rikka.hidden.compat) + implementation(libs.rikka.hidden.stub) + // From Shizuku :manager module build.gradle file. implementation("io.github.vvb2060.ndk:boringssl:20250114") implementation("dev.rikka.ndk.thirdparty:cxx:1.2.0") diff --git a/sysbridge/src/main/AndroidManifest.xml b/sysbridge/src/main/AndroidManifest.xml index 0b0d37149b..dc5abee567 100644 --- a/sysbridge/src/main/AndroidManifest.xml +++ b/sysbridge/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ to system apps, such as the Shell package, so 3rd party apps can never connect.. --> #include "libevdev/libevdev.h" -#define LOG_TAG "KeyMapperPrivService" +#define LOG_TAG "KeyMapperSystemBridge" #include "logging.h" extern "C" JNIEXPORT jstring JNICALL -Java_io_github_sds100_keymapper_priv_service_PrivService_stringFromJNI(JNIEnv *env, +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, jobject /* this */) { char *input_file_path = "/dev/input/event12"; struct libevdev *dev = NULL; diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 6a0ae1cbbf..23c5246148 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.sysbridge.manager import android.content.Context import android.os.IBinder import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.sysbridge.ISystemBridge import javax.inject.Inject import javax.inject.Singleton @@ -14,12 +15,19 @@ class SystemBridgeManagerImpl @Inject constructor( @ApplicationContext private val ctx: Context ) : SystemBridgeManager { + private val lock: Any = Any() + private var systemBridge: ISystemBridge? = null + fun pingBinder(): Boolean { return false } fun onBinderReceived(binder: IBinder) { + synchronized(lock) { + this.systemBridge = ISystemBridge.Stub.asInterface(binder) + this.systemBridge?.sendEvent() + } } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt index 5ff130c829..ebcdc90b99 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt @@ -24,7 +24,7 @@ class SystemBridgeBinderProvider : ContentProvider() { // For receive Binder from Shizuku const val METHOD_SEND_BINDER: String = "sendBinder" - private const val EXTRA_BINDER = "moe.shizuku.privileged.api.intent.extra.BINDER" + const val EXTRA_BINDER = "io.github.sds100.keymapper.sysbridge.EXTRA_BINDER" } private val systemBridgeManager: SystemBridgeManagerImpl by lazy { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 5fa00716a1..47e185ee1f 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -1,10 +1,23 @@ package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint +import android.content.Context +import android.content.IContentProvider import android.ddm.DdmHandleAppName +import android.os.Binder +import android.os.Bundle +import android.os.IBinder +import android.os.ServiceManager import android.system.Os import android.util.Log import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.provider.BinderContainer +import io.github.sds100.keymapper.sysbridge.provider.SystemBridgeBinderProvider +import io.github.sds100.keymapper.sysbridge.utils.IContentProviderUtils +import rikka.hidden.compat.ActivityManagerApis +import rikka.hidden.compat.DeviceIdleControllerApis +import rikka.hidden.compat.UserManagerApis +import timber.log.Timber import kotlin.system.exitProcess @SuppressLint("LogNotTimber") @@ -15,26 +28,136 @@ class SystemBridge : ISystemBridge.Stub() { external fun stringFromJNI(): String companion object { - private const val TAG: String = "PrivService" + private const val TAG: String = "SystemBridge" @JvmStatic fun main(args: Array) { - DdmHandleAppName.setAppName("keymapper_priv", 0) + DdmHandleAppName.setAppName("keymapper_sysbridge", 0) SystemBridge() } + + private fun waitSystemService(name: String?) { + while (ServiceManager.getService(name) == null) { + try { + Log.i(TAG, "service $name is not started, wait 1s.") + Thread.sleep(1000) + } catch (e: InterruptedException) { + Log.w(TAG, e.message, e) + } + } + } + + fun sendBinderToApp( + binder: Binder?, + packageName: String?, + userId: Int, + ) { + try { + DeviceIdleControllerApis.addPowerSaveTempWhitelistApp( + packageName, + 30 * 1000, + userId, + 316, /* PowerExemptionManager#REASON_SHELL */"shell" + ) + Timber.d( + "Add $userId:$packageName to power save temp whitelist for 30s", + userId, + packageName + ) + } catch (tr: Throwable) { + Timber.e(tr) + } + + val providerName = "$packageName.sysbridge" + var provider: IContentProvider? = null + + val token: IBinder? = null + + try { + provider = ActivityManagerApis.getContentProviderExternal( + providerName, + userId, + token, + providerName + ) + if (provider == null) { + Log.e(TAG, "provider is null $providerName $userId") + return + } + + if (!provider.asBinder().pingBinder()) { + Log.e(TAG, "provider is dead $providerName $userId") + return + } + + val extra = Bundle() + extra.putParcelable( + SystemBridgeBinderProvider.EXTRA_BINDER, + BinderContainer(binder) + ) + + val reply: Bundle? = IContentProviderUtils.callCompat( + provider, + null, + providerName, + "sendBinder", + null, + extra + ) + if (reply != null) { + Log.i(TAG, "Send binder to user app $packageName in user $userId") + } else { + Log.w(TAG, "Failed to send binder to user app $packageName in user $userId") + } + } catch (tr: Throwable) { + Log.e(TAG, "Failed to send binder to user app $packageName in user $userId", tr) + } finally { + if (provider != null) { + try { + ActivityManagerApis.removeContentProviderExternal(providerName, token) + } catch (tr: Throwable) { + Log.w(TAG, "Failed to remove content provider $providerName", tr) + } + } + } + } } init { @SuppressLint("UnsafeDynamicallyLoadedCode") // TODO can we change "shizuku.library.path" property? - System.load("${System.getProperty("shizuku.library.path")}/libpriv.so") - Log.d(TAG, "PrivService started") + System.load("${System.getProperty("shizuku.library.path")}/libsysbridge.so") + Log.d(TAG, "SystemBridge started") + + waitSystemService("package") + waitSystemService(Context.ACTIVITY_SERVICE) + waitSystemService(Context.USER_SERVICE) + waitSystemService(Context.APP_OPS_SERVICE) + + // TODO check that the key mapper app is installed, otherwise end the process. +// val ai: ApplicationInfo? = rikka.shizuku.server.ShizukuService.getManagerApplicationInfo() +// if (ai == null) { +// System.exit(ServerConstants.MANAGER_APP_NOT_FOUND) +// } + + // TODO listen for key mapper being uninstalled, and stop the process +// ApkChangedObservers.start(ai.sourceDir, { +// if (rikka.shizuku.server.ShizukuService.getManagerApplicationInfo() == null) { +// LOGGER.w("manager app is uninstalled in user 0, exiting...") +// System.exit(ServerConstants.MANAGER_APP_NOT_FOUND) +// } +// }) + + for (userId in UserManagerApis.getUserIdsNoThrow()) { + // TODO use correct package name + sendBinderToApp(this, "io.github.sds100.keymapper.debug", userId) + } } // TODO ungrab all evdev devices // TODO ungrab all evdev devices if no key mapper app is bound to the service override fun destroy() { - Log.d(TAG, "PrivService destroyed") + Log.d(TAG, "SystemBridge destroyed") exitProcess(0) } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 7a9f18a7ef..724b4435e5 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,16 +1,11 @@ package io.github.sds100.keymapper.sysbridge.service -import android.content.ComponentName import android.content.Context -import android.content.Intent -import android.content.ServiceConnection import android.os.Build -import android.os.IBinder import android.preference.PreferenceManager import android.util.Log import androidx.annotation.RequiresApi import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.adb.AdbClient import io.github.sds100.keymapper.sysbridge.adb.AdbKey import io.github.sds100.keymapper.sysbridge.adb.AdbKeyException @@ -42,20 +37,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) private val adbConnectMdns: AdbMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) - private val serviceConnection = object : ServiceConnection { - override fun onServiceConnected( - name: ComponentName?, - service: IBinder? - ) { - Timber.d("sysbridge service connected") - ISystemBridge.Stub.asInterface(service).sendEvent() - } - - override fun onServiceDisconnected(name: ComponentName?) { - Timber.d("sysbridge service disconnected") - } - } - // TODO clean up // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) @@ -133,10 +114,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( adbConnectMdns.stop() - val serviceIntent = Intent(ctx, SystemBridge::class.java) - - Timber.d("BINDING TO SERVICE") - ctx.bindService(serviceIntent, serviceConnection, 0) } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt new file mode 100644 index 0000000000..49e9b583cb --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt @@ -0,0 +1,41 @@ +package io.github.sds100.keymapper.sysbridge.utils + +import android.content.AttributionSource +import android.content.IContentProvider +import android.os.Build +import android.os.Bundle + +object IContentProviderUtils { + + @Throws(android.os.RemoteException::class) + fun callCompat( + provider: IContentProvider, + callingPkg: String?, + authority: String?, + method: String?, + arg: String?, + extras: Bundle? + ): Bundle? { + val result: Bundle? + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val uid = android.system.Os.getuid() + + result = provider.call( + (AttributionSource.Builder(uid)).setPackageName(callingPkg).build(), + authority, + method, + arg, + extras + ) + } else if (Build.VERSION.SDK_INT >= 30) { + result = + provider.call(callingPkg, null as String?, authority, method, arg, extras) + } else if (Build.VERSION.SDK_INT >= 29) { + result = provider.call(callingPkg, authority, method, arg, extras) + } else { + result = provider.call(callingPkg, method, arg, extras) + } + + return result + } +} From 551af8c92d803f8b10fddecc93c71835564da45c Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Jul 2025 14:53:56 -0600 Subject: [PATCH 019/215] AccessibilityServiceAdapterImpl: do not spam log when checking service is crashed --- .../base/system/accessibility/AccessibilityServiceAdapterImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt index 01cb749de0..bb08d91614 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt @@ -244,7 +244,6 @@ class AccessibilityServiceAdapterImpl @Inject constructor( } override suspend fun isCrashed(): Boolean { - Timber.i("Accessibility service: checking if it is crashed") val key = "check_is_crashed" val pingJob = coroutineScope.launch { From 7c3c2183043a46cfd1bd041804ba929c35415d04 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Jul 2025 16:54:09 -0600 Subject: [PATCH 020/215] #1394 bunch of fixes and removing uses of the word "shizuku" in the code --- sysbridge/src/main/cpp/CMakeLists.txt | 14 +++++----- .../cpp/{sysbridge.cpp => libevdev_jni.cpp} | 2 +- sysbridge/src/main/cpp/starter.cpp | 27 +++++++++---------- .../keymapper/sysbridge/adb/AdbClient.kt | 8 +++--- .../sysbridge/service/SystemBridge.kt | 16 +++++++++-- .../keymapper/sysbridge/starter/Starter.kt | 5 +--- sysbridge/src/main/res/raw/start.sh | 8 +++--- 7 files changed, 44 insertions(+), 36 deletions(-) rename sysbridge/src/main/cpp/{sysbridge.cpp => libevdev_jni.cpp} (98%) diff --git a/sysbridge/src/main/cpp/CMakeLists.txt b/sysbridge/src/main/cpp/CMakeLists.txt index da8ea83532..4bc7668977 100644 --- a/sysbridge/src/main/cpp/CMakeLists.txt +++ b/sysbridge/src/main/cpp/CMakeLists.txt @@ -38,14 +38,14 @@ find_library(log-lib log) find_package(boringssl REQUIRED CONFIG) find_package(cxx REQUIRED CONFIG) -add_executable(libshizuku.so +add_executable(libsysbridge.so starter.cpp misc.cpp selinux.cpp cgroup.cpp android.cpp) -target_link_libraries(libshizuku.so ${log-lib} cxx::cxx) +target_link_libraries(libsysbridge.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") + add_custom_command(TARGET libsysbridge.so POST_BUILD + COMMAND ${CMAKE_STRIP} --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libsysbridge.so") endif () add_library(adb SHARED @@ -72,9 +72,9 @@ endif () # 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 +add_library(evdev SHARED # List C/C++ source files with relative paths to this CMakeLists.txt. - sysbridge.cpp + libevdev_jni.cpp libevdev/libevdev.c libevdev/libevdev-names.c libevdev/libevdev-uinput.c) @@ -82,7 +82,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED # 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} +target_link_libraries(evdev # List libraries link to the target library android log) diff --git a/sysbridge/src/main/cpp/sysbridge.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp similarity index 98% rename from sysbridge/src/main/cpp/sysbridge.cpp rename to sysbridge/src/main/cpp/libevdev_jni.cpp index 771ef03659..8a9ccded67 100644 --- a/sysbridge/src/main/cpp/sysbridge.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -11,7 +11,7 @@ extern "C" JNIEXPORT jstring JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, - jobject /* this */) { + jobject /* this */) { char *input_file_path = "/dev/input/event12"; struct libevdev *dev = NULL; int fd; diff --git a/sysbridge/src/main/cpp/starter.cpp b/sysbridge/src/main/cpp/starter.cpp index 363961d896..025d3582a7 100644 --- a/sysbridge/src/main/cpp/starter.cpp +++ b/sysbridge/src/main/cpp/starter.cpp @@ -32,7 +32,7 @@ // TODO take package name as argument #define PACKAGE_NAME "io.github.sds100.keymapper.debug" -#define SERVER_NAME "keymapper_priv" +#define SERVER_NAME "keymapper_sysbridge" #define SERVER_CLASS_PATH "io.github.sds100.keymapper.sysbridge.service.SystemBridge" #if defined(__arm__) @@ -96,7 +96,7 @@ v_current = (uintptr_t) v + v_size - sizeof(char *); \ 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_FMT(argv, "-Dkeymapper_sysbridge.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) @@ -186,15 +186,16 @@ int starter_main(int argc, char *argv[]) { int uid = getuid(); if (uid != 0 && uid != 2000) { - perrorf("fatal: run Shizuku from non root nor adb user (uid=%d).\n", uid); + perrorf("fatal: run system bridge 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"); + chown("/data/local/tmp/keymapper_sysbridge_starter", 2000, 2000); + se::setfilecon("/data/local/tmp/keymapper_sysbridge_starter", + "u:object_r:shell_data_file:s0"); switch_cgroup(); int sdkLevel = 0; @@ -224,11 +225,11 @@ int starter_main(int argc, char *argv[]) { } } - mkdir("/data/local/tmp/shizuku", 0707); - chmod("/data/local/tmp/shizuku", 0707); + mkdir("/data/local/tmp/keymapper_sysbridge", 0707); + chmod("/data/local/tmp/keymapper_sysbridge", 0707); if (uid == 0) { - chown("/data/local/tmp/shizuku", 2000, 2000); - se::setfilecon("/data/local/tmp/shizuku", "u:object_r:shell_data_file:s0"); + chown("/data/local/tmp/keymapper_sysbridge", 2000, 2000); + se::setfilecon("/data/local/tmp/keymapper_sysbridge", "u:object_r:shell_data_file:s0"); } printf("info: starter begin\n"); @@ -244,14 +245,12 @@ int starter_main(int argc, char *argv[]) { 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 (strcmp(SERVER_NAME, 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", + perrorf("fatal: can't kill %d, please try to stop existing sysbridge from app first.\n", pid); exit(EXIT_FATAL_KILL); } else { @@ -315,7 +314,7 @@ int main(int argc, char **argv) { LOGD("applet %s", base.data()); - constexpr const char *applet_names[] = {"shizuku_starter", nullptr}; + constexpr const char *applet_names[] = {"keymapper_sysbridge_starter", nullptr}; for (int i = 0; applet_names[i]; ++i) { if (base == applet_names[i]) { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt index 21574c8b6c..a0839d3841 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt @@ -1,7 +1,6 @@ package io.github.sds100.keymapper.sysbridge.adb import android.os.Build -import android.util.Log import androidx.annotation.RequiresApi import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_SIGNATURE @@ -16,6 +15,7 @@ import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS_VERSION import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_VERSION import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_WRTE +import timber.log.Timber import java.io.Closeable import java.io.DataInputStream import java.io.DataOutputStream @@ -61,7 +61,7 @@ internal class AdbClient(private val host: String, private val port: Int, privat val sslContext = key.sslContext tlsSocket = sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket tlsSocket.startHandshake() - Log.d(TAG, "Handshake succeeded.") + Timber.d("Handshake succeeded.") tlsInputStream = DataInputStream(tlsSocket.inputStream) tlsOutputStream = DataOutputStream(tlsSocket.outputStream) @@ -133,7 +133,7 @@ internal class AdbClient(private val host: String, private val port: Int, privat private fun write(message: AdbMessage) { outputStream.write(message.toByteArray()) outputStream.flush() - Log.d(TAG, "write ${message.toStringShort()}") + Timber.d("write ${message.toStringShort()}") } private fun read(): AdbMessage { @@ -156,7 +156,7 @@ internal class AdbClient(private val host: String, private val port: Int, privat } val message = AdbMessage(command, arg0, arg1, dataLength, checksum, magic, data) message.validateOrThrow() - Log.d(TAG, "read ${message.toStringShort()}") + Timber.d("read ${message.toStringShort()}") return message } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 47e185ee1f..7c0fbf839d 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -17,6 +17,7 @@ import io.github.sds100.keymapper.sysbridge.utils.IContentProviderUtils import rikka.hidden.compat.ActivityManagerApis import rikka.hidden.compat.DeviceIdleControllerApis import rikka.hidden.compat.UserManagerApis +import rikka.hidden.compat.adapter.ProcessObserverAdapter import timber.log.Timber import kotlin.system.exitProcess @@ -123,10 +124,19 @@ class SystemBridge : ISystemBridge.Stub() { } } + private val processObserver = object : ProcessObserverAdapter() { + override fun onProcessStateChanged(pid: Int, uid: Int, procState: Int) { + + } + + override fun onProcessDied(pid: Int, uid: Int) { + + } + } + init { @SuppressLint("UnsafeDynamicallyLoadedCode") - // TODO can we change "shizuku.library.path" property? - System.load("${System.getProperty("shizuku.library.path")}/libsysbridge.so") + System.load("${System.getProperty("keymapper_sysbridge.library.path")}/libevdev.so") Log.d(TAG, "SystemBridge started") waitSystemService("package") @@ -148,6 +158,8 @@ class SystemBridge : ISystemBridge.Stub() { // } // }) + // TODO use the process observer to rebind when key mapper starts + for (userId in UserManagerApis.getUserIdsNoThrow()) { // TODO use correct package name sendBinderToApp(this, "io.github.sds100.keymapper.debug", userId) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt index b82c70de03..2a4aff326e 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt @@ -31,9 +31,6 @@ object Starter { val sdcardCommand get() = commandInternal[1]!! - val adbCommand: String - get() = "adb shell $sdcardCommand" - fun writeSdcardFiles(context: Context) { if (commandInternal[1] != null) { logd("already written") @@ -102,7 +99,7 @@ object Starter { } private fun copyStarter(context: Context, out: File): String { - val so = "lib/${Build.SUPPORTED_ABIS[0]}/libshizuku.so" + val so = "lib/${Build.SUPPORTED_ABIS[0]}/libsysbridge.so" val ai = context.applicationInfo val fos = FileOutputStream(out) diff --git a/sysbridge/src/main/res/raw/start.sh b/sysbridge/src/main/res/raw/start.sh index 815483b755..9d58884e07 100644 --- a/sysbridge/src/main/res/raw/start.sh +++ b/sysbridge/src/main/res/raw/start.sh @@ -1,7 +1,7 @@ #!/system/bin/sh SOURCE_PATH="%%%STARTER_PATH%%%" -STARTER_PATH="/data/local/tmp/shizuku_starter" +STARTER_PATH="/data/local/tmp/keymapper_sysbridge_starter" echo "info: start.sh begin" @@ -42,10 +42,10 @@ if [ -f $STARTER_PATH ]; then $STARTER_PATH "$1" "$2" result=$? if [ ${result} -ne 0 ]; then - echo "info: shizuku_starter exit with non-zero value $result" + echo "info: keymapper_sysbridge_starter exit with non-zero value $result" else - echo "info: shizuku_starter exit with 0" + echo "info: keymapper_sysbridge_starter exit with 0" fi else - echo "Starter file not exist, please open Shizuku and try again." + echo "Starter file not exist, please open Key Mapper and try again." fi From 938ffbfc063b58ba6a5abd5975ce545cc666e7f6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Jul 2025 18:05:26 -0600 Subject: [PATCH 021/215] #1394 delete shizuku strings --- sysbridge/src/main/res/values/strings.xml | 169 ---------------------- 1 file changed, 169 deletions(-) delete mode 100644 sysbridge/src/main/res/values/strings.xml diff --git a/sysbridge/src/main/res/values/strings.xml b/sysbridge/src/main/res/values/strings.xml deleted file mode 100644 index 5e469e7d09..0000000000 --- a/sysbridge/src/main/res/values/strings.xml +++ /dev/null @@ -1,169 +0,0 @@ - - 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 - - From 111983dc74560a97d94bc71bc3d85f50d3d995e1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Jul 2025 22:00:37 -0600 Subject: [PATCH 022/215] #1394 WIP: add AOSP code to convert evdev events to Android key events --- .../cpp/android/input/InputEventLabels.cpp | 1414 +++++++++++++++++ .../main/cpp/android/input/InputEventLabels.h | 101 ++ .../main/cpp/android/input/KeyLayoutMap.cpp | 441 +++++ .../src/main/cpp/android/input/KeyLayoutMap.h | 118 ++ .../src/main/cpp/android/libbase/errors.h | 154 ++ .../src/main/cpp/android/libbase/expected.h | 622 ++++++++ .../src/main/cpp/android/libbase/format.h | 33 + .../src/main/cpp/android/libbase/result.cpp | 27 + .../src/main/cpp/android/libbase/result.h | 487 ++++++ sysbridge/src/main/cpp/android/utils/Errors.h | 77 + .../src/main/cpp/android/utils/FileMap.h | 129 ++ .../src/main/cpp/android/utils/String16.cpp | 401 +++++ .../src/main/cpp/android/utils/String16.h | 411 +++++ .../src/main/cpp/android/utils/String8.h | 386 +++++ .../src/main/cpp/android/utils/Tokenizer.cpp | 177 +++ .../src/main/cpp/android/utils/Tokenizer.h | 137 ++ .../src/main/cpp/android/utils/TypeHelpers.h | 341 ++++ .../src/main/cpp/android/utils/Unicode.h | 139 ++ sysbridge/src/main/cpp/libevdev_jni.cpp | 5 +- 19 files changed, 5598 insertions(+), 2 deletions(-) create mode 100644 sysbridge/src/main/cpp/android/input/InputEventLabels.cpp create mode 100644 sysbridge/src/main/cpp/android/input/InputEventLabels.h create mode 100644 sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp create mode 100644 sysbridge/src/main/cpp/android/input/KeyLayoutMap.h create mode 100644 sysbridge/src/main/cpp/android/libbase/errors.h create mode 100644 sysbridge/src/main/cpp/android/libbase/expected.h create mode 100644 sysbridge/src/main/cpp/android/libbase/format.h create mode 100644 sysbridge/src/main/cpp/android/libbase/result.cpp create mode 100644 sysbridge/src/main/cpp/android/libbase/result.h create mode 100644 sysbridge/src/main/cpp/android/utils/Errors.h create mode 100644 sysbridge/src/main/cpp/android/utils/FileMap.h create mode 100644 sysbridge/src/main/cpp/android/utils/String16.cpp create mode 100644 sysbridge/src/main/cpp/android/utils/String16.h create mode 100644 sysbridge/src/main/cpp/android/utils/String8.h create mode 100644 sysbridge/src/main/cpp/android/utils/Tokenizer.cpp create mode 100644 sysbridge/src/main/cpp/android/utils/Tokenizer.h create mode 100644 sysbridge/src/main/cpp/android/utils/TypeHelpers.h create mode 100644 sysbridge/src/main/cpp/android/utils/Unicode.h diff --git a/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp b/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp new file mode 100644 index 0000000000..18a3284cef --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp @@ -0,0 +1,1414 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InputEventLabels.h" + +#include +#include +#include + +#define DEFINE_KEYCODE(key) { #key, AKEYCODE_##key } +#define DEFINE_AXIS(axis) { #axis, AMOTION_EVENT_AXIS_##axis } +#define DEFINE_LED(led) { #led, ALED_##led } +#define DEFINE_FLAG(flag) { #flag, POLICY_FLAG_##flag } + +namespace android { + +// clang-format off + +// NOTE: If you add a new keycode here you must also add it to several other files. +// Refer to frameworks/base/core/java/android/view/KeyEvent.java for the full list. +#define KEYCODES_SEQUENCE \ + DEFINE_KEYCODE(UNKNOWN), \ + DEFINE_KEYCODE(SOFT_LEFT), \ + DEFINE_KEYCODE(SOFT_RIGHT), \ + DEFINE_KEYCODE(HOME), \ + DEFINE_KEYCODE(BACK), \ + DEFINE_KEYCODE(CALL), \ + DEFINE_KEYCODE(ENDCALL), \ + DEFINE_KEYCODE(0), \ + DEFINE_KEYCODE(1), \ + DEFINE_KEYCODE(2), \ + DEFINE_KEYCODE(3), \ + DEFINE_KEYCODE(4), \ + DEFINE_KEYCODE(5), \ + DEFINE_KEYCODE(6), \ + DEFINE_KEYCODE(7), \ + DEFINE_KEYCODE(8), \ + DEFINE_KEYCODE(9), \ + DEFINE_KEYCODE(STAR), \ + DEFINE_KEYCODE(POUND), \ + DEFINE_KEYCODE(DPAD_UP), \ + DEFINE_KEYCODE(DPAD_DOWN), \ + DEFINE_KEYCODE(DPAD_LEFT), \ + DEFINE_KEYCODE(DPAD_RIGHT), \ + DEFINE_KEYCODE(DPAD_CENTER), \ + DEFINE_KEYCODE(VOLUME_UP), \ + DEFINE_KEYCODE(VOLUME_DOWN), \ + DEFINE_KEYCODE(POWER), \ + DEFINE_KEYCODE(CAMERA), \ + DEFINE_KEYCODE(CLEAR), \ + DEFINE_KEYCODE(A), \ + DEFINE_KEYCODE(B), \ + DEFINE_KEYCODE(C), \ + DEFINE_KEYCODE(D), \ + DEFINE_KEYCODE(E), \ + DEFINE_KEYCODE(F), \ + DEFINE_KEYCODE(G), \ + DEFINE_KEYCODE(H), \ + DEFINE_KEYCODE(I), \ + DEFINE_KEYCODE(J), \ + DEFINE_KEYCODE(K), \ + DEFINE_KEYCODE(L), \ + DEFINE_KEYCODE(M), \ + DEFINE_KEYCODE(N), \ + DEFINE_KEYCODE(O), \ + DEFINE_KEYCODE(P), \ + DEFINE_KEYCODE(Q), \ + DEFINE_KEYCODE(R), \ + DEFINE_KEYCODE(S), \ + DEFINE_KEYCODE(T), \ + DEFINE_KEYCODE(U), \ + DEFINE_KEYCODE(V), \ + DEFINE_KEYCODE(W), \ + DEFINE_KEYCODE(X), \ + DEFINE_KEYCODE(Y), \ + DEFINE_KEYCODE(Z), \ + DEFINE_KEYCODE(COMMA), \ + DEFINE_KEYCODE(PERIOD), \ + DEFINE_KEYCODE(ALT_LEFT), \ + DEFINE_KEYCODE(ALT_RIGHT), \ + DEFINE_KEYCODE(SHIFT_LEFT), \ + DEFINE_KEYCODE(SHIFT_RIGHT), \ + DEFINE_KEYCODE(TAB), \ + DEFINE_KEYCODE(SPACE), \ + DEFINE_KEYCODE(SYM), \ + DEFINE_KEYCODE(EXPLORER), \ + DEFINE_KEYCODE(ENVELOPE), \ + DEFINE_KEYCODE(ENTER), \ + DEFINE_KEYCODE(DEL), \ + DEFINE_KEYCODE(GRAVE), \ + DEFINE_KEYCODE(MINUS), \ + DEFINE_KEYCODE(EQUALS), \ + DEFINE_KEYCODE(LEFT_BRACKET), \ + DEFINE_KEYCODE(RIGHT_BRACKET), \ + DEFINE_KEYCODE(BACKSLASH), \ + DEFINE_KEYCODE(SEMICOLON), \ + DEFINE_KEYCODE(APOSTROPHE), \ + DEFINE_KEYCODE(SLASH), \ + DEFINE_KEYCODE(AT), \ + DEFINE_KEYCODE(NUM), \ + DEFINE_KEYCODE(HEADSETHOOK), \ + DEFINE_KEYCODE(FOCUS), \ + DEFINE_KEYCODE(PLUS), \ + DEFINE_KEYCODE(MENU), \ + DEFINE_KEYCODE(NOTIFICATION), \ + DEFINE_KEYCODE(SEARCH), \ + DEFINE_KEYCODE(MEDIA_PLAY_PAUSE), \ + DEFINE_KEYCODE(MEDIA_STOP), \ + DEFINE_KEYCODE(MEDIA_NEXT), \ + DEFINE_KEYCODE(MEDIA_PREVIOUS), \ + DEFINE_KEYCODE(MEDIA_REWIND), \ + DEFINE_KEYCODE(MEDIA_FAST_FORWARD), \ + DEFINE_KEYCODE(MUTE), \ + DEFINE_KEYCODE(PAGE_UP), \ + DEFINE_KEYCODE(PAGE_DOWN), \ + DEFINE_KEYCODE(PICTSYMBOLS), \ + DEFINE_KEYCODE(SWITCH_CHARSET), \ + DEFINE_KEYCODE(BUTTON_A), \ + DEFINE_KEYCODE(BUTTON_B), \ + DEFINE_KEYCODE(BUTTON_C), \ + DEFINE_KEYCODE(BUTTON_X), \ + DEFINE_KEYCODE(BUTTON_Y), \ + DEFINE_KEYCODE(BUTTON_Z), \ + DEFINE_KEYCODE(BUTTON_L1), \ + DEFINE_KEYCODE(BUTTON_R1), \ + DEFINE_KEYCODE(BUTTON_L2), \ + DEFINE_KEYCODE(BUTTON_R2), \ + DEFINE_KEYCODE(BUTTON_THUMBL), \ + DEFINE_KEYCODE(BUTTON_THUMBR), \ + DEFINE_KEYCODE(BUTTON_START), \ + DEFINE_KEYCODE(BUTTON_SELECT), \ + DEFINE_KEYCODE(BUTTON_MODE), \ + DEFINE_KEYCODE(ESCAPE), \ + DEFINE_KEYCODE(FORWARD_DEL), \ + DEFINE_KEYCODE(CTRL_LEFT), \ + DEFINE_KEYCODE(CTRL_RIGHT), \ + DEFINE_KEYCODE(CAPS_LOCK), \ + DEFINE_KEYCODE(SCROLL_LOCK), \ + DEFINE_KEYCODE(META_LEFT), \ + DEFINE_KEYCODE(META_RIGHT), \ + DEFINE_KEYCODE(FUNCTION), \ + DEFINE_KEYCODE(SYSRQ), \ + DEFINE_KEYCODE(BREAK), \ + DEFINE_KEYCODE(MOVE_HOME), \ + DEFINE_KEYCODE(MOVE_END), \ + DEFINE_KEYCODE(INSERT), \ + DEFINE_KEYCODE(FORWARD), \ + DEFINE_KEYCODE(MEDIA_PLAY), \ + DEFINE_KEYCODE(MEDIA_PAUSE), \ + DEFINE_KEYCODE(MEDIA_CLOSE), \ + DEFINE_KEYCODE(MEDIA_EJECT), \ + DEFINE_KEYCODE(MEDIA_RECORD), \ + DEFINE_KEYCODE(F1), \ + DEFINE_KEYCODE(F2), \ + DEFINE_KEYCODE(F3), \ + DEFINE_KEYCODE(F4), \ + DEFINE_KEYCODE(F5), \ + DEFINE_KEYCODE(F6), \ + DEFINE_KEYCODE(F7), \ + DEFINE_KEYCODE(F8), \ + DEFINE_KEYCODE(F9), \ + DEFINE_KEYCODE(F10), \ + DEFINE_KEYCODE(F11), \ + DEFINE_KEYCODE(F12), \ + DEFINE_KEYCODE(NUM_LOCK), \ + DEFINE_KEYCODE(NUMPAD_0), \ + DEFINE_KEYCODE(NUMPAD_1), \ + DEFINE_KEYCODE(NUMPAD_2), \ + DEFINE_KEYCODE(NUMPAD_3), \ + DEFINE_KEYCODE(NUMPAD_4), \ + DEFINE_KEYCODE(NUMPAD_5), \ + DEFINE_KEYCODE(NUMPAD_6), \ + DEFINE_KEYCODE(NUMPAD_7), \ + DEFINE_KEYCODE(NUMPAD_8), \ + DEFINE_KEYCODE(NUMPAD_9), \ + DEFINE_KEYCODE(NUMPAD_DIVIDE), \ + DEFINE_KEYCODE(NUMPAD_MULTIPLY), \ + DEFINE_KEYCODE(NUMPAD_SUBTRACT), \ + DEFINE_KEYCODE(NUMPAD_ADD), \ + DEFINE_KEYCODE(NUMPAD_DOT), \ + DEFINE_KEYCODE(NUMPAD_COMMA), \ + DEFINE_KEYCODE(NUMPAD_ENTER), \ + DEFINE_KEYCODE(NUMPAD_EQUALS), \ + DEFINE_KEYCODE(NUMPAD_LEFT_PAREN), \ + DEFINE_KEYCODE(NUMPAD_RIGHT_PAREN), \ + DEFINE_KEYCODE(VOLUME_MUTE), \ + DEFINE_KEYCODE(INFO), \ + DEFINE_KEYCODE(CHANNEL_UP), \ + DEFINE_KEYCODE(CHANNEL_DOWN), \ + DEFINE_KEYCODE(ZOOM_IN), \ + DEFINE_KEYCODE(ZOOM_OUT), \ + DEFINE_KEYCODE(TV), \ + DEFINE_KEYCODE(WINDOW), \ + DEFINE_KEYCODE(GUIDE), \ + DEFINE_KEYCODE(DVR), \ + DEFINE_KEYCODE(BOOKMARK), \ + DEFINE_KEYCODE(CAPTIONS), \ + DEFINE_KEYCODE(SETTINGS), \ + DEFINE_KEYCODE(TV_POWER), \ + DEFINE_KEYCODE(TV_INPUT), \ + DEFINE_KEYCODE(STB_POWER), \ + DEFINE_KEYCODE(STB_INPUT), \ + DEFINE_KEYCODE(AVR_POWER), \ + DEFINE_KEYCODE(AVR_INPUT), \ + DEFINE_KEYCODE(PROG_RED), \ + DEFINE_KEYCODE(PROG_GREEN), \ + DEFINE_KEYCODE(PROG_YELLOW), \ + DEFINE_KEYCODE(PROG_BLUE), \ + DEFINE_KEYCODE(APP_SWITCH), \ + DEFINE_KEYCODE(BUTTON_1), \ + DEFINE_KEYCODE(BUTTON_2), \ + DEFINE_KEYCODE(BUTTON_3), \ + DEFINE_KEYCODE(BUTTON_4), \ + DEFINE_KEYCODE(BUTTON_5), \ + DEFINE_KEYCODE(BUTTON_6), \ + DEFINE_KEYCODE(BUTTON_7), \ + DEFINE_KEYCODE(BUTTON_8), \ + DEFINE_KEYCODE(BUTTON_9), \ + DEFINE_KEYCODE(BUTTON_10), \ + DEFINE_KEYCODE(BUTTON_11), \ + DEFINE_KEYCODE(BUTTON_12), \ + DEFINE_KEYCODE(BUTTON_13), \ + DEFINE_KEYCODE(BUTTON_14), \ + DEFINE_KEYCODE(BUTTON_15), \ + DEFINE_KEYCODE(BUTTON_16), \ + DEFINE_KEYCODE(LANGUAGE_SWITCH), \ + DEFINE_KEYCODE(MANNER_MODE), \ + DEFINE_KEYCODE(3D_MODE), \ + DEFINE_KEYCODE(CONTACTS), \ + DEFINE_KEYCODE(CALENDAR), \ + DEFINE_KEYCODE(MUSIC), \ + DEFINE_KEYCODE(CALCULATOR), \ + DEFINE_KEYCODE(ZENKAKU_HANKAKU), \ + DEFINE_KEYCODE(EISU), \ + DEFINE_KEYCODE(MUHENKAN), \ + DEFINE_KEYCODE(HENKAN), \ + DEFINE_KEYCODE(KATAKANA_HIRAGANA), \ + DEFINE_KEYCODE(YEN), \ + DEFINE_KEYCODE(RO), \ + DEFINE_KEYCODE(KANA), \ + DEFINE_KEYCODE(ASSIST), \ + DEFINE_KEYCODE(BRIGHTNESS_DOWN), \ + DEFINE_KEYCODE(BRIGHTNESS_UP), \ + DEFINE_KEYCODE(MEDIA_AUDIO_TRACK), \ + DEFINE_KEYCODE(SLEEP), \ + DEFINE_KEYCODE(WAKEUP), \ + DEFINE_KEYCODE(PAIRING), \ + DEFINE_KEYCODE(MEDIA_TOP_MENU), \ + DEFINE_KEYCODE(11), \ + DEFINE_KEYCODE(12), \ + DEFINE_KEYCODE(LAST_CHANNEL), \ + DEFINE_KEYCODE(TV_DATA_SERVICE), \ + DEFINE_KEYCODE(VOICE_ASSIST), \ + DEFINE_KEYCODE(TV_RADIO_SERVICE), \ + DEFINE_KEYCODE(TV_TELETEXT), \ + DEFINE_KEYCODE(TV_NUMBER_ENTRY), \ + DEFINE_KEYCODE(TV_TERRESTRIAL_ANALOG), \ + DEFINE_KEYCODE(TV_TERRESTRIAL_DIGITAL), \ + DEFINE_KEYCODE(TV_SATELLITE), \ + DEFINE_KEYCODE(TV_SATELLITE_BS), \ + DEFINE_KEYCODE(TV_SATELLITE_CS), \ + DEFINE_KEYCODE(TV_SATELLITE_SERVICE), \ + DEFINE_KEYCODE(TV_NETWORK), \ + DEFINE_KEYCODE(TV_ANTENNA_CABLE), \ + DEFINE_KEYCODE(TV_INPUT_HDMI_1), \ + DEFINE_KEYCODE(TV_INPUT_HDMI_2), \ + DEFINE_KEYCODE(TV_INPUT_HDMI_3), \ + DEFINE_KEYCODE(TV_INPUT_HDMI_4), \ + DEFINE_KEYCODE(TV_INPUT_COMPOSITE_1), \ + DEFINE_KEYCODE(TV_INPUT_COMPOSITE_2), \ + DEFINE_KEYCODE(TV_INPUT_COMPONENT_1), \ + DEFINE_KEYCODE(TV_INPUT_COMPONENT_2), \ + DEFINE_KEYCODE(TV_INPUT_VGA_1), \ + DEFINE_KEYCODE(TV_AUDIO_DESCRIPTION), \ + DEFINE_KEYCODE(TV_AUDIO_DESCRIPTION_MIX_UP), \ + DEFINE_KEYCODE(TV_AUDIO_DESCRIPTION_MIX_DOWN), \ + DEFINE_KEYCODE(TV_ZOOM_MODE), \ + DEFINE_KEYCODE(TV_CONTENTS_MENU), \ + DEFINE_KEYCODE(TV_MEDIA_CONTEXT_MENU), \ + DEFINE_KEYCODE(TV_TIMER_PROGRAMMING), \ + DEFINE_KEYCODE(HELP), \ + DEFINE_KEYCODE(NAVIGATE_PREVIOUS), \ + DEFINE_KEYCODE(NAVIGATE_NEXT), \ + DEFINE_KEYCODE(NAVIGATE_IN), \ + DEFINE_KEYCODE(NAVIGATE_OUT), \ + DEFINE_KEYCODE(STEM_PRIMARY), \ + DEFINE_KEYCODE(STEM_1), \ + DEFINE_KEYCODE(STEM_2), \ + DEFINE_KEYCODE(STEM_3), \ + DEFINE_KEYCODE(DPAD_UP_LEFT), \ + DEFINE_KEYCODE(DPAD_DOWN_LEFT), \ + DEFINE_KEYCODE(DPAD_UP_RIGHT), \ + DEFINE_KEYCODE(DPAD_DOWN_RIGHT), \ + DEFINE_KEYCODE(MEDIA_SKIP_FORWARD), \ + DEFINE_KEYCODE(MEDIA_SKIP_BACKWARD), \ + DEFINE_KEYCODE(MEDIA_STEP_FORWARD), \ + DEFINE_KEYCODE(MEDIA_STEP_BACKWARD), \ + DEFINE_KEYCODE(SOFT_SLEEP), \ + DEFINE_KEYCODE(CUT), \ + DEFINE_KEYCODE(COPY), \ + DEFINE_KEYCODE(PASTE), \ + DEFINE_KEYCODE(SYSTEM_NAVIGATION_UP), \ + DEFINE_KEYCODE(SYSTEM_NAVIGATION_DOWN), \ + DEFINE_KEYCODE(SYSTEM_NAVIGATION_LEFT), \ + DEFINE_KEYCODE(SYSTEM_NAVIGATION_RIGHT), \ + DEFINE_KEYCODE(ALL_APPS), \ + DEFINE_KEYCODE(REFRESH), \ + DEFINE_KEYCODE(THUMBS_UP), \ + DEFINE_KEYCODE(THUMBS_DOWN), \ + DEFINE_KEYCODE(PROFILE_SWITCH), \ + DEFINE_KEYCODE(VIDEO_APP_1), \ + DEFINE_KEYCODE(VIDEO_APP_2), \ + DEFINE_KEYCODE(VIDEO_APP_3), \ + DEFINE_KEYCODE(VIDEO_APP_4), \ + DEFINE_KEYCODE(VIDEO_APP_5), \ + DEFINE_KEYCODE(VIDEO_APP_6), \ + DEFINE_KEYCODE(VIDEO_APP_7), \ + DEFINE_KEYCODE(VIDEO_APP_8), \ + DEFINE_KEYCODE(FEATURED_APP_1), \ + DEFINE_KEYCODE(FEATURED_APP_2), \ + DEFINE_KEYCODE(FEATURED_APP_3), \ + DEFINE_KEYCODE(FEATURED_APP_4), \ + DEFINE_KEYCODE(DEMO_APP_1), \ + DEFINE_KEYCODE(DEMO_APP_2), \ + DEFINE_KEYCODE(DEMO_APP_3), \ + DEFINE_KEYCODE(DEMO_APP_4), \ + DEFINE_KEYCODE(KEYBOARD_BACKLIGHT_DOWN), \ + DEFINE_KEYCODE(KEYBOARD_BACKLIGHT_UP), \ + DEFINE_KEYCODE(KEYBOARD_BACKLIGHT_TOGGLE), \ + DEFINE_KEYCODE(STYLUS_BUTTON_PRIMARY), \ + DEFINE_KEYCODE(STYLUS_BUTTON_SECONDARY), \ + DEFINE_KEYCODE(STYLUS_BUTTON_TERTIARY), \ + DEFINE_KEYCODE(STYLUS_BUTTON_TAIL), \ + DEFINE_KEYCODE(RECENT_APPS), \ + DEFINE_KEYCODE(MACRO_1), \ + DEFINE_KEYCODE(MACRO_2), \ + DEFINE_KEYCODE(MACRO_3), \ + DEFINE_KEYCODE(MACRO_4), \ +// DEFINE_KEYCODE(EMOJI_PICKER), \ +// DEFINE_KEYCODE(SCREENSHOT), \ +// DEFINE_KEYCODE(DICTATE), \ +// DEFINE_KEYCODE(NEW), \ +// DEFINE_KEYCODE(CLOSE), \ +// DEFINE_KEYCODE(DO_NOT_DISTURB), \ +// DEFINE_KEYCODE(PRINT), \ +// DEFINE_KEYCODE(LOCK), \ +// DEFINE_KEYCODE(FULLSCREEN), \ +// DEFINE_KEYCODE(F13), \ +// DEFINE_KEYCODE(F14), \ +// DEFINE_KEYCODE(F15), \ +// DEFINE_KEYCODE(F16), \ +// DEFINE_KEYCODE(F17), \ +// DEFINE_KEYCODE(F18), \ +// DEFINE_KEYCODE(F19),\ +// DEFINE_KEYCODE(F20), \ +// DEFINE_KEYCODE(F21), \ +// DEFINE_KEYCODE(F22), \ +// DEFINE_KEYCODE(F23), \ +// DEFINE_KEYCODE(F24) + +// NOTE: If you add a new axis here you must also add it to several other files. +// Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list. +#define AXES_SEQUENCE \ + DEFINE_AXIS(X), \ + DEFINE_AXIS(Y), \ + DEFINE_AXIS(PRESSURE), \ + DEFINE_AXIS(SIZE), \ + DEFINE_AXIS(TOUCH_MAJOR), \ + DEFINE_AXIS(TOUCH_MINOR), \ + DEFINE_AXIS(TOOL_MAJOR), \ + DEFINE_AXIS(TOOL_MINOR), \ + DEFINE_AXIS(ORIENTATION), \ + DEFINE_AXIS(VSCROLL), \ + DEFINE_AXIS(HSCROLL), \ + DEFINE_AXIS(Z), \ + DEFINE_AXIS(RX), \ + DEFINE_AXIS(RY), \ + DEFINE_AXIS(RZ), \ + DEFINE_AXIS(HAT_X), \ + DEFINE_AXIS(HAT_Y), \ + DEFINE_AXIS(LTRIGGER), \ + DEFINE_AXIS(RTRIGGER), \ + DEFINE_AXIS(THROTTLE), \ + DEFINE_AXIS(RUDDER), \ + DEFINE_AXIS(WHEEL), \ + DEFINE_AXIS(GAS), \ + DEFINE_AXIS(BRAKE), \ + DEFINE_AXIS(DISTANCE), \ + DEFINE_AXIS(TILT), \ + DEFINE_AXIS(SCROLL), \ + DEFINE_AXIS(RELATIVE_X), \ + DEFINE_AXIS(RELATIVE_Y), \ + {"RESERVED_29", 29}, \ + {"RESERVED_30", 30}, \ + {"RESERVED_31", 31}, \ + DEFINE_AXIS(GENERIC_1), \ + DEFINE_AXIS(GENERIC_2), \ + DEFINE_AXIS(GENERIC_3), \ + DEFINE_AXIS(GENERIC_4), \ + DEFINE_AXIS(GENERIC_5), \ + DEFINE_AXIS(GENERIC_6), \ + DEFINE_AXIS(GENERIC_7), \ + DEFINE_AXIS(GENERIC_8), \ + DEFINE_AXIS(GENERIC_9), \ + DEFINE_AXIS(GENERIC_10), \ + DEFINE_AXIS(GENERIC_11), \ + DEFINE_AXIS(GENERIC_12), \ + DEFINE_AXIS(GENERIC_13), \ + DEFINE_AXIS(GENERIC_14), \ + DEFINE_AXIS(GENERIC_15), \ + DEFINE_AXIS(GENERIC_16), \ + DEFINE_AXIS(GESTURE_X_OFFSET), \ + DEFINE_AXIS(GESTURE_Y_OFFSET), \ + DEFINE_AXIS(GESTURE_SCROLL_X_DISTANCE), \ + DEFINE_AXIS(GESTURE_SCROLL_Y_DISTANCE), \ + DEFINE_AXIS(GESTURE_PINCH_SCALE_FACTOR), \ + DEFINE_AXIS(GESTURE_SWIPE_FINGER_COUNT) + +// clang-format on + +// --- InputEventLookup --- + + InputEventLookup::InputEventLookup() + : KEYCODES({KEYCODES_SEQUENCE}), + KEY_NAMES({KEYCODES_SEQUENCE}), + AXES({AXES_SEQUENCE}), + AXES_NAMES({AXES_SEQUENCE}) {} + + std::optional InputEventLookup::lookupValueByLabel( + const std::unordered_map &map, const char *literal) { + std::string str(literal); + auto it = map.find(str); + return it != map.end() ? std::make_optional(it->second) : std::nullopt; + } + + const char *InputEventLookup::lookupLabelByValue(const std::vector &vec, + int value) { + if (static_cast(value) < vec.size()) { + return vec[value].literal; + } + return nullptr; + } + + std::optional InputEventLookup::getKeyCodeByLabel(const char *label) { + const auto &self = get(); + return self.lookupValueByLabel(self.KEYCODES, label); + } + + const char *InputEventLookup::getLabelByKeyCode(int32_t keyCode) { + const auto &self = get(); + if (keyCode >= 0 && static_cast(keyCode) < self.KEYCODES.size()) { + return get().lookupLabelByValue(self.KEY_NAMES, keyCode); + } + return nullptr; + } + + std::optional InputEventLookup::getKeyFlagByLabel(const char *label) { + const auto &self = get(); + return lookupValueByLabel(self.FLAGS, label); + } + + std::optional InputEventLookup::getAxisByLabel(const char *label) { + const auto &self = get(); + return lookupValueByLabel(self.AXES, label); + } + + const char *InputEventLookup::getAxisLabel(int32_t axisId) { + const auto &self = get(); + return lookupLabelByValue(self.AXES_NAMES, axisId); + } + + std::optional InputEventLookup::getLedByLabel(const char *label) { + const auto &self = get(); + return lookupValueByLabel(self.LEDS, label); + } + + namespace { + + struct label { + const char *name; + int value; + }; + +#define LABEL(constant) \ + { #constant, constant } +#define LABEL_END \ + { nullptr, -1 } + +// Inserted from the file: out/soong/.intermediates/system/core/toolbox/toolbox_input_labels/gen/input.h-labels.h + static struct label ev_key_value_labels[] = { + {"UP", 0}, + {"DOWN", 1}, + {"REPEAT", 2}, + LABEL_END, + }; + + + static struct label input_prop_labels[] = { + LABEL(INPUT_PROP_POINTER), + LABEL(INPUT_PROP_DIRECT), + LABEL(INPUT_PROP_BUTTONPAD), + LABEL(INPUT_PROP_SEMI_MT), + LABEL(INPUT_PROP_TOPBUTTONPAD), + LABEL(INPUT_PROP_POINTING_STICK), + LABEL(INPUT_PROP_ACCELEROMETER), + LABEL(INPUT_PROP_MAX), + LABEL_END, + }; + static struct label ev_labels[] = { + LABEL(EV_VERSION), + LABEL(EV_SYN), + LABEL(EV_KEY), + LABEL(EV_REL), + LABEL(EV_ABS), + LABEL(EV_MSC), + LABEL(EV_SW), + LABEL(EV_LED), + LABEL(EV_SND), + LABEL(EV_REP), + LABEL(EV_FF), + LABEL(EV_PWR), + LABEL(EV_FF_STATUS), + LABEL(EV_MAX), + LABEL_END, + }; + static struct label syn_labels[] = { + LABEL(SYN_REPORT), + LABEL(SYN_CONFIG), + LABEL(SYN_MT_REPORT), + LABEL(SYN_DROPPED), + LABEL(SYN_MAX), + LABEL_END, + }; + static struct label key_labels[] = { + LABEL(KEY_RESERVED), + LABEL(KEY_ESC), + LABEL(KEY_1), + LABEL(KEY_2), + LABEL(KEY_3), + LABEL(KEY_4), + LABEL(KEY_5), + LABEL(KEY_6), + LABEL(KEY_7), + LABEL(KEY_8), + LABEL(KEY_9), + LABEL(KEY_0), + LABEL(KEY_MINUS), + LABEL(KEY_EQUAL), + LABEL(KEY_BACKSPACE), + LABEL(KEY_TAB), + LABEL(KEY_Q), + LABEL(KEY_W), + LABEL(KEY_E), + LABEL(KEY_R), + LABEL(KEY_T), + LABEL(KEY_Y), + LABEL(KEY_U), + LABEL(KEY_I), + LABEL(KEY_O), + LABEL(KEY_P), + LABEL(KEY_LEFTBRACE), + LABEL(KEY_RIGHTBRACE), + LABEL(KEY_ENTER), + LABEL(KEY_LEFTCTRL), + LABEL(KEY_A), + LABEL(KEY_S), + LABEL(KEY_D), + LABEL(KEY_F), + LABEL(KEY_G), + LABEL(KEY_H), + LABEL(KEY_J), + LABEL(KEY_K), + LABEL(KEY_L), + LABEL(KEY_SEMICOLON), + LABEL(KEY_APOSTROPHE), + LABEL(KEY_GRAVE), + LABEL(KEY_LEFTSHIFT), + LABEL(KEY_BACKSLASH), + LABEL(KEY_Z), + LABEL(KEY_X), + LABEL(KEY_C), + LABEL(KEY_V), + LABEL(KEY_B), + LABEL(KEY_N), + LABEL(KEY_M), + LABEL(KEY_COMMA), + LABEL(KEY_DOT), + LABEL(KEY_SLASH), + LABEL(KEY_RIGHTSHIFT), + LABEL(KEY_KPASTERISK), + LABEL(KEY_LEFTALT), + LABEL(KEY_SPACE), + LABEL(KEY_CAPSLOCK), + LABEL(KEY_F1), + LABEL(KEY_F2), + LABEL(KEY_F3), + LABEL(KEY_F4), + LABEL(KEY_F5), + LABEL(KEY_F6), + LABEL(KEY_F7), + LABEL(KEY_F8), + LABEL(KEY_F9), + LABEL(KEY_F10), + LABEL(KEY_NUMLOCK), + LABEL(KEY_SCROLLLOCK), + LABEL(KEY_KP7), + LABEL(KEY_KP8), + LABEL(KEY_KP9), + LABEL(KEY_KPMINUS), + LABEL(KEY_KP4), + LABEL(KEY_KP5), + LABEL(KEY_KP6), + LABEL(KEY_KPPLUS), + LABEL(KEY_KP1), + LABEL(KEY_KP2), + LABEL(KEY_KP3), + LABEL(KEY_KP0), + LABEL(KEY_KPDOT), + LABEL(KEY_ZENKAKUHANKAKU), + LABEL(KEY_102ND), + LABEL(KEY_F11), + LABEL(KEY_F12), + LABEL(KEY_RO), + LABEL(KEY_KATAKANA), + LABEL(KEY_HIRAGANA), + LABEL(KEY_HENKAN), + LABEL(KEY_KATAKANAHIRAGANA), + LABEL(KEY_MUHENKAN), + LABEL(KEY_KPJPCOMMA), + LABEL(KEY_KPENTER), + LABEL(KEY_RIGHTCTRL), + LABEL(KEY_KPSLASH), + LABEL(KEY_SYSRQ), + LABEL(KEY_RIGHTALT), + LABEL(KEY_LINEFEED), + LABEL(KEY_HOME), + LABEL(KEY_UP), + LABEL(KEY_PAGEUP), + LABEL(KEY_LEFT), + LABEL(KEY_RIGHT), + LABEL(KEY_END), + LABEL(KEY_DOWN), + LABEL(KEY_PAGEDOWN), + LABEL(KEY_INSERT), + LABEL(KEY_DELETE), + LABEL(KEY_MACRO), + LABEL(KEY_MUTE), + LABEL(KEY_VOLUMEDOWN), + LABEL(KEY_VOLUMEUP), + LABEL(KEY_POWER), + LABEL(KEY_KPEQUAL), + LABEL(KEY_KPPLUSMINUS), + LABEL(KEY_PAUSE), + LABEL(KEY_SCALE), + LABEL(KEY_KPCOMMA), + LABEL(KEY_HANGEUL), + LABEL(KEY_HANJA), + LABEL(KEY_YEN), + LABEL(KEY_LEFTMETA), + LABEL(KEY_RIGHTMETA), + LABEL(KEY_COMPOSE), + LABEL(KEY_STOP), + LABEL(KEY_AGAIN), + LABEL(KEY_PROPS), + LABEL(KEY_UNDO), + LABEL(KEY_FRONT), + LABEL(KEY_COPY), + LABEL(KEY_OPEN), + LABEL(KEY_PASTE), + LABEL(KEY_FIND), + LABEL(KEY_CUT), + LABEL(KEY_HELP), + LABEL(KEY_MENU), + LABEL(KEY_CALC), + LABEL(KEY_SETUP), + LABEL(KEY_SLEEP), + LABEL(KEY_WAKEUP), + LABEL(KEY_FILE), + LABEL(KEY_SENDFILE), + LABEL(KEY_DELETEFILE), + LABEL(KEY_XFER), + LABEL(KEY_PROG1), + LABEL(KEY_PROG2), + LABEL(KEY_WWW), + LABEL(KEY_MSDOS), + LABEL(KEY_COFFEE), + LABEL(KEY_ROTATE_DISPLAY), + LABEL(KEY_CYCLEWINDOWS), + LABEL(KEY_MAIL), + LABEL(KEY_BOOKMARKS), + LABEL(KEY_COMPUTER), + LABEL(KEY_BACK), + LABEL(KEY_FORWARD), + LABEL(KEY_CLOSECD), + LABEL(KEY_EJECTCD), + LABEL(KEY_EJECTCLOSECD), + LABEL(KEY_NEXTSONG), + LABEL(KEY_PLAYPAUSE), + LABEL(KEY_PREVIOUSSONG), + LABEL(KEY_STOPCD), + LABEL(KEY_RECORD), + LABEL(KEY_REWIND), + LABEL(KEY_PHONE), + LABEL(KEY_ISO), + LABEL(KEY_CONFIG), + LABEL(KEY_HOMEPAGE), + LABEL(KEY_REFRESH), + LABEL(KEY_EXIT), + LABEL(KEY_MOVE), + LABEL(KEY_EDIT), + LABEL(KEY_SCROLLUP), + LABEL(KEY_SCROLLDOWN), + LABEL(KEY_KPLEFTPAREN), + LABEL(KEY_KPRIGHTPAREN), + LABEL(KEY_NEW), + LABEL(KEY_REDO), + LABEL(KEY_F13), + LABEL(KEY_F14), + LABEL(KEY_F15), + LABEL(KEY_F16), + LABEL(KEY_F17), + LABEL(KEY_F18), + LABEL(KEY_F19), + LABEL(KEY_F20), + LABEL(KEY_F21), + LABEL(KEY_F22), + LABEL(KEY_F23), + LABEL(KEY_F24), + LABEL(KEY_PLAYCD), + LABEL(KEY_PAUSECD), + LABEL(KEY_PROG3), + LABEL(KEY_PROG4), + LABEL(KEY_ALL_APPLICATIONS), + LABEL(KEY_SUSPEND), + LABEL(KEY_CLOSE), + LABEL(KEY_PLAY), + LABEL(KEY_FASTFORWARD), + LABEL(KEY_BASSBOOST), + LABEL(KEY_PRINT), + LABEL(KEY_HP), + LABEL(KEY_CAMERA), + LABEL(KEY_SOUND), + LABEL(KEY_QUESTION), + LABEL(KEY_EMAIL), + LABEL(KEY_CHAT), + LABEL(KEY_SEARCH), + LABEL(KEY_CONNECT), + LABEL(KEY_FINANCE), + LABEL(KEY_SPORT), + LABEL(KEY_SHOP), + LABEL(KEY_ALTERASE), + LABEL(KEY_CANCEL), + LABEL(KEY_BRIGHTNESSDOWN), + LABEL(KEY_BRIGHTNESSUP), + LABEL(KEY_MEDIA), + LABEL(KEY_SWITCHVIDEOMODE), + LABEL(KEY_KBDILLUMTOGGLE), + LABEL(KEY_KBDILLUMDOWN), + LABEL(KEY_KBDILLUMUP), + LABEL(KEY_SEND), + LABEL(KEY_REPLY), + LABEL(KEY_FORWARDMAIL), + LABEL(KEY_SAVE), + LABEL(KEY_DOCUMENTS), + LABEL(KEY_BATTERY), + LABEL(KEY_BLUETOOTH), + LABEL(KEY_WLAN), + LABEL(KEY_UWB), + LABEL(KEY_UNKNOWN), + LABEL(KEY_VIDEO_NEXT), + LABEL(KEY_VIDEO_PREV), + LABEL(KEY_BRIGHTNESS_CYCLE), + LABEL(KEY_BRIGHTNESS_AUTO), + LABEL(KEY_DISPLAY_OFF), + LABEL(KEY_WWAN), + LABEL(KEY_RFKILL), + LABEL(KEY_MICMUTE), + LABEL(BTN_MISC), + LABEL(BTN_0), + LABEL(BTN_1), + LABEL(BTN_2), + LABEL(BTN_3), + LABEL(BTN_4), + LABEL(BTN_5), + LABEL(BTN_6), + LABEL(BTN_7), + LABEL(BTN_8), + LABEL(BTN_9), + LABEL(BTN_MOUSE), + LABEL(BTN_LEFT), + LABEL(BTN_RIGHT), + LABEL(BTN_MIDDLE), + LABEL(BTN_SIDE), + LABEL(BTN_EXTRA), + LABEL(BTN_FORWARD), + LABEL(BTN_BACK), + LABEL(BTN_TASK), + LABEL(BTN_JOYSTICK), + LABEL(BTN_TRIGGER), + LABEL(BTN_THUMB), + LABEL(BTN_THUMB2), + LABEL(BTN_TOP), + LABEL(BTN_TOP2), + LABEL(BTN_PINKIE), + LABEL(BTN_BASE), + LABEL(BTN_BASE2), + LABEL(BTN_BASE3), + LABEL(BTN_BASE4), + LABEL(BTN_BASE5), + LABEL(BTN_BASE6), + LABEL(BTN_DEAD), + LABEL(BTN_GAMEPAD), + LABEL(BTN_SOUTH), + LABEL(BTN_EAST), + LABEL(BTN_C), + LABEL(BTN_NORTH), + LABEL(BTN_WEST), + LABEL(BTN_Z), + LABEL(BTN_TL), + LABEL(BTN_TR), + LABEL(BTN_TL2), + LABEL(BTN_TR2), + LABEL(BTN_SELECT), + LABEL(BTN_START), + LABEL(BTN_MODE), + LABEL(BTN_THUMBL), + LABEL(BTN_THUMBR), + LABEL(BTN_DIGI), + LABEL(BTN_TOOL_PEN), + LABEL(BTN_TOOL_RUBBER), + LABEL(BTN_TOOL_BRUSH), + LABEL(BTN_TOOL_PENCIL), + LABEL(BTN_TOOL_AIRBRUSH), + LABEL(BTN_TOOL_FINGER), + LABEL(BTN_TOOL_MOUSE), + LABEL(BTN_TOOL_LENS), + LABEL(BTN_TOOL_QUINTTAP), + LABEL(BTN_STYLUS3), + LABEL(BTN_TOUCH), + LABEL(BTN_STYLUS), + LABEL(BTN_STYLUS2), + LABEL(BTN_TOOL_DOUBLETAP), + LABEL(BTN_TOOL_TRIPLETAP), + LABEL(BTN_TOOL_QUADTAP), + LABEL(BTN_WHEEL), + LABEL(BTN_GEAR_DOWN), + LABEL(BTN_GEAR_UP), + LABEL(KEY_OK), + LABEL(KEY_SELECT), + LABEL(KEY_GOTO), + LABEL(KEY_CLEAR), + LABEL(KEY_POWER2), + LABEL(KEY_OPTION), + LABEL(KEY_INFO), + LABEL(KEY_TIME), + LABEL(KEY_VENDOR), + LABEL(KEY_ARCHIVE), + LABEL(KEY_PROGRAM), + LABEL(KEY_CHANNEL), + LABEL(KEY_FAVORITES), + LABEL(KEY_EPG), + LABEL(KEY_PVR), + LABEL(KEY_MHP), + LABEL(KEY_LANGUAGE), + LABEL(KEY_TITLE), + LABEL(KEY_SUBTITLE), + LABEL(KEY_ANGLE), + LABEL(KEY_FULL_SCREEN), + LABEL(KEY_MODE), + LABEL(KEY_KEYBOARD), + LABEL(KEY_ASPECT_RATIO), + LABEL(KEY_PC), + LABEL(KEY_TV), + LABEL(KEY_TV2), + LABEL(KEY_VCR), + LABEL(KEY_VCR2), + LABEL(KEY_SAT), + LABEL(KEY_SAT2), + LABEL(KEY_CD), + LABEL(KEY_TAPE), + LABEL(KEY_RADIO), + LABEL(KEY_TUNER), + LABEL(KEY_PLAYER), + LABEL(KEY_TEXT), + LABEL(KEY_DVD), + LABEL(KEY_AUX), + LABEL(KEY_MP3), + LABEL(KEY_AUDIO), + LABEL(KEY_VIDEO), + LABEL(KEY_DIRECTORY), + LABEL(KEY_LIST), + LABEL(KEY_MEMO), + LABEL(KEY_CALENDAR), + LABEL(KEY_RED), + LABEL(KEY_GREEN), + LABEL(KEY_YELLOW), + LABEL(KEY_BLUE), + LABEL(KEY_CHANNELUP), + LABEL(KEY_CHANNELDOWN), + LABEL(KEY_FIRST), + LABEL(KEY_LAST), + LABEL(KEY_AB), + LABEL(KEY_NEXT), + LABEL(KEY_RESTART), + LABEL(KEY_SLOW), + LABEL(KEY_SHUFFLE), + LABEL(KEY_BREAK), + LABEL(KEY_PREVIOUS), + LABEL(KEY_DIGITS), + LABEL(KEY_TEEN), + LABEL(KEY_TWEN), + LABEL(KEY_VIDEOPHONE), + LABEL(KEY_GAMES), + LABEL(KEY_ZOOMIN), + LABEL(KEY_ZOOMOUT), + LABEL(KEY_ZOOMRESET), + LABEL(KEY_WORDPROCESSOR), + LABEL(KEY_EDITOR), + LABEL(KEY_SPREADSHEET), + LABEL(KEY_GRAPHICSEDITOR), + LABEL(KEY_PRESENTATION), + LABEL(KEY_DATABASE), + LABEL(KEY_NEWS), + LABEL(KEY_VOICEMAIL), + LABEL(KEY_ADDRESSBOOK), + LABEL(KEY_MESSENGER), + LABEL(KEY_DISPLAYTOGGLE), + LABEL(KEY_SPELLCHECK), + LABEL(KEY_LOGOFF), + LABEL(KEY_DOLLAR), + LABEL(KEY_EURO), + LABEL(KEY_FRAMEBACK), + LABEL(KEY_FRAMEFORWARD), + LABEL(KEY_CONTEXT_MENU), + LABEL(KEY_MEDIA_REPEAT), + LABEL(KEY_10CHANNELSUP), + LABEL(KEY_10CHANNELSDOWN), + LABEL(KEY_IMAGES), + LABEL(KEY_NOTIFICATION_CENTER), + LABEL(KEY_PICKUP_PHONE), + LABEL(KEY_HANGUP_PHONE), + LABEL(KEY_DEL_EOL), + LABEL(KEY_DEL_EOS), + LABEL(KEY_INS_LINE), + LABEL(KEY_DEL_LINE), + LABEL(KEY_FN), + LABEL(KEY_FN_ESC), + LABEL(KEY_FN_F1), + LABEL(KEY_FN_F2), + LABEL(KEY_FN_F3), + LABEL(KEY_FN_F4), + LABEL(KEY_FN_F5), + LABEL(KEY_FN_F6), + LABEL(KEY_FN_F7), + LABEL(KEY_FN_F8), + LABEL(KEY_FN_F9), + LABEL(KEY_FN_F10), + LABEL(KEY_FN_F11), + LABEL(KEY_FN_F12), + LABEL(KEY_FN_1), + LABEL(KEY_FN_2), + LABEL(KEY_FN_D), + LABEL(KEY_FN_E), + LABEL(KEY_FN_F), + LABEL(KEY_FN_S), + LABEL(KEY_FN_B), + LABEL(KEY_FN_RIGHT_SHIFT), + LABEL(KEY_BRL_DOT1), + LABEL(KEY_BRL_DOT2), + LABEL(KEY_BRL_DOT3), + LABEL(KEY_BRL_DOT4), + LABEL(KEY_BRL_DOT5), + LABEL(KEY_BRL_DOT6), + LABEL(KEY_BRL_DOT7), + LABEL(KEY_BRL_DOT8), + LABEL(KEY_BRL_DOT9), + LABEL(KEY_BRL_DOT10), + LABEL(KEY_NUMERIC_0), + LABEL(KEY_NUMERIC_1), + LABEL(KEY_NUMERIC_2), + LABEL(KEY_NUMERIC_3), + LABEL(KEY_NUMERIC_4), + LABEL(KEY_NUMERIC_5), + LABEL(KEY_NUMERIC_6), + LABEL(KEY_NUMERIC_7), + LABEL(KEY_NUMERIC_8), + LABEL(KEY_NUMERIC_9), + LABEL(KEY_NUMERIC_STAR), + LABEL(KEY_NUMERIC_POUND), + LABEL(KEY_NUMERIC_A), + LABEL(KEY_NUMERIC_B), + LABEL(KEY_NUMERIC_C), + LABEL(KEY_NUMERIC_D), + LABEL(KEY_CAMERA_FOCUS), + LABEL(KEY_WPS_BUTTON), + LABEL(KEY_TOUCHPAD_TOGGLE), + LABEL(KEY_TOUCHPAD_ON), + LABEL(KEY_TOUCHPAD_OFF), + LABEL(KEY_CAMERA_ZOOMIN), + LABEL(KEY_CAMERA_ZOOMOUT), + LABEL(KEY_CAMERA_UP), + LABEL(KEY_CAMERA_DOWN), + LABEL(KEY_CAMERA_LEFT), + LABEL(KEY_CAMERA_RIGHT), + LABEL(KEY_ATTENDANT_ON), + LABEL(KEY_ATTENDANT_OFF), + LABEL(KEY_ATTENDANT_TOGGLE), + LABEL(KEY_LIGHTS_TOGGLE), + LABEL(BTN_DPAD_UP), + LABEL(BTN_DPAD_DOWN), + LABEL(BTN_DPAD_LEFT), + LABEL(BTN_DPAD_RIGHT), + LABEL(KEY_ALS_TOGGLE), + LABEL(KEY_ROTATE_LOCK_TOGGLE), +// LABEL(KEY_REFRESH_RATE_TOGGLE), + LABEL(KEY_BUTTONCONFIG), + LABEL(KEY_TASKMANAGER), + LABEL(KEY_JOURNAL), + LABEL(KEY_CONTROLPANEL), + LABEL(KEY_APPSELECT), + LABEL(KEY_SCREENSAVER), + LABEL(KEY_VOICECOMMAND), + LABEL(KEY_ASSISTANT), + LABEL(KEY_KBD_LAYOUT_NEXT), + LABEL(KEY_EMOJI_PICKER), + LABEL(KEY_DICTATE), + LABEL(KEY_CAMERA_ACCESS_ENABLE), + LABEL(KEY_CAMERA_ACCESS_DISABLE), + LABEL(KEY_CAMERA_ACCESS_TOGGLE), +// LABEL(KEY_ACCESSIBILITY), +// LABEL(KEY_DO_NOT_DISTURB), + LABEL(KEY_BRIGHTNESS_MIN), + LABEL(KEY_BRIGHTNESS_MAX), + LABEL(KEY_KBDINPUTASSIST_PREV), + LABEL(KEY_KBDINPUTASSIST_NEXT), + LABEL(KEY_KBDINPUTASSIST_PREVGROUP), + LABEL(KEY_KBDINPUTASSIST_NEXTGROUP), + LABEL(KEY_KBDINPUTASSIST_ACCEPT), + LABEL(KEY_KBDINPUTASSIST_CANCEL), + LABEL(KEY_RIGHT_UP), + LABEL(KEY_RIGHT_DOWN), + LABEL(KEY_LEFT_UP), + LABEL(KEY_LEFT_DOWN), + LABEL(KEY_ROOT_MENU), + LABEL(KEY_MEDIA_TOP_MENU), + LABEL(KEY_NUMERIC_11), + LABEL(KEY_NUMERIC_12), + LABEL(KEY_AUDIO_DESC), + LABEL(KEY_3D_MODE), + LABEL(KEY_NEXT_FAVORITE), + LABEL(KEY_STOP_RECORD), + LABEL(KEY_PAUSE_RECORD), + LABEL(KEY_VOD), + LABEL(KEY_UNMUTE), + LABEL(KEY_FASTREVERSE), + LABEL(KEY_SLOWREVERSE), + LABEL(KEY_DATA), + LABEL(KEY_ONSCREEN_KEYBOARD), + LABEL(KEY_PRIVACY_SCREEN_TOGGLE), + LABEL(KEY_SELECTIVE_SCREENSHOT), + LABEL(KEY_NEXT_ELEMENT), + LABEL(KEY_PREVIOUS_ELEMENT), + LABEL(KEY_AUTOPILOT_ENGAGE_TOGGLE), + LABEL(KEY_MARK_WAYPOINT), + LABEL(KEY_SOS), + LABEL(KEY_NAV_CHART), + LABEL(KEY_FISHING_CHART), + LABEL(KEY_SINGLE_RANGE_RADAR), + LABEL(KEY_DUAL_RANGE_RADAR), + LABEL(KEY_RADAR_OVERLAY), + LABEL(KEY_TRADITIONAL_SONAR), + LABEL(KEY_CLEARVU_SONAR), + LABEL(KEY_SIDEVU_SONAR), + LABEL(KEY_NAV_INFO), + LABEL(KEY_BRIGHTNESS_MENU), + LABEL(KEY_MACRO1), + LABEL(KEY_MACRO2), + LABEL(KEY_MACRO3), + LABEL(KEY_MACRO4), + LABEL(KEY_MACRO5), + LABEL(KEY_MACRO6), + LABEL(KEY_MACRO7), + LABEL(KEY_MACRO8), + LABEL(KEY_MACRO9), + LABEL(KEY_MACRO10), + LABEL(KEY_MACRO11), + LABEL(KEY_MACRO12), + LABEL(KEY_MACRO13), + LABEL(KEY_MACRO14), + LABEL(KEY_MACRO15), + LABEL(KEY_MACRO16), + LABEL(KEY_MACRO17), + LABEL(KEY_MACRO18), + LABEL(KEY_MACRO19), + LABEL(KEY_MACRO20), + LABEL(KEY_MACRO21), + LABEL(KEY_MACRO22), + LABEL(KEY_MACRO23), + LABEL(KEY_MACRO24), + LABEL(KEY_MACRO25), + LABEL(KEY_MACRO26), + LABEL(KEY_MACRO27), + LABEL(KEY_MACRO28), + LABEL(KEY_MACRO29), + LABEL(KEY_MACRO30), + LABEL(KEY_MACRO_RECORD_START), + LABEL(KEY_MACRO_RECORD_STOP), + LABEL(KEY_MACRO_PRESET_CYCLE), + LABEL(KEY_MACRO_PRESET1), + LABEL(KEY_MACRO_PRESET2), + LABEL(KEY_MACRO_PRESET3), + LABEL(KEY_KBD_LCD_MENU1), + LABEL(KEY_KBD_LCD_MENU2), + LABEL(KEY_KBD_LCD_MENU3), + LABEL(KEY_KBD_LCD_MENU4), + LABEL(KEY_KBD_LCD_MENU5), + LABEL(BTN_TRIGGER_HAPPY), + LABEL(BTN_TRIGGER_HAPPY1), + LABEL(BTN_TRIGGER_HAPPY2), + LABEL(BTN_TRIGGER_HAPPY3), + LABEL(BTN_TRIGGER_HAPPY4), + LABEL(BTN_TRIGGER_HAPPY5), + LABEL(BTN_TRIGGER_HAPPY6), + LABEL(BTN_TRIGGER_HAPPY7), + LABEL(BTN_TRIGGER_HAPPY8), + LABEL(BTN_TRIGGER_HAPPY9), + LABEL(BTN_TRIGGER_HAPPY10), + LABEL(BTN_TRIGGER_HAPPY11), + LABEL(BTN_TRIGGER_HAPPY12), + LABEL(BTN_TRIGGER_HAPPY13), + LABEL(BTN_TRIGGER_HAPPY14), + LABEL(BTN_TRIGGER_HAPPY15), + LABEL(BTN_TRIGGER_HAPPY16), + LABEL(BTN_TRIGGER_HAPPY17), + LABEL(BTN_TRIGGER_HAPPY18), + LABEL(BTN_TRIGGER_HAPPY19), + LABEL(BTN_TRIGGER_HAPPY20), + LABEL(BTN_TRIGGER_HAPPY21), + LABEL(BTN_TRIGGER_HAPPY22), + LABEL(BTN_TRIGGER_HAPPY23), + LABEL(BTN_TRIGGER_HAPPY24), + LABEL(BTN_TRIGGER_HAPPY25), + LABEL(BTN_TRIGGER_HAPPY26), + LABEL(BTN_TRIGGER_HAPPY27), + LABEL(BTN_TRIGGER_HAPPY28), + LABEL(BTN_TRIGGER_HAPPY29), + LABEL(BTN_TRIGGER_HAPPY30), + LABEL(BTN_TRIGGER_HAPPY31), + LABEL(BTN_TRIGGER_HAPPY32), + LABEL(BTN_TRIGGER_HAPPY33), + LABEL(BTN_TRIGGER_HAPPY34), + LABEL(BTN_TRIGGER_HAPPY35), + LABEL(BTN_TRIGGER_HAPPY36), + LABEL(BTN_TRIGGER_HAPPY37), + LABEL(BTN_TRIGGER_HAPPY38), + LABEL(BTN_TRIGGER_HAPPY39), + LABEL(BTN_TRIGGER_HAPPY40), + LABEL(KEY_MAX), + LABEL_END, + }; + static struct label rel_labels[] = { + LABEL(REL_X), + LABEL(REL_Y), + LABEL(REL_Z), + LABEL(REL_RX), + LABEL(REL_RY), + LABEL(REL_RZ), + LABEL(REL_HWHEEL), + LABEL(REL_DIAL), + LABEL(REL_WHEEL), + LABEL(REL_MISC), + LABEL(REL_RESERVED), + LABEL(REL_WHEEL_HI_RES), + LABEL(REL_HWHEEL_HI_RES), + LABEL(REL_MAX), + LABEL_END, + }; + static struct label abs_labels[] = { + LABEL(ABS_X), + LABEL(ABS_Y), + LABEL(ABS_Z), + LABEL(ABS_RX), + LABEL(ABS_RY), + LABEL(ABS_RZ), + LABEL(ABS_THROTTLE), + LABEL(ABS_RUDDER), + LABEL(ABS_WHEEL), + LABEL(ABS_GAS), + LABEL(ABS_BRAKE), + LABEL(ABS_HAT0X), + LABEL(ABS_HAT0Y), + LABEL(ABS_HAT1X), + LABEL(ABS_HAT1Y), + LABEL(ABS_HAT2X), + LABEL(ABS_HAT2Y), + LABEL(ABS_HAT3X), + LABEL(ABS_HAT3Y), + LABEL(ABS_PRESSURE), + LABEL(ABS_DISTANCE), + LABEL(ABS_TILT_X), + LABEL(ABS_TILT_Y), + LABEL(ABS_TOOL_WIDTH), + LABEL(ABS_VOLUME), + LABEL(ABS_PROFILE), + LABEL(ABS_MISC), + LABEL(ABS_RESERVED), + LABEL(ABS_MT_SLOT), + LABEL(ABS_MT_TOUCH_MAJOR), + LABEL(ABS_MT_TOUCH_MINOR), + LABEL(ABS_MT_WIDTH_MAJOR), + LABEL(ABS_MT_WIDTH_MINOR), + LABEL(ABS_MT_ORIENTATION), + LABEL(ABS_MT_POSITION_X), + LABEL(ABS_MT_POSITION_Y), + LABEL(ABS_MT_TOOL_TYPE), + LABEL(ABS_MT_BLOB_ID), + LABEL(ABS_MT_TRACKING_ID), + LABEL(ABS_MT_PRESSURE), + LABEL(ABS_MT_DISTANCE), + LABEL(ABS_MT_TOOL_X), + LABEL(ABS_MT_TOOL_Y), + LABEL(ABS_MAX), + LABEL_END, + }; + static struct label sw_labels[] = { + LABEL(SW_LID), + LABEL(SW_TABLET_MODE), + LABEL(SW_HEADPHONE_INSERT), + LABEL(SW_RFKILL_ALL), + LABEL(SW_MICROPHONE_INSERT), + LABEL(SW_DOCK), + LABEL(SW_LINEOUT_INSERT), + LABEL(SW_JACK_PHYSICAL_INSERT), + LABEL(SW_VIDEOOUT_INSERT), + LABEL(SW_CAMERA_LENS_COVER), + LABEL(SW_KEYPAD_SLIDE), + LABEL(SW_FRONT_PROXIMITY), + LABEL(SW_ROTATE_LOCK), + LABEL(SW_LINEIN_INSERT), + LABEL(SW_MUTE_DEVICE), + LABEL(SW_PEN_INSERTED), + LABEL(SW_MACHINE_COVER), + LABEL(SW_MAX), + LABEL_END, + }; + static struct label msc_labels[] = { + LABEL(MSC_SERIAL), + LABEL(MSC_PULSELED), + LABEL(MSC_GESTURE), + LABEL(MSC_RAW), + LABEL(MSC_SCAN), + LABEL(MSC_TIMESTAMP), + LABEL(MSC_MAX), + LABEL_END, + }; + static struct label led_labels[] = { + LABEL(LED_NUML), + LABEL(LED_CAPSL), + LABEL(LED_SCROLLL), + LABEL(LED_COMPOSE), + LABEL(LED_KANA), + LABEL(LED_SLEEP), + LABEL(LED_SUSPEND), + LABEL(LED_MUTE), + LABEL(LED_MISC), + LABEL(LED_MAIL), + LABEL(LED_CHARGING), + LABEL(LED_MAX), + LABEL_END, + }; + static struct label rep_labels[] = { + LABEL(REP_DELAY), + LABEL(REP_PERIOD), + LABEL(REP_MAX), + LABEL_END, + }; + static struct label snd_labels[] = { + LABEL(SND_CLICK), + LABEL(SND_BELL), + LABEL(SND_TONE), + LABEL(SND_MAX), + LABEL_END, + }; + static struct label mt_tool_labels[] = { + LABEL(MT_TOOL_FINGER), + LABEL(MT_TOOL_PEN), + LABEL(MT_TOOL_PALM), + LABEL(MT_TOOL_DIAL), + LABEL(MT_TOOL_MAX), + LABEL_END, + }; + static struct label ff_status_labels[] = { + LABEL(FF_STATUS_STOPPED), + LABEL(FF_STATUS_PLAYING), + LABEL(FF_STATUS_MAX), + LABEL_END, + }; + static struct label ff_labels[] = { + LABEL(FF_RUMBLE), + LABEL(FF_PERIODIC), + LABEL(FF_CONSTANT), + LABEL(FF_SPRING), + LABEL(FF_FRICTION), + LABEL(FF_DAMPER), + LABEL(FF_INERTIA), + LABEL(FF_RAMP), + LABEL(FF_SQUARE), + LABEL(FF_TRIANGLE), + LABEL(FF_SINE), + LABEL(FF_SAW_UP), + LABEL(FF_SAW_DOWN), + LABEL(FF_CUSTOM), + LABEL(FF_GAIN), + LABEL(FF_AUTOCENTER), + LABEL(FF_MAX), + LABEL_END, + }; + +#undef LABEL +#undef LABEL_END + + std::string getLabel(const label *labels, int value) { + if (labels == nullptr) return std::to_string(value); + while (labels->name != nullptr && value != labels->value) { + labels++; + } + return labels->name != nullptr ? labels->name : std::to_string(value); + } + + std::optional getValue(const label *labels, const char *searchLabel) { + if (labels == nullptr) return {}; + while (labels->name != nullptr && ::strcasecmp(labels->name, searchLabel) != 0) { + labels++; + } + return labels->name != nullptr ? std::make_optional(labels->value) : std::nullopt; + } + + const label *getCodeLabelsForType(int32_t type) { + switch (type) { + case EV_SYN: + return syn_labels; + case EV_KEY: + return key_labels; + case EV_REL: + return rel_labels; + case EV_ABS: + return abs_labels; + case EV_SW: + return sw_labels; + case EV_MSC: + return msc_labels; + case EV_LED: + return led_labels; + case EV_REP: + return rep_labels; + case EV_SND: + return snd_labels; + case EV_FF: + return ff_labels; + case EV_FF_STATUS: + return ff_status_labels; + default: + return nullptr; + } + } + + const label *getValueLabelsForTypeAndCode(int32_t type, int32_t code) { + if (type == EV_KEY) { + return ev_key_value_labels; + } + if (type == EV_ABS && code == ABS_MT_TOOL_TYPE) { + return mt_tool_labels; + } + return nullptr; + } + + } // namespace + + EvdevEventLabel + InputEventLookup::getLinuxEvdevLabel(int32_t type, int32_t code, int32_t value) { + return { + .type = getLabel(ev_labels, type), + .code = getLabel(getCodeLabelsForType(type), code), + .value = getLabel(getValueLabelsForTypeAndCode(type, code), value), + }; + } + + std::optional InputEventLookup::getLinuxEvdevEventTypeByLabel(const char *label) { + return getValue(ev_labels, label); + } + + std::optional InputEventLookup::getLinuxEvdevEventCodeByLabel(int32_t type, + const char *label) { + return getValue(getCodeLabelsForType(type), label); + } + + std::optional InputEventLookup::getLinuxEvdevInputPropByLabel(const char *label) { + return getValue(input_prop_labels, label); + } + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/input/InputEventLabels.h b/sysbridge/src/main/cpp/android/input/InputEventLabels.h new file mode 100644 index 0000000000..066ff06ca7 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/InputEventLabels.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace android { + + template + size_t size(T (&)[N]) { return N; } + + struct InputEventLabel { + const char *literal; + int value; + }; + + struct EvdevEventLabel { + std::string type; + std::string code; + std::string value; + }; + +// NOTE: If you want a new key code, axis code, led code or flag code in keylayout file, +// then you must add it to InputEventLabels.cpp. + + class InputEventLookup { + /** + * This class is not purely static, but uses a singleton pattern in order to delay the + * initialization of the maps that it contains. If it were purely static, the maps could be + * created early, and would cause sanitizers to report memory leaks. + */ + public: + InputEventLookup(InputEventLookup& other) = delete; + + void operator=(const InputEventLookup&) = delete; + + static std::optional lookupValueByLabel(const std::unordered_map& map, + const char* literal); + + static const char* lookupLabelByValue(const std::vector& vec, int value); + + static std::optional getKeyCodeByLabel(const char* label); + + static const char* getLabelByKeyCode(int32_t keyCode); + + static std::optional getKeyFlagByLabel(const char* label); + + static std::optional getAxisByLabel(const char* label); + + static const char* getAxisLabel(int32_t axisId); + + static std::optional getLedByLabel(const char* label); + + static EvdevEventLabel getLinuxEvdevLabel(int32_t type, int32_t code, int32_t value); + + static std::optional getLinuxEvdevEventTypeByLabel(const char* label); + + static std::optional getLinuxEvdevEventCodeByLabel(int32_t type, const char* label); + + static std::optional getLinuxEvdevInputPropByLabel(const char* label); + + private: + InputEventLookup(); + + static const InputEventLookup& get() { + static InputEventLookup sLookup; + return sLookup; + } + + const std::unordered_map KEYCODES; + + const std::vector KEY_NAMES; + + const std::unordered_map AXES; + + const std::vector AXES_NAMES; + + const std::unordered_map LEDS; + + const std::unordered_map FLAGS; + }; + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp new file mode 100644 index 0000000000..56468309f2 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, Tokenizer.cppeither express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "KeyLayoutMap" + +#include +#include +#include "../utils/String8.h" +#include "KeyLayoutMap.h" +#include "../utils/Tokenizer.h" +#include "InputEventLabels.h" +#include + +#include +#include +#include +#include "../libbase/result.h" + +/** + * Log debug output for the parser. + * Enable this via "adb shell setprop log.tag.KeyLayoutMapParser DEBUG" (requires restart) + */ +const bool DEBUG_PARSER = + __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "Parser", ANDROID_LOG_INFO); + +// Enables debug output for parser performance. +#define DEBUG_PARSER_PERFORMANCE 0 + +/** + * Log debug output for mapping. + * Enable this via "adb shell setprop log.tag.KeyLayoutMapMapping DEBUG" (requires restart) + */ +const bool DEBUG_MAPPING = + __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "Mapping", ANDROID_LOG_INFO); + +namespace android { + namespace { + + std::optional parseInt(const char *str) { + char *end; + errno = 0; + const int value = strtol(str, &end, 0); + if (end == str) { + LOG(ERROR) << "Could not parse " << str; + return {}; + } + if (errno == ERANGE) { + LOG(ERROR) << "Out of bounds: " << str; + return {}; + } + return value; + } + + constexpr const char *WHITESPACE = " \t\r"; + + } // namespace + + KeyLayoutMap::KeyLayoutMap() = default; + + KeyLayoutMap::~KeyLayoutMap() = default; + + base::Result> + KeyLayoutMap::loadContents(const std::string &filename, + const char *contents) { + return load(filename, contents); + } + + base::Result> KeyLayoutMap::load(const std::string &filename, + const char *contents) { + Tokenizer *tokenizer; + status_t status; + if (contents == nullptr) { + status = Tokenizer::open(String8(filename.c_str()), &tokenizer); + } else { + status = Tokenizer::fromContents(String8(filename.c_str()), contents, &tokenizer); + } + if (status) { + __android_log_print(ANDROID_LOG_ERROR, "Error %d opening key layout map file %s.", + status, filename.c_str()); + return Errorf("Error {} opening key layout map file {}.", status, filename.c_str()); + } + std::unique_ptr t(tokenizer); + auto ret = load(t.get()); + if (!ret.ok()) { + return ret; + } + const std::shared_ptr &map = *ret; + LOG_ALWAYS_FATAL_IF(map == nullptr, "Returned map should not be null if there's no error"); + + map->mLoadFileName = filename; + return ret; + } + + base::Result> KeyLayoutMap::load(Tokenizer *tokenizer) { + std::shared_ptr map = std::shared_ptr(new KeyLayoutMap()); + status_t status = OK; + if (!map.get()) { + __android_log_print(ANDROID_LOG_ERROR, "Error allocating key layout map."); + return Errorf("Error allocating key layout map."); + } else { +#if DEBUG_PARSER_PERFORMANCE + nsecs_t startTime = systemTime(SYSTEM_TIME_MONOTONIC); +#endif + Parser parser(map.get(), tokenizer); + status = parser.parse(); +#if DEBUG_PARSER_PERFORMANCE + nsecs_t elapsedTime = systemTime(SYSTEM_TIME_MONOTONIC) - startTime; + ALOGD("Parsed key layout map file '%s' %d lines in %0.3fms.", + tokenizer->getFilename().c_str(), tokenizer->getLineNumber(), + elapsedTime / 1000000.0); +#endif + if (!status) { + return std::move(map); + } + } + return Errorf("Load KeyLayoutMap failed {}.", status); + } + + status_t KeyLayoutMap::mapKey(int32_t scanCode, int32_t usageCode, + int32_t *outKeyCode, uint32_t *outFlags) const { + const Key *key = getKey(scanCode, usageCode); + if (!key) { + ALOGD_IF(DEBUG_MAPPING, "mapKey: scanCode=%d, usageCode=0x%08x ~ Failed.", scanCode, + usageCode); + *outKeyCode = AKEYCODE_UNKNOWN; + *outFlags = 0; + return NAME_NOT_FOUND; + } + + *outKeyCode = key->keyCode; + *outFlags = key->flags; + + ALOGD_IF(DEBUG_MAPPING, + "mapKey: scanCode=%d, usageCode=0x%08x ~ Result keyCode=%d, outFlags=0x%08x.", + scanCode, usageCode, *outKeyCode, *outFlags); + return NO_ERROR; + } + + const KeyLayoutMap::Key *KeyLayoutMap::getKey(int32_t scanCode, int32_t usageCode) const { + if (usageCode) { + auto it = mKeysByUsageCode.find(usageCode); + if (it != mKeysByUsageCode.end()) { + return &it->second; + } + } + if (scanCode) { + auto it = mKeysByScanCode.find(scanCode); + if (it != mKeysByScanCode.end()) { + return &it->second; + } + } + return nullptr; + } + + std::vector KeyLayoutMap::findScanCodesForKey(int32_t keyCode) const { + std::vector scanCodes; + // b/354333072: Only consider keys without FUNCTION flag + for (const auto &[scanCode, key]: mKeysByScanCode) { + if (keyCode == key.keyCode && !(key.flags & POLICY_FLAG_FUNCTION)) { + scanCodes.push_back(scanCode); + } + } + return scanCodes; + } + + std::vector KeyLayoutMap::findUsageCodesForKey(int32_t keyCode) const { + std::vector usageCodes; + for (const auto &[usageCode, key]: mKeysByUsageCode) { + if (keyCode == key.keyCode && !(key.flags & POLICY_FLAG_FALLBACK_USAGE_MAPPING)) { + usageCodes.push_back(usageCode); + } + } + return usageCodes; + } + + std::optional KeyLayoutMap::mapAxis(int32_t scanCode) const { + auto it = mAxes.find(scanCode); + if (it == mAxes.end()) { + ALOGD_IF(DEBUG_MAPPING, "mapAxis: scanCode=%d ~ Failed.", scanCode); + return std::nullopt; + } + + const AxisInfo &axisInfo = it->second; + ALOGD_IF(DEBUG_MAPPING, + "mapAxis: scanCode=%d ~ Result mode=%d, axis=%d, highAxis=%d, " + "splitValue=%d, flatOverride=%d.", + scanCode, axisInfo.mode, axisInfo.axis, axisInfo.highAxis, axisInfo.splitValue, + axisInfo.flatOverride); + return axisInfo; + } + +// --- KeyLayoutMap::Parser --- + + KeyLayoutMap::Parser::Parser(KeyLayoutMap *map, Tokenizer *tokenizer) : + mMap(map), mTokenizer(tokenizer) { + } + + KeyLayoutMap::Parser::~Parser() { + } + + status_t KeyLayoutMap::Parser::parse() { + while (!mTokenizer->isEof()) { + ALOGD_IF(DEBUG_PARSER, "Parsing %s: '%s'.", mTokenizer->getLocation().c_str(), + mTokenizer->peekRemainderOfLine().c_str()); + + mTokenizer->skipDelimiters(WHITESPACE); + + if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') { + String8 keywordToken = mTokenizer->nextToken(WHITESPACE); + if (keywordToken == "key") { + mTokenizer->skipDelimiters(WHITESPACE); + status_t status = parseKey(); + if (status) return status; + } else if (keywordToken == "axis") { + mTokenizer->skipDelimiters(WHITESPACE); + status_t status = parseAxis(); + if (status) return status; +// } else if (keywordToken == "led") { +// mTokenizer->skipDelimiters(WHITESPACE); +// status_t status = parseLed(); +// if (status) return status; +// } else if (keywordToken == "sensor") { +// mTokenizer->skipDelimiters(WHITESPACE); +// status_t status = parseSensor(); +// if (status) return status; + } else if (keywordToken == "requires_kernel_config") { + mTokenizer->skipDelimiters(WHITESPACE); + status_t status = parseRequiredKernelConfig(); + if (status) return status; + } else { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected keyword, got '%s'.", + mTokenizer->getLocation().c_str(), + keywordToken.c_str()); + return BAD_VALUE; + } + + mTokenizer->skipDelimiters(WHITESPACE); + if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') { + __android_log_print(ANDROID_LOG_ERROR, + "%s: Expected end of line or trailing comment, got '%s'.", + mTokenizer->getLocation().c_str(), + mTokenizer->peekRemainderOfLine().c_str()); + return BAD_VALUE; + } + } + + mTokenizer->nextLine(); + } + return NO_ERROR; + } + + status_t KeyLayoutMap::Parser::parseKey() { + String8 codeToken = mTokenizer->nextToken(WHITESPACE); + bool mapUsage = false; + if (codeToken == "usage") { + mapUsage = true; + mTokenizer->skipDelimiters(WHITESPACE); + codeToken = mTokenizer->nextToken(WHITESPACE); + } + + std::optional code = parseInt(codeToken.c_str()); + if (!code) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected key %s number, got '%s'.", + mTokenizer->getLocation().c_str(), + mapUsage ? "usage" : "scan code", codeToken.c_str()); + return BAD_VALUE; + } + std::unordered_map &map = + mapUsage ? mMap->mKeysByUsageCode : mMap->mKeysByScanCode; + if (map.find(*code) != map.end()) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Duplicate entry for key %s '%s'.", + mTokenizer->getLocation().c_str(), + mapUsage ? "usage" : "scan code", codeToken.c_str()); + return BAD_VALUE; + } + + mTokenizer->skipDelimiters(WHITESPACE); + String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE); + std::optional keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.c_str()); + if (!keyCode) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected key code label, got '%s'.", + mTokenizer->getLocation().c_str(), + keyCodeToken.c_str()); + return BAD_VALUE; + } + + uint32_t flags = 0; + for (;;) { + mTokenizer->skipDelimiters(WHITESPACE); + if (mTokenizer->isEol() || mTokenizer->peekChar() == '#') break; + + String8 flagToken = mTokenizer->nextToken(WHITESPACE); + std::optional flag = InputEventLookup::getKeyFlagByLabel(flagToken.c_str()); + if (!flag) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected key flag label, got '%s'.", + mTokenizer->getLocation().c_str(), + flagToken.c_str()); + return BAD_VALUE; + } + if (flags & *flag) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Duplicate key flag '%s'.", + mTokenizer->getLocation().c_str(), + flagToken.c_str()); + return BAD_VALUE; + } + flags |= *flag; + } + + ALOGD_IF(DEBUG_PARSER, "Parsed key %s: code=%d, keyCode=%d, flags=0x%08x.", + mapUsage ? "usage" : "scan code", *code, *keyCode, flags); + + Key key; + key.keyCode = *keyCode; + key.flags = flags; + map.insert({*code, key}); + return NO_ERROR; + } + + status_t KeyLayoutMap::Parser::parseAxis() { + String8 scanCodeToken = mTokenizer->nextToken(WHITESPACE); + std::optional scanCode = parseInt(scanCodeToken.c_str()); + if (!scanCode) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected axis scan code number, got '%s'.", + mTokenizer->getLocation().c_str(), + scanCodeToken.c_str()); + return BAD_VALUE; + } + if (mMap->mAxes.find(*scanCode) != mMap->mAxes.end()) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Duplicate entry for axis scan code '%s'.", + mTokenizer->getLocation().c_str(), + scanCodeToken.c_str()); + return BAD_VALUE; + } + + AxisInfo axisInfo; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 token = mTokenizer->nextToken(WHITESPACE); + if (token == "invert") { + axisInfo.mode = AxisInfo::MODE_INVERT; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 axisToken = mTokenizer->nextToken(WHITESPACE); + std::optional axis = InputEventLookup::getAxisByLabel(axisToken.c_str()); + if (!axis) { + __android_log_print(ANDROID_LOG_ERROR, + "%s: Expected inverted axis label, got '%s'.", + mTokenizer->getLocation().c_str(), axisToken.c_str()); + return BAD_VALUE; + } + axisInfo.axis = *axis; + } else if (token == "split") { + axisInfo.mode = AxisInfo::MODE_SPLIT; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 splitToken = mTokenizer->nextToken(WHITESPACE); + std::optional splitValue = parseInt(splitToken.c_str()); + if (!splitValue) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected split value, got '%s'.", + mTokenizer->getLocation().c_str(), splitToken.c_str()); + return BAD_VALUE; + } + axisInfo.splitValue = *splitValue; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 lowAxisToken = mTokenizer->nextToken(WHITESPACE); + std::optional axis = InputEventLookup::getAxisByLabel(lowAxisToken.c_str()); + if (!axis) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected low axis label, got '%s'.", + mTokenizer->getLocation().c_str(), lowAxisToken.c_str()); + return BAD_VALUE; + } + axisInfo.axis = *axis; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 highAxisToken = mTokenizer->nextToken(WHITESPACE); + std::optional highAxis = InputEventLookup::getAxisByLabel(highAxisToken.c_str()); + if (!highAxis) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected high axis label, got '%s'.", + mTokenizer->getLocation().c_str(), highAxisToken.c_str()); + return BAD_VALUE; + } + axisInfo.highAxis = *highAxis; + } else { + std::optional axis = InputEventLookup::getAxisByLabel(token.c_str()); + if (!axis) { + __android_log_print(ANDROID_LOG_ERROR, + "%s: Expected axis label, 'split' or 'invert', got '%s'.", + mTokenizer->getLocation().c_str(), token.c_str()); + return BAD_VALUE; + } + axisInfo.axis = *axis; + } + + for (;;) { + mTokenizer->skipDelimiters(WHITESPACE); + if (mTokenizer->isEol() || mTokenizer->peekChar() == '#') { + break; + } + String8 keywordToken = mTokenizer->nextToken(WHITESPACE); + if (keywordToken == "flat") { + mTokenizer->skipDelimiters(WHITESPACE); + String8 flatToken = mTokenizer->nextToken(WHITESPACE); + std::optional flatOverride = parseInt(flatToken.c_str()); + if (!flatOverride) { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected flat value, got '%s'.", + mTokenizer->getLocation().c_str(), flatToken.c_str()); + return BAD_VALUE; + } + axisInfo.flatOverride = *flatOverride; + } else { + __android_log_print(ANDROID_LOG_ERROR, "%s: Expected keyword 'flat', got '%s'.", + mTokenizer->getLocation().c_str(), + keywordToken.c_str()); + return BAD_VALUE; + } + } + + ALOGD_IF(DEBUG_PARSER, + "Parsed axis: scanCode=%d, mode=%d, axis=%d, highAxis=%d, " + "splitValue=%d, flatOverride=%d.", + *scanCode, axisInfo.mode, axisInfo.axis, axisInfo.highAxis, axisInfo.splitValue, + axisInfo.flatOverride); + mMap->mAxes.insert({*scanCode, axisInfo}); + return NO_ERROR; + } + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h new file mode 100644 index 0000000000..ff811b8570 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include "../utils/Tokenizer.h" +#include + +namespace android { + +struct AxisInfo { + enum Mode { + // Axis value is reported directly. + MODE_NORMAL = 0, + // Axis value should be inverted before reporting. + MODE_INVERT = 1, + // Axis value should be split into two axes + MODE_SPLIT = 2, + }; + + // Axis mode. + Mode mode; + + // Axis id. + // When split, this is the axis used for values smaller than the split position. + int32_t axis; + + // When split, this is the axis used for values after higher than the split position. + int32_t highAxis; + + // The split value, or 0 if not split. + int32_t splitValue; + + // The flat value, or -1 if none. + int32_t flatOverride; + + AxisInfo() : mode(MODE_NORMAL), axis(-1), highAxis(-1), splitValue(0), flatOverride(-1) { + } +}; + +/** + * Describes a mapping from keyboard scan codes and joystick axes to Android key codes and axes. + * + * This object is immutable after it has been loaded. + */ +class KeyLayoutMap { +public: + static base::Result> load(const std::string& filename, + const char* contents = nullptr); + static base::Result> loadContents(const std::string& filename, + const char* contents); + + status_t mapKey(int32_t scanCode, int32_t usageCode, + int32_t* outKeyCode, uint32_t* outFlags) const; + std::vector findScanCodesForKey(int32_t keyCode) const; + std::optional findScanCodeForLed(int32_t ledCode) const; + std::vector findUsageCodesForKey(int32_t keyCode) const; + std::optional findUsageCodeForLed(int32_t ledCode) const; + + std::optional mapAxis(int32_t scanCode) const; + const std::string getLoadFileName() const; + // Return pair of sensor type and sensor data index, for the input device abs code + base::Result> mapSensor(int32_t absCode) const; + + virtual ~KeyLayoutMap(); + +private: + static base::Result> load(Tokenizer* tokenizer); + + struct Key { + int32_t keyCode; + uint32_t flags; + }; + + std::unordered_map mKeysByScanCode; + std::unordered_map mKeysByUsageCode; + std::unordered_map mAxes; + std::set mRequiredKernelConfigs; + std::string mLoadFileName; + + KeyLayoutMap(); + + const Key* getKey(int32_t scanCode, int32_t usageCode) const; + + class Parser { + KeyLayoutMap* mMap; + Tokenizer* mTokenizer; + + public: + Parser(KeyLayoutMap* map, Tokenizer* tokenizer); + ~Parser(); + status_t parse(); + + private: + status_t parseKey(); + status_t parseAxis(); + status_t parseRequiredKernelConfig(); + }; +}; + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/errors.h b/sysbridge/src/main/cpp/android/libbase/errors.h new file mode 100644 index 0000000000..cca4e887bb --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/errors.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Portable error handling functions. This is only necessary for host-side +// code that needs to be cross-platform; code that is only run on Unix should +// just use errno and strerror() for simplicity. +// +// There is some complexity since Windows has (at least) three different error +// numbers, not all of which share the same type: +// * errno: for C runtime errors. +// * GetLastError(): Windows non-socket errors. +// * WSAGetLastError(): Windows socket errors. +// errno can be passed to strerror() on all platforms, but the other two require +// special handling to get the error string. Refer to Microsoft documentation +// to determine which error code to check for each function. + +#pragma once + +#include + +#include + +namespace android { +namespace base { + +// Returns a string describing the given system error code. |error_code| must +// be errno on Unix or GetLastError()/WSAGetLastError() on Windows. Passing +// errno on Windows has undefined behavior. +std::string SystemErrorCodeToString(int error_code); + +} // namespace base +} // namespace android + +// Convenient macros for evaluating a statement, checking if the result is error, and returning it +// to the caller. If it is ok then the inner value is unwrapped (if applicable) and returned. +// +// Usage with Result: +// +// Result getFoo() {...} +// +// Result getBar() { +// Foo foo = OR_RETURN(getFoo()); +// return Bar{foo}; +// } +// +// Usage with status_t: +// +// status_t getFoo(Foo*) {...} +// +// status_t getBar(Bar* bar) { +// Foo foo; +// OR_RETURN(getFoo(&foo)); +// *bar = Bar{foo}; +// return OK; +// } +// +// Actually this can be used for any type as long as the OkOrFail contract is satisfied. See +// below. +// If implicit conversion compilation errors occur involving a value type with a templated +// forwarding ref ctor, compilation with cpp20 or explicitly converting to the desired +// return type is required. +#define OR_RETURN(expr) \ + UNWRAP_OR_DO(__or_return_expr, expr, { return ok_or_fail::Fail(std::move(__or_return_expr)); }) + +// Same as OR_RETURN, but aborts if expr is a failure. +#if defined(__BIONIC__) +#define OR_FATAL(expr) \ + UNWRAP_OR_DO(__or_fatal_expr, expr, { \ + __assert(__FILE__, __LINE__, ok_or_fail::ErrorMessage(__or_fatal_expr).c_str()); \ + }) +#else +#define OR_FATAL(expr) \ + UNWRAP_OR_DO(__or_fatal_expr, expr, { \ + fprintf(stderr, "%s:%d: assertion \"%s\" failed", __FILE__, __LINE__, \ + ok_or_fail::ErrorMessage(__or_fatal_expr).c_str()); \ + abort(); \ + }) +#endif + +// Variant for use in gtests, which aborts the test function with an assertion failure on error. +// This is akin to ASSERT_OK_AND_ASSIGN for absl::Status, except the assignment is external. It +// assumes the user depends on libgmock and includes gtest/gtest.h. +#define OR_ASSERT_FAIL(expr) \ + UNWRAP_OR_DO(__or_assert_expr, expr, { \ + FAIL() << "Value of: " << #expr << "\n" \ + << " Actual: " << __or_assert_expr.error().message() << "\n" \ + << "Expected: is ok\n"; \ + }) + +// Generic macro to execute any statement(s) on error. Execution should never reach the end of them. +// result_var is assigned expr and is only visible to on_error_stmts. +#define UNWRAP_OR_DO(result_var, expr, on_error_stmts) \ + ({ \ + decltype(expr)&& result_var = (expr); \ + typedef android::base::OkOrFail> ok_or_fail; \ + if (!ok_or_fail::IsOk(result_var)) { \ + { \ + on_error_stmts; \ + } \ + __builtin_unreachable(); \ + } \ + ok_or_fail::Unwrap(std::move(result_var)); \ + }) + +namespace android { +namespace base { + +// The OkOrFail contract for a type T. This must be implemented for a type T if you want to use +// OR_RETURN(stmt) where stmt evalues to a value of type T. +template +struct OkOrFail { + // Checks if T is ok or fail. + static bool IsOk(const T&); + + // Turns T into the success value. + template + static U Unwrap(T&&); + + // Moves T into OkOrFail, so that we can convert it to other types + OkOrFail(T&& v); + OkOrFail() = delete; + OkOrFail(const T&) = delete; + + // And there need to be one or more conversion operators that turns the error value of T into a + // target type. For example, for T = Result, there can be ... + // + // // for the case where OR_RETURN is called in a function expecting E + // operator E()&& { return val_.error().code(); } + // + // // for the case where OR_RETURN is called in a function expecting Result + // template + // operator Result()&& { return val_.error(); } + + // And there needs to be a method that returns the string representation of the fail value. + // static const std::string& ErrorMessage(const T& v); + // or + // static std::string ErrorMessage(const T& v); +}; + +} // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/expected.h b/sysbridge/src/main/cpp/android/libbase/expected.h new file mode 100644 index 0000000000..ddab9e5b57 --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/expected.h @@ -0,0 +1,622 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +// android::base::expected is a partial implementation of C++23's std::expected +// for Android. +// +// Usage: +// using android::base::expected; +// using android::base::unexpected; +// +// expected safe_divide(double i, double j) { +// if (j == 0) return unexpected("divide by zero"); +// else return i / j; +// } +// +// void test() { +// auto q = safe_divide(10, 0); +// if (q.ok()) { printf("%f\n", q.value()); } +// else { printf("%s\n", q.error().c_str()); } +// } +// +// Once the Android platform has moved to C++23, this will be removed and +// android::base::expected will be type aliased to std::expected. +// + +namespace android { +namespace base { + +// Synopsis +template +class expected; + +template +class unexpected; +template +unexpected(E) -> unexpected; + +template +class bad_expected_access; + +template <> +class bad_expected_access; + +struct unexpect_t { + explicit unexpect_t() = default; +}; +inline constexpr unexpect_t unexpect{}; + +// macros for SFINAE +#define _ENABLE_IF(...) \ + , std::enable_if_t<(__VA_ARGS__)>* = nullptr + +// Define NODISCARD_EXPECTED to prevent expected from being +// ignored when used as a return value. This is off by default. +#ifdef NODISCARD_EXPECTED +#define _NODISCARD_ [[nodiscard]] +#else +#define _NODISCARD_ +#endif + +#define _EXPLICIT(cond) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wc++20-extensions\"") explicit(cond) \ + _Pragma("clang diagnostic pop") + +#define _COMMA , + +namespace expected_internal { + +template +struct remove_cvref { + using type = std::remove_cv_t>; +}; + +template +using remove_cvref_t = typename remove_cvref::type; + +// Can T be constructed from W (or W converted to T)? W can be lvalue or rvalue, +// const or not. +template +inline constexpr bool converts_from_any_cvref = + std::disjunction_v, std::is_convertible, + std::is_constructible, std::is_convertible, + std::is_constructible, std::is_convertible, + std::is_constructible, std::is_convertible>; + +template +struct is_expected : std::false_type {}; + +template +struct is_expected> : std::true_type {}; + +template +inline constexpr bool is_expected_v = is_expected::value; + +template +struct is_unexpected : std::false_type {}; + +template +struct is_unexpected> : std::true_type {}; + +template +inline constexpr bool is_unexpected_v = is_unexpected::value; + +// Constraints on constructing an expected from an expected +// related to T and U. UF is either "const U&" or "U". +template +inline constexpr bool convert_value_constraints = + std::is_constructible_v && + (std::is_same_v, bool> || !converts_from_any_cvref>); + +// Constraints on constructing an expected<..., E> from an expected +// related to E, G, and expected. GF is either "const G&" or "G". +template +inline constexpr bool convert_error_constraints = + std::is_constructible_v && + !std::is_constructible_v, expected&> && + !std::is_constructible_v, expected> && + !std::is_constructible_v, const expected&> && + !std::is_constructible_v, const expected>; + +// If an exception is thrown in expected::operator=, while changing the expected +// object between a value and an error, the expected object is supposed to +// retain its original value, which is only possible if certain constructors +// are noexcept. This implementation doesn't try to be exception-safe, but +// enforce these constraints anyway because std::expected also will enforce +// them, and we intend to switch to it eventually. +template +inline constexpr bool eh_assign_constraints = + std::is_nothrow_constructible_v || + std::is_nothrow_move_constructible_v || + std::is_nothrow_move_constructible_v; + +// Implement expected<..., E>::expected([const] unexpected [&/&&]). +#define _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(GF, ParamType, forward_func) \ + template )> \ + constexpr _EXPLICIT((!std::is_convertible_v)) \ + expected(ParamType e) noexcept(std::is_nothrow_constructible_v) \ + : var_(std::in_place_index<1>, forward_func(e.error())) {} + +// Implement expected<..., E>::operator=([const] unexpected [&/&&]). +#define _ASSIGN_UNEXPECTED_TO_EXPECTED(GF, ParamType, forward_func, extra_constraints) \ + template && \ + std::is_assignable_v) && \ + extra_constraints> \ + constexpr expected& operator=(ParamType e) noexcept(std::is_nothrow_constructible_v && \ + std::is_nothrow_assignable_v) { \ + if (has_value()) { \ + var_.template emplace<1>(forward_func(e.error())); \ + } else { \ + error() = forward_func(e.error()); \ + } \ + return *this; \ + } + +} // namespace expected_internal + +// Class expected +template +class _NODISCARD_ expected { + static_assert(std::is_object_v && !std::is_array_v && + !std::is_same_v, std::in_place_t> && + !std::is_same_v, unexpect_t> && + !expected_internal::is_unexpected_v>, + "expected value type cannot be a reference, a function, an array, in_place_t, " + "unexpect_t, or unexpected"); + + public: + using value_type = T; + using error_type = E; + using unexpected_type = unexpected; + + template + using rebind = expected; + + // Delegate simple operations to the underlying std::variant. std::variant + // doesn't set noexcept well, at least for copy ctor/assign, so set it + // explicitly. Technically the copy/move assignment operators should also be + // deleted if neither T nor E satisfies is_nothrow_move_constructible_v, but + // that would require making these operator= methods into template functions. + constexpr expected() = default; + constexpr expected(const expected& rhs) noexcept( + std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_constructible_v) = default; + constexpr expected(expected&& rhs) noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_move_constructible_v) = default; + constexpr expected& operator=(const expected& rhs) noexcept( + std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v && + std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) = default; + constexpr expected& operator=(expected&& rhs) noexcept( + std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v && + std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) = default; + + // Construct this expected from a different expected type. +#define _CONVERTING_CTOR(UF, GF, ParamType, forward_func) \ + template && \ + expected_internal::convert_error_constraints)> \ + constexpr _EXPLICIT((!std::is_convertible_v || !std::is_convertible_v)) \ + expected(ParamType rhs) noexcept(std::is_nothrow_constructible_v && \ + std::is_nothrow_constructible_v) \ + : var_(rhs.has_value() ? variant_type(std::in_place_index<0>, forward_func(rhs.value())) \ + : variant_type(std::in_place_index<1>, forward_func(rhs.error()))) {} + + // NOLINTNEXTLINE(google-explicit-constructor) + _CONVERTING_CTOR(const U&, const G&, const expected&, ) + // NOLINTNEXTLINE(google-explicit-constructor) + _CONVERTING_CTOR(U, G, expected&&, std::move) + +#undef _CONVERTING_CTOR + + // Construct from (converted) success value, using a forwarding reference. + template , std::in_place_t> && + !std::is_same_v, expected> && + !expected_internal::is_unexpected_v> && + std::is_constructible_v && + (!std::is_same_v, bool> || + !expected_internal::is_expected_v>))> + constexpr _EXPLICIT((!std::is_convertible_v)) + // NOLINTNEXTLINE(google-explicit-constructor) + expected(U&& v) noexcept(std::is_nothrow_constructible_v) + : var_(std::in_place_index<0>, std::forward(v)) {} + + // NOLINTNEXTLINE(google-explicit-constructor) + _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(const G&, const unexpected&, ) + // NOLINTNEXTLINE(google-explicit-constructor) + _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(G, unexpected&&, std::move) + + // in_place_t construction + template )> + constexpr explicit expected(std::in_place_t, Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : var_(std::in_place_index<0>, std::forward(args)...) {} + + // in_place_t with initializer_list construction + template &, Args...>)> + constexpr explicit expected(std::in_place_t, std::initializer_list il, Args&&... args) + noexcept(std::is_nothrow_constructible_v&, Args...>) + : var_(std::in_place_index<0>, il, std::forward(args)...) {} + + // unexpect_t construction + template )> + constexpr explicit expected(unexpect_t, Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : var_(std::in_place_index<1>, unexpected_type(std::forward(args)...)) {} + + // unexpect_t with initializer_list construction + template &, Args...>)> + constexpr explicit expected(unexpect_t, std::initializer_list il, Args&&... args) + noexcept(std::is_nothrow_constructible_v&, Args...>) + : var_(std::in_place_index<1>, unexpected_type(il, std::forward(args)...)) {} + + // Assignment from (converted) success value, using a forwarding reference. + template > && + !expected_internal::is_unexpected_v> && + std::is_constructible_v && std::is_assignable_v && + expected_internal::eh_assign_constraints)> + constexpr expected& operator=(U&& v) noexcept(std::is_nothrow_constructible_v && + std::is_nothrow_assignable_v) { + if (has_value()) { + value() = std::forward(v); + } else { + var_.template emplace<0>(std::forward(v)); + } + return *this; + } + + _ASSIGN_UNEXPECTED_TO_EXPECTED(const G&, const unexpected&, , + (expected_internal::eh_assign_constraints)) + _ASSIGN_UNEXPECTED_TO_EXPECTED(G, unexpected&&, std::move, + (expected_internal::eh_assign_constraints)) + + // modifiers + template )> + constexpr T& emplace(Args&&... args) noexcept { + var_.template emplace<0>(std::forward(args)...); + return value(); + } + + template &, Args...>)> + constexpr T& emplace(std::initializer_list il, Args&&... args) noexcept { + var_.template emplace<0>(il, std::forward(args)...); + return value(); + } + + // Swap. This function takes a template argument so that _ENABLE_IF works. + template && + std::is_swappable_v && std::is_swappable_v && + std::is_move_constructible_v && std::is_move_constructible_v && + (std::is_nothrow_move_constructible_v || + std::is_nothrow_move_constructible_v))> + constexpr void swap(expected& rhs) noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_swappable_v && + std::is_nothrow_move_constructible_v && + std::is_nothrow_swappable_v) { + var_.swap(rhs.var_); + } + + // observers + constexpr const T* operator->() const { return std::addressof(value()); } + constexpr T* operator->() { return std::addressof(value()); } + constexpr const T& operator*() const& { return value(); } + constexpr T& operator*() & { return value(); } + constexpr const T&& operator*() const&& { return std::move(std::get(var_)); } + constexpr T&& operator*() && { return std::move(std::get(var_)); } + + constexpr bool has_value() const noexcept { return var_.index() == 0; } + constexpr bool ok() const noexcept { return has_value(); } + constexpr explicit operator bool() const noexcept { return has_value(); } + + constexpr const T& value() const& { return std::get(var_); } + constexpr T& value() & { return std::get(var_); } + constexpr const T&& value() const&& { return std::move(std::get(var_)); } + constexpr T&& value() && { return std::move(std::get(var_)); } + + constexpr const E& error() const& { return std::get(var_).error(); } + constexpr E& error() & { return std::get(var_).error(); } + constexpr const E&& error() const&& { return std::move(std::get(var_)).error(); } + constexpr E&& error() && { return std::move(std::get(var_)).error(); } + + template && + std::is_convertible_v + )> + constexpr T value_or(U&& v) const& { + if (has_value()) return value(); + else return static_cast(std::forward(v)); + } + + template && + std::is_convertible_v + )> + constexpr T value_or(U&& v) && { + if (has_value()) return std::move(value()); + else return static_cast(std::forward(v)); + } + + // expected equality operators + template + friend constexpr bool operator==(const expected& x, const expected& y); + template + friend constexpr bool operator!=(const expected& x, const expected& y); + + // Comparison with unexpected + template + friend constexpr bool operator==(const expected&, const unexpected&); + template + friend constexpr bool operator==(const unexpected&, const expected&); + template + friend constexpr bool operator!=(const expected&, const unexpected&); + template + friend constexpr bool operator!=(const unexpected&, const expected&); + + private: + using variant_type = std::variant; + variant_type var_; +}; + +template +constexpr bool operator==(const expected& x, const expected& y) { + if (x.has_value() != y.has_value()) return false; + if (!x.has_value()) return x.error() == y.error(); + return *x == *y; +} + +template +constexpr bool operator!=(const expected& x, const expected& y) { + return !(x == y); +} + +// Comparison with unexpected +template +constexpr bool operator==(const expected& x, const unexpected& y) { + return !x.has_value() && (x.error() == y.error()); +} +template +constexpr bool operator==(const unexpected& x, const expected& y) { + return !y.has_value() && (x.error() == y.error()); +} +template +constexpr bool operator!=(const expected& x, const unexpected& y) { + return x.has_value() || (x.error() != y.error()); +} +template +constexpr bool operator!=(const unexpected& x, const expected& y) { + return y.has_value() || (x.error() != y.error()); +} + +template +class _NODISCARD_ expected { + public: + using value_type = void; + using error_type = E; + using unexpected_type = unexpected; + + template + using rebind = expected; + + // Delegate simple operations to the underlying std::variant. + constexpr expected() = default; + constexpr expected(const expected& rhs) noexcept(std::is_nothrow_copy_constructible_v) = + default; + constexpr expected(expected&& rhs) noexcept(std::is_nothrow_move_constructible_v) = default; + constexpr expected& operator=(const expected& rhs) noexcept( + std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) = default; + constexpr expected& operator=(expected&& rhs) noexcept( + std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) = default; + + // Construct this expected from a different expected type. +#define _CONVERTING_CTOR(GF, ParamType, forward_func) \ + template && \ + expected_internal::convert_error_constraints)> \ + constexpr _EXPLICIT((!std::is_convertible_v)) \ + expected(ParamType rhs) noexcept(std::is_nothrow_constructible_v) \ + : var_(rhs.has_value() ? variant_type(std::in_place_index<0>, std::monostate()) \ + : variant_type(std::in_place_index<1>, forward_func(rhs.error()))) {} + + // NOLINTNEXTLINE(google-explicit-constructor) + _CONVERTING_CTOR(const G&, const expected&, ) + // NOLINTNEXTLINE(google-explicit-constructor) + _CONVERTING_CTOR(G, expected&&, std::move) + +#undef _CONVERTING_CTOR + + // NOLINTNEXTLINE(google-explicit-constructor) + _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(const G&, const unexpected&, ) + // NOLINTNEXTLINE(google-explicit-constructor) + _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(G, unexpected&&, std::move) + + // in_place_t construction + constexpr explicit expected(std::in_place_t) noexcept {} + + // unexpect_t construction + template )> + constexpr explicit expected(unexpect_t, Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : var_(std::in_place_index<1>, unexpected_type(std::forward(args)...)) {} + + // unexpect_t with initializer_list construction + template &, Args...>)> + constexpr explicit expected(unexpect_t, std::initializer_list il, Args&&... args) + noexcept(std::is_nothrow_constructible_v&, Args...>) + : var_(std::in_place_index<1>, unexpected_type(il, std::forward(args)...)) {} + + _ASSIGN_UNEXPECTED_TO_EXPECTED(const G&, const unexpected&, , true) + _ASSIGN_UNEXPECTED_TO_EXPECTED(G, unexpected&&, std::move, true) + + // modifiers + constexpr void emplace() noexcept { var_.template emplace<0>(std::monostate()); } + + // Swap. This function takes a template argument so that _ENABLE_IF works. + template && + std::is_swappable_v && std::is_move_constructible_v)> + constexpr void swap(expected& rhs) noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_swappable_v) { + var_.swap(rhs.var_); + } + + // observers + constexpr bool has_value() const noexcept { return var_.index() == 0; } + constexpr bool ok() const noexcept { return has_value(); } + constexpr explicit operator bool() const noexcept { return has_value(); } + + constexpr void value() const& { if (!has_value()) std::get<0>(var_); } + + constexpr const E& error() const& { return std::get<1>(var_).error(); } + constexpr E& error() & { return std::get<1>(var_).error(); } + constexpr const E&& error() const&& { return std::move(std::get<1>(var_)).error(); } + constexpr E&& error() && { return std::move(std::get<1>(var_)).error(); } + + // expected equality operators + template + friend constexpr bool operator==(const expected& x, const expected& y); + + private: + using variant_type = std::variant; + variant_type var_; +}; + +template +constexpr bool operator==(const expected& x, const expected& y) { + if (x.has_value() != y.has_value()) return false; + if (!x.has_value()) return x.error() == y.error(); + return true; +} + +template +constexpr bool operator==(const expected& x, const expected& y) { + if (x.has_value() != y.has_value()) return false; + if (!x.has_value()) return x.error() == y.error(); + return false; +} + +template +constexpr bool operator==(const expected& x, const expected& y) { + if (x.has_value() != y.has_value()) return false; + if (!x.has_value()) return x.error() == y.error(); + return false; +} + +template &>().swap(std::declval&>()))> +constexpr void swap(expected& x, expected& y) noexcept(noexcept(x.swap(y))) { + x.swap(y); +} + +template +class unexpected { + static_assert(std::is_object_v && !std::is_array_v && !std::is_const_v && + !std::is_volatile_v && !expected_internal::is_unexpected_v, + "unexpected error type cannot be a reference, a function, an array, cv-qualified, " + "or unexpected"); + + public: + // constructors + constexpr unexpected(const unexpected&) = default; + constexpr unexpected(unexpected&&) = default; + + template , unexpected> && + !std::is_same_v, std::in_place_t> && + std::is_constructible_v)> + constexpr explicit unexpected(Err&& e) noexcept(std::is_nothrow_constructible_v) + : val_(std::forward(e)) {} + + template )> + constexpr explicit unexpected(std::in_place_t, Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : val_(std::forward(args)...) {} + + template &, Args...>)> + constexpr explicit unexpected(std::in_place_t, std::initializer_list il, Args&&... args) + noexcept(std::is_nothrow_constructible_v&, Args...>) + : val_(il, std::forward(args)...) {} + + constexpr unexpected& operator=(const unexpected&) = default; + constexpr unexpected& operator=(unexpected&&) = default; + + // observer + constexpr const E& error() const& noexcept { return val_; } + constexpr E& error() & noexcept { return val_; } + constexpr const E&& error() const&& noexcept { return std::move(val_); } + constexpr E&& error() && noexcept { return std::move(val_); } + + // Swap. This function takes a template argument so that _ENABLE_IF works. + template && std::is_swappable_v)> + void swap(unexpected& other) noexcept(std::is_nothrow_swappable_v) { + // Make std::swap visible to provide swap for STL and builtin types, but use + // an unqualified swap to invoke argument-dependent lookup to find the swap + // functions for user-declared types. + using std::swap; + swap(val_, other.val_); + } + + template + friend constexpr bool + operator==(const unexpected& e1, const unexpected& e2); + template + friend constexpr bool + operator!=(const unexpected& e1, const unexpected& e2); + + private: + E val_; +}; + +template +constexpr bool +operator==(const unexpected& e1, const unexpected& e2) { + return e1.error() == e2.error(); +} + +template +constexpr bool +operator!=(const unexpected& e1, const unexpected& e2) { + return e1.error() != e2.error(); +} + +template )> +void swap(unexpected& x, unexpected& y) noexcept(noexcept(x.swap(y))) { + x.swap(y); +} + +// TODO: bad_expected_access class + +#undef _ENABLE_IF +#undef _NODISCARD_ +#undef _EXPLICIT +#undef _COMMA +#undef _CONSTRUCT_EXPECTED_FROM_UNEXPECTED +#undef _ASSIGN_UNEXPECTED_TO_EXPECTED + +} // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/format.h b/sysbridge/src/main/cpp/android/libbase/format.h new file mode 100644 index 0000000000..065051351b --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/format.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +// We include fmtlib here as an alias, since libbase will have fmtlib statically linked already. +// It is accessed through its normal fmt:: namespace. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshadow" +#include +#pragma clang diagnostic pop +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#endif // _WIN32 diff --git a/sysbridge/src/main/cpp/android/libbase/result.cpp b/sysbridge/src/main/cpp/android/libbase/result.cpp new file mode 100644 index 0000000000..c7163f270b --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/result.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "result.h" + +namespace android { +namespace base { + +ResultError MakeResultErrorWithCode(std::string&& message, Errno code) { + return ResultError(std::move(message) + ": " + code.print(), code); +} + +} // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/result.h b/sysbridge/src/main/cpp/android/libbase/result.h new file mode 100644 index 0000000000..04c15d772a --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/result.h @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Result is the type that is used to pass a success value of type T or an error code of type +// E, optionally together with an error message. T and E can be any type. If E is omitted it +// defaults to int, which is useful when errno(3) is used as the error code. +// +// Passing a success value or an error value: +// +// Result readFile() { +// std::string content; +// if (base::ReadFileToString("path", &content)) { +// return content; // ok case +// } else { +// return ErrnoError() << "failed to read"; // error case +// } +// } +// +// Checking the result and then unwrapping the value or propagating the error: +// +// Result hasAWord() { +// auto content = readFile(); +// if (!content.ok()) { +// return Error() << "failed to process: " << content.error(); +// } +// return (*content.find("happy") != std::string::npos); +// } +// +// Using custom error code type: +// +// enum class MyError { A, B }; // assume that this is the error code you already have +// +// // To use the error code with Result, define a wrapper class that provides the following +// operations and use the wrapper class as the second type parameter (E) when instantiating +// Result +// +// 1. default constructor +// 2. copy constructor / and move constructor if copying is expensive +// 3. conversion operator to the error code type +// 4. value() function that return the error code value +// 5. print() function that gives a string representation of the error ode value +// +// struct MyErrorWrapper { +// MyError val_; +// MyErrorWrapper() : val_(/* reasonable default value */) {} +// MyErrorWrapper(MyError&& e) : val_(std:forward(e)) {} +// operator const MyError&() const { return val_; } +// MyError value() const { return val_; } +// std::string print() const { +// switch(val_) { +// MyError::A: return "A"; +// MyError::B: return "B"; +// } +// } +// }; +// +// #define NewMyError(e) Error(MyError::e) +// +// Result val = NewMyError(A) << "some message"; +// +// Formatting the error message using fmtlib: +// +// Errorf("{} errors", num); // equivalent to Error() << num << " errors"; +// ErrnoErrorf("{} errors", num); // equivalent to ErrnoError() << num << " errors"; +// +// Returning success or failure, but not the value: +// +// Result doSomething() { +// if (success) return {}; +// else return Error() << "error occurred"; +// } +// +// Extracting error code: +// +// Result val = Error(3) << "some error occurred"; +// assert(3 == val.error().code()); +// + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "errors.h" +#include "expected.h" +#include "format.h" + +namespace android { +namespace base { + +// Errno is a wrapper class for errno(3). Use this type instead of `int` when instantiating +// `Result` and `Error` template classes. This is required to distinguish errno from other +// integer-based error code types like `status_t`. +struct Errno { + Errno() : val_(0) {} + Errno(int e) : val_(e) {} + int value() const { return val_; } + operator int() const { return value(); } + const char* print() const { return strerror(value()); } + + int val_; + + // TODO(b/209929099): remove this conversion operator. This currently is needed to not break + // existing places where error().code() is used to construct enum values. + template >> + operator E() const { + return E(val_); + } +}; + +static_assert(std::is_trivially_copyable_v == true); + +template +struct ResultError { + template >> + ResultError(T&& message, P&& code) + : message_(std::forward(message)), code_(E(std::forward

(code))) {} + + ResultError(const ResultError& other) = default; + ResultError(ResultError&& other) = default; + ResultError& operator=(const ResultError& other) = default; + ResultError& operator=(ResultError&& other) = default; + + template + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() && { + return android::base::unexpected(std::move(*this)); + } + + template + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const& { + return android::base::unexpected(*this); + } + + const std::string& message() const { return message_; } + const E& code() const { return code_; } + + private: + std::string message_; + E code_; +}; + +template +auto format_as(ResultError error) { + return error.message(); +} + +template +struct ResultError { + template >> + ResultError(P&& code) : code_(E(std::forward

(code))) {} + + template + operator android::base::expected>() const { + return android::base::unexpected(ResultError(code_)); + } + + const E& code() const { return code_; } + + private: + E code_; +}; + +template +inline bool operator==(const ResultError& lhs, const ResultError& rhs) { + return lhs.message() == rhs.message() && lhs.code() == rhs.code(); +} + +template +inline bool operator!=(const ResultError& lhs, const ResultError& rhs) { + return !(lhs == rhs); +} + +template +inline std::ostream& operator<<(std::ostream& os, const ResultError& t) { + os << t.message(); + return os; +} + +namespace internal { +// Stream class that does nothing and is has zero (actually 1) size. It is used instead of +// std::stringstream when include_message is false so that we use less on stack. +// sizeof(std::stringstream) is 280 on arm64. +struct DoNothingStream { + template + DoNothingStream& operator<<(T&&) { + return *this; + } + + std::string str() const { return ""; } +}; +} // namespace internal + +template >> +class Error { + public: + Error() : code_(0), has_code_(false) {} + template >> + // NOLINTNEXTLINE(google-explicit-constructor) + Error(P&& code) : code_(std::forward

(code)), has_code_(true) {} + + template >> + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const { + return android::base::unexpected(ResultError

(str(), static_cast

(code_))); + } + + template >> + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const { + return android::base::unexpected(ResultError(static_cast

(code_))); + } + + template + Error& operator<<(T&& t) { + static_assert(include_message, "<< not supported when include_message = false"); + // NOLINTNEXTLINE(bugprone-suspicious-semicolon) + if constexpr (std::is_same_v>, ResultError>) { + if (!has_code_) { + code_ = t.code(); + } + return (*this) << t.message(); + } + int saved = errno; + ss_ << t; + errno = saved; + return *this; + } + + const std::string str() const { + static_assert(include_message, "str() not supported when include_message = false"); + std::string str = ss_.str(); + if (has_code_) { + if (str.empty()) { + return code_.print(); + } + return std::move(str) + ": " + code_.print(); + } + return str; + } + + Error(const Error&) = delete; + Error(Error&&) = delete; + Error& operator=(const Error&) = delete; + Error& operator=(Error&&) = delete; + + template + friend Error ErrorfImpl(fmt::format_string fmt, const Args&... args); + + template + friend Error ErrnoErrorfImpl(fmt::format_string fmt, const Args&... args); + + private: + Error(bool has_code, E code, const std::string& message) : code_(code), has_code_(has_code) { + (*this) << message; + } + + std::conditional_t ss_; + E code_; + const bool has_code_; +}; + +inline Error ErrnoError() { + return Error(Errno{errno}); +} + +template +inline E ErrorCode(E code) { + return code; +} + +// Return the error code of the last ResultError object, if any. +// Otherwise, return `code` as it is. +template +inline E ErrorCode(E code, T&& t, const Args&... args) { + if constexpr (std::is_same_v>, ResultError>) { + return ErrorCode(t.code(), args...); + } + return ErrorCode(code, args...); +} + +__attribute__((noinline)) ResultError MakeResultErrorWithCode(std::string&& message, + Errno code); + +template +inline ResultError ErrorfImpl(fmt::format_string fmt, const Args&... args) { + return ResultError(fmt::vformat(fmt.get(), fmt::make_format_args(args...)), + ErrorCode(Errno{}, args...)); +} + +template +inline ResultError ErrnoErrorfImpl(fmt::format_string fmt, const Args&... args) { + Errno code{errno}; + return MakeResultErrorWithCode(fmt::vformat(fmt.get(), fmt::make_format_args(args...)), code); +} + +#define Errorf(fmt, ...) android::base::ErrorfImpl(FMT_STRING(fmt), ##__VA_ARGS__) +#define ErrnoErrorf(fmt, ...) android::base::ErrnoErrorfImpl(FMT_STRING(fmt), ##__VA_ARGS__) + +template +using Result = android::base::expected>; + +// Specialization of android::base::OkOrFail for V = Result. See android-base/errors.h +// for the contract. + +namespace impl { +template +using Code = std::decay_t().error().code())>; + +template +using ErrorType = std::decay_t().error())>; + +template +constexpr bool IsNumeric = std::is_integral_v || std::is_floating_point_v || + (std::is_enum_v && std::is_convertible_v); + +// This base class exists to take advantage of shadowing +// We include the conversion in this base class so that if the conversion in NumericConversions +// overlaps, we (arbitrarily) choose the implementation in NumericConversions due to shadowing. +template +struct ConversionBase { + ErrorType error_; + // T is a expected>. + operator T() const& { return unexpected(error_); } + operator T() && { return unexpected(std::move(error_)); } + + operator Code() const { return error_.code(); } +}; + +// User defined conversions can be followed by numeric conversions +// Although we template specialize for the exact code type, we need +// specializations for conversions to all numeric types to avoid an +// ambiguous conversion sequence. +template +struct NumericConversions : public ConversionBase {}; +template +struct NumericConversions>> + > : public ConversionBase +{ +#pragma push_macro("SPECIALIZED_CONVERSION") +#define SPECIALIZED_CONVERSION(type) \ + operator expected>() const& { return unexpected(this->error_); } \ + operator expected>()&& { return unexpected(std::move(this->error_)); } + + SPECIALIZED_CONVERSION(int) + SPECIALIZED_CONVERSION(short int) + SPECIALIZED_CONVERSION(unsigned short int) + SPECIALIZED_CONVERSION(unsigned int) + SPECIALIZED_CONVERSION(long int) + SPECIALIZED_CONVERSION(unsigned long int) + SPECIALIZED_CONVERSION(long long int) + SPECIALIZED_CONVERSION(unsigned long long int) + SPECIALIZED_CONVERSION(bool) + SPECIALIZED_CONVERSION(char) + SPECIALIZED_CONVERSION(unsigned char) + SPECIALIZED_CONVERSION(signed char) + SPECIALIZED_CONVERSION(wchar_t) + SPECIALIZED_CONVERSION(char16_t) + SPECIALIZED_CONVERSION(char32_t) + SPECIALIZED_CONVERSION(float) + SPECIALIZED_CONVERSION(double) + SPECIALIZED_CONVERSION(long double) + +#undef SPECIALIZED_CONVERSION +#pragma pop_macro("SPECIALIZED_CONVERSION") + // For debugging purposes + using IsNumericT = std::true_type; +}; + +#ifdef __cpp_concepts +template +// Define a concept which **any** type matches to +concept Universal = std::is_same_v; +#endif + +// A type that is never used. +struct Never {}; +} // namespace impl + +template +struct OkOrFail> + : public impl::NumericConversions> { + using V = Result; + using Err = impl::ErrorType; + using C = impl::Code; +private: + OkOrFail(Err&& v): impl::NumericConversions{std::move(v)} {} + OkOrFail(const OkOrFail& other) = delete; + OkOrFail(const OkOrFail&& other) = delete; +public: + // Checks if V is ok or fail + static bool IsOk(const V& val) { return val.ok(); } + + // Turns V into a success value + static T Unwrap(V&& val) { + if constexpr (std::is_same_v) { + assert(IsOk(val)); + return; + } else { + return std::move(val.value()); + } + } + + // Consumes V when it's a fail value + static OkOrFail Fail(V&& v) { + assert(!IsOk(v)); + return OkOrFail{std::move(v.error())}; + } + + // We specialize as much as possible to avoid ambiguous conversion with templated expected ctor. + // We don't need this specialization if `C` is numeric because that case is already covered by + // `NumericConversions`. + operator Result, impl::Never, C>, E, include_message>() + const& { + return unexpected(this->error_); + } + operator Result, impl::Never, C>, E, include_message>() && { + return unexpected(std::move(this->error_)); + } + +#ifdef __cpp_concepts + // The idea here is to match this template method to any type (not simply trivial types). + // The reason for including a constraint is to take advantage of the fact that a constrained + // method always has strictly lower precedence than a non-constrained method in template + // specialization rules (thus avoiding ambiguity). So we use a universally matching constraint to + // mark this function as less preferable (but still accepting of all types). + template + operator Result() const& { + return unexpected(this->error_); + } + template + operator Result() && { + return unexpected(std::move(this->error_)); + } +#else + template + operator Result() const& { + return unexpected(this->error_); + } + template + operator Result() && { + return unexpected(std::move(this->error_)); + } +#endif + + static const std::string& ErrorMessage(const V& val) { return val.error().message(); } +}; + +// Macros for testing the results of functions that return android::base::Result. These also work +// with base::android::expected. They assume the user depends on libgmock and includes +// gtest/gtest.h. For advanced matchers and customized error messages, see result-gmock.h. + +#define ASSERT_RESULT_OK(stmt) \ + if (const auto& tmp = (stmt); !tmp.ok()) \ + FAIL() << "Value of: " << #stmt << "\n" \ + << " Actual: " << tmp.error().message() << "\n" \ + << "Expected: is ok\n" + +#define EXPECT_RESULT_OK(stmt) \ + if (const auto& tmp = (stmt); !tmp.ok()) \ + ADD_FAILURE() << "Value of: " << #stmt << "\n" \ + << " Actual: " << tmp.error().message() << "\n" \ + << "Expected: is ok\n" + +} // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/Errors.h b/sysbridge/src/main/cpp/android/utils/Errors.h new file mode 100644 index 0000000000..22fb36d250 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Errors.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +namespace android { + +/** + * The type used to return success/failure from frameworks APIs. + * See the anonymous enum below for valid values. + */ +typedef int32_t status_t; + +/* + * Error codes. + * All error codes are negative values. + */ + +enum { + OK = 0, // Preferred constant for checking success. +#ifndef NO_ERROR + // Win32 #defines NO_ERROR as well. It has the same value, so there's no + // real conflict, though it's a bit awkward. + NO_ERROR = OK, // Deprecated synonym for `OK`. Prefer `OK` because it doesn't conflict with Windows. +#endif + + UNKNOWN_ERROR = (-2147483647-1), // INT32_MIN value + + NO_MEMORY = -ENOMEM, + INVALID_OPERATION = -ENOSYS, + BAD_VALUE = -EINVAL, + BAD_TYPE = (UNKNOWN_ERROR + 1), + NAME_NOT_FOUND = -ENOENT, + PERMISSION_DENIED = -EPERM, + NO_INIT = -ENODEV, + ALREADY_EXISTS = -EEXIST, + DEAD_OBJECT = -EPIPE, + FAILED_TRANSACTION = (UNKNOWN_ERROR + 2), +#if !defined(_WIN32) + BAD_INDEX = -EOVERFLOW, + NOT_ENOUGH_DATA = -ENODATA, + WOULD_BLOCK = -EWOULDBLOCK, + TIMED_OUT = -ETIMEDOUT, + UNKNOWN_TRANSACTION = -EBADMSG, +#else + BAD_INDEX = -E2BIG, + NOT_ENOUGH_DATA = (UNKNOWN_ERROR + 3), + WOULD_BLOCK = (UNKNOWN_ERROR + 4), + TIMED_OUT = (UNKNOWN_ERROR + 5), + UNKNOWN_TRANSACTION = (UNKNOWN_ERROR + 6), +#endif + FDS_NOT_ALLOWED = (UNKNOWN_ERROR + 7), + UNEXPECTED_NULL = (UNKNOWN_ERROR + 8), +}; + +// Human readable name of error +std::string statusToString(status_t status); + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/FileMap.h b/sysbridge/src/main/cpp/android/utils/FileMap.h new file mode 100644 index 0000000000..4b37a5f7f4 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/FileMap.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Encapsulate a shared file mapping. +// +#ifndef __LIBS_FILE_MAP_H +#define __LIBS_FILE_MAP_H + +#include + +#if defined(__MINGW32__) +// Ensure that we always pull in winsock2.h before windows.h +#if defined(_WIN32) +#include +#endif +#include +#endif + +namespace android { + +/* + * This represents a memory-mapped file. It might be the entire file or + * only part of it. This requires a little bookkeeping because the mapping + * needs to be aligned on page boundaries, and in some cases we'd like to + * have multiple references to the mapped area without creating additional + * maps. + * + * This always uses MAP_SHARED. + * + * TODO: we should be able to create a new FileMap that is a subset of + * an existing FileMap and shares the underlying mapped pages. Requires + * completing the refcounting stuff and possibly introducing the notion + * of a FileMap hierarchy. + */ + class FileMap { + public: + FileMap(void); + + FileMap(FileMap &&f) noexcept; + + FileMap &operator=(FileMap &&f) noexcept; + + /* + * Create a new mapping on an open file. + * + * Closing the file descriptor does not unmap the pages, so we don't + * claim ownership of the fd. + * + * Returns "false" on failure. + */ + bool create(const char *origFileName, int fd, + off64_t offset, size_t length, bool readOnly); + + ~FileMap(void); + + /* + * Return the name of the file this map came from, if known. + */ + const char *getFileName(void) const { return mFileName; } + + /* + * Get a pointer to the piece of the file we requested. + */ + void *getDataPtr(void) const { return mDataPtr; } + + /* + * Get the length we requested. + */ + size_t getDataLength(void) const { return mDataLength; } + + /* + * Get the data offset used to create this map. + */ + off64_t getDataOffset(void) const { return mDataOffset; } + + /* + * This maps directly to madvise() values, but allows us to avoid + * including everywhere. + */ + enum MapAdvice { + NORMAL, RANDOM, SEQUENTIAL, WILLNEED, DONTNEED + }; + + /* + * Apply an madvise() call to the entire file. + * + * Returns 0 on success, -1 on failure. + */ + int advise(MapAdvice advice); + + protected: + + private: + // these are not implemented + FileMap(const FileMap &src); + + const FileMap &operator=(const FileMap &src); + + char *mFileName; // original file name, if known + void *mBasePtr; // base of mmap area; page aligned + size_t mBaseLength; // length, measured from "mBasePtr" + off64_t mDataOffset; // offset used when map was created + void *mDataPtr; // start of requested data, offset from base + size_t mDataLength; // length, measured from "mDataPtr" +#if defined(__MINGW32__) + HANDLE mFileHandle; // Win32 file handle + HANDLE mFileMapping; // Win32 file mapping handle +#endif + + static long mPageSize; + }; + +} // namespace android + +#endif // __LIBS_FILE_MAP_H diff --git a/sysbridge/src/main/cpp/android/utils/String16.cpp b/sysbridge/src/main/cpp/android/utils/String16.cpp new file mode 100644 index 0000000000..96e1477215 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/String16.cpp @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include + +#include "SharedBuffer.h" + +#define LIBUTILS_PRAGMA(arg) _Pragma(#arg) +#if defined(__clang__) +#define LIBUTILS_PRAGMA_FOR_COMPILER(arg) LIBUTILS_PRAGMA(clang arg) +#elif defined(__GNUC__) +#define LIBUTILS_PRAGMA_FOR_COMPILER(arg) LIBUTILS_PRAGMA(GCC arg) +#else +#define LIBUTILS_PRAGMA_FOR_COMPILER(arg) +#endif +#define LIBUTILS_IGNORE(warning_flag) \ + LIBUTILS_PRAGMA_FOR_COMPILER(diagnostic push) \ + LIBUTILS_PRAGMA_FOR_COMPILER(diagnostic ignored warning_flag) +#define LIBUTILS_IGNORE_END() LIBUTILS_PRAGMA_FOR_COMPILER(diagnostic pop) + +namespace android { + +static const StaticString16 emptyString(u""); +static inline char16_t* getEmptyString() { + return const_cast(emptyString.c_str()); +} + +// --------------------------------------------------------------------------- + +void* String16::alloc(size_t size) +{ + SharedBuffer* buf = SharedBuffer::alloc(size); + buf->mClientMetadata = kIsSharedBufferAllocated; + return buf; +} + +char16_t* String16::allocFromUTF8(const char* u8str, size_t u8len) +{ + if (u8len == 0) return getEmptyString(); + + const uint8_t* u8cur = (const uint8_t*) u8str; + + const ssize_t u16len = utf8_to_utf16_length(u8cur, u8len); + if (u16len < 0) { + return getEmptyString(); + } + + SharedBuffer* buf = static_cast(alloc(sizeof(char16_t) * (u16len + 1))); + if (buf) { + u8cur = (const uint8_t*) u8str; + char16_t* u16str = (char16_t*)buf->data(); + + utf8_to_utf16(u8cur, u8len, u16str, ((size_t) u16len) + 1); + + //printf("Created UTF-16 string from UTF-8 \"%s\":", in); + //printHexData(1, str, buf->size(), 16, 1); + //printf("\n"); + + return u16str; + } + + return getEmptyString(); +} + +char16_t* String16::allocFromUTF16(const char16_t* u16str, size_t u16len) { + if (u16len >= SIZE_MAX / sizeof(char16_t)) { + android_errorWriteLog(0x534e4554, "73826242"); + abort(); + } + + SharedBuffer* buf = static_cast(alloc((u16len + 1) * sizeof(char16_t))); + ALOG_ASSERT(buf, "Unable to allocate shared buffer"); + if (buf) { + char16_t* str = (char16_t*)buf->data(); + memcpy(str, u16str, u16len * sizeof(char16_t)); + str[u16len] = 0; + return str; + } + return getEmptyString(); +} + +// --------------------------------------------------------------------------- + +String16::String16() + : mString(getEmptyString()) +{ +} + +String16::String16(const String16& o) + : mString(o.mString) +{ + acquire(); +} + +String16::String16(String16&& o) noexcept + : mString(o.mString) +{ + o.mString = getEmptyString(); +} + +String16::String16(const String16& o, size_t len, size_t begin) + : mString(getEmptyString()) +{ + setTo(o, len, begin); +} + +String16::String16(const char16_t* o) : mString(allocFromUTF16(o, strlen16(o))) {} + +String16::String16(const char16_t* o, size_t len) : mString(allocFromUTF16(o, len)) {} + +String16::String16(const String8& o) : mString(allocFromUTF8(o.c_str(), o.size())) {} + +String16::String16(const char* o) + : mString(allocFromUTF8(o, strlen(o))) +{ +} + +String16::String16(const char* o, size_t len) + : mString(allocFromUTF8(o, len)) +{ +} + +String16::~String16() +{ + release(); +} + +String16& String16::operator=(String16&& other) noexcept { + release(); + mString = other.mString; + other.mString = getEmptyString(); + return *this; +} + +size_t String16::size() const +{ + if (isStaticString()) { + return staticStringSize(); + } else { + return SharedBuffer::sizeFromData(mString) / sizeof(char16_t) - 1; + } +} + +void String16::setTo(const String16& other) +{ + release(); + mString = other.mString; + acquire(); +} + +status_t String16::setTo(const String16& other, size_t len, size_t begin) +{ + const size_t N = other.size(); + if (begin >= N) { + release(); + mString = getEmptyString(); + return OK; + } + if ((begin+len) > N) len = N-begin; + if (begin == 0 && len == N) { + setTo(other); + return OK; + } + + if (&other == this) { + LOG_ALWAYS_FATAL("Not implemented"); + } + + return setTo(other.c_str() + begin, len); +} + +status_t String16::setTo(const char16_t* other) +{ + return setTo(other, strlen16(other)); +} + +status_t String16::setTo(const char16_t* other, size_t len) +{ + if (len >= SIZE_MAX / sizeof(char16_t)) { + android_errorWriteLog(0x534e4554, "73826242"); + abort(); + } + + SharedBuffer* buf = static_cast(editResize((len + 1) * sizeof(char16_t))); + if (buf) { + char16_t* str = (char16_t*)buf->data(); + memmove(str, other, len*sizeof(char16_t)); + str[len] = 0; + mString = str; + return OK; + } + return NO_MEMORY; +} + +status_t String16::append(const String16& other) { + return append(other.c_str(), other.size()); +} + +status_t String16::append(const char16_t* chrs, size_t otherLen) { + const size_t myLen = size(); + + if (myLen == 0) return setTo(chrs, otherLen); + + if (otherLen == 0) return OK; + + size_t size = myLen; + if (__builtin_add_overflow(size, otherLen, &size) || + __builtin_add_overflow(size, 1, &size) || + __builtin_mul_overflow(size, sizeof(char16_t), &size)) return NO_MEMORY; + + SharedBuffer* buf = static_cast(editResize(size)); + if (!buf) return NO_MEMORY; + + char16_t* str = static_cast(buf->data()); + memcpy(str + myLen, chrs, otherLen * sizeof(char16_t)); + str[myLen + otherLen] = 0; + mString = str; + return OK; +} + +status_t String16::insert(size_t pos, const char16_t* chrs) { + return insert(pos, chrs, strlen16(chrs)); +} + +status_t String16::insert(size_t pos, const char16_t* chrs, size_t otherLen) { + const size_t myLen = size(); + + if (myLen == 0) return setTo(chrs, otherLen); + + if (otherLen == 0) return OK; + + if (pos > myLen) pos = myLen; + + size_t size = myLen; + if (__builtin_add_overflow(size, otherLen, &size) || + __builtin_add_overflow(size, 1, &size) || + __builtin_mul_overflow(size, sizeof(char16_t), &size)) return NO_MEMORY; + + SharedBuffer* buf = static_cast(editResize(size)); + if (!buf) return NO_MEMORY; + + char16_t* str = static_cast(buf->data()); + if (pos < myLen) memmove(str + pos + otherLen, str + pos, (myLen - pos) * sizeof(char16_t)); + memcpy(str + pos, chrs, otherLen * sizeof(char16_t)); + str[myLen + otherLen] = 0; + mString = str; + return OK; +} + +ssize_t String16::findFirst(char16_t c) const +{ + const char16_t* str = string(); + const char16_t* p = str; + const char16_t* e = p + size(); + while (p < e) { + if (*p == c) { + return p-str; + } + p++; + } + return -1; +} + +ssize_t String16::findLast(char16_t c) const +{ + const char16_t* str = string(); + const char16_t* p = str; + const char16_t* e = p + size(); + while (p < e) { + e--; + if (*e == c) { + return e-str; + } + } + return -1; +} + +bool String16::startsWith(const String16& prefix) const +{ + const size_t ps = prefix.size(); + if (ps > size()) return false; + return strzcmp16(mString, ps, prefix.c_str(), ps) == 0; +} + +bool String16::startsWith(const char16_t* prefix) const +{ + const size_t ps = strlen16(prefix); + if (ps > size()) return false; + return strncmp16(mString, prefix, ps) == 0; +} + +bool String16::contains(const char16_t* chrs) const +{ + return strstr16(mString, chrs) != nullptr; +} + +void* String16::edit() { + SharedBuffer* buf; + if (isStaticString()) { + buf = static_cast(alloc((size() + 1) * sizeof(char16_t))); + if (buf) { + memcpy(buf->data(), mString, (size() + 1) * sizeof(char16_t)); + } + } else { + buf = SharedBuffer::bufferFromData(mString)->edit(); + buf->mClientMetadata = kIsSharedBufferAllocated; + } + return buf; +} + +void* String16::editResize(size_t newSize) { + SharedBuffer* buf; + if (isStaticString()) { + size_t copySize = (size() + 1) * sizeof(char16_t); + if (newSize < copySize) { + copySize = newSize; + } + buf = static_cast(alloc(newSize)); + if (buf) { + memcpy(buf->data(), mString, copySize); + } + } else { + buf = SharedBuffer::bufferFromData(mString)->editResize(newSize); + buf->mClientMetadata = kIsSharedBufferAllocated; + } + return buf; +} + +void String16::acquire() +{ + if (!isStaticString()) { + SharedBuffer::bufferFromData(mString)->acquire(); + } +} + +void String16::release() +{ + if (!isStaticString()) { + SharedBuffer::bufferFromData(mString)->release(); + } +} + +bool String16::isStaticString() const { + // See String16.h for notes on the memory layout of String16::StaticData and + // SharedBuffer. + LIBUTILS_IGNORE("-Winvalid-offsetof") + static_assert(sizeof(SharedBuffer) - offsetof(SharedBuffer, mClientMetadata) == 4); + LIBUTILS_IGNORE_END() + const uint32_t* p = reinterpret_cast(mString); + return (*(p - 1) & kIsSharedBufferAllocated) == 0; +} + +size_t String16::staticStringSize() const { + // See String16.h for notes on the memory layout of String16::StaticData and + // SharedBuffer. + LIBUTILS_IGNORE("-Winvalid-offsetof") + static_assert(sizeof(SharedBuffer) - offsetof(SharedBuffer, mClientMetadata) == 4); + LIBUTILS_IGNORE_END() + const uint32_t* p = reinterpret_cast(mString); + return static_cast(*(p - 1)); +} + +status_t String16::replaceAll(char16_t replaceThis, char16_t withThis) +{ + const size_t N = size(); + const char16_t* str = string(); + char16_t* edited = nullptr; + for (size_t i=0; i(edit()); + if (!buf) { + return NO_MEMORY; + } + edited = (char16_t*)buf->data(); + mString = str = edited; + } + edited[i] = withThis; + } + } + return OK; +} + +}; // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/String16.h b/sysbridge/src/main/cpp/android/utils/String16.h new file mode 100644 index 0000000000..54c6d21ae6 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/String16.h @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_STRING16_H +#define ANDROID_STRING16_H + +#include +#include +#include + +#include "Errors.h" +#include "String8.h" +#include "TypeHelpers.h" + +#if __cplusplus >= 202002L +#include +#endif + +// --------------------------------------------------------------------------- + +namespace android { + +// --------------------------------------------------------------------------- + +template +class StaticString16; + +// DO NOT USE: please use std::u16string + +//! This is a string holding UTF-16 characters. +class String16 +{ +public: + String16(); + String16(const String16& o); + String16(String16&& o) noexcept; + String16(const String16& o, + size_t len, + size_t begin=0); + explicit String16(const char16_t* o); + explicit String16(const char16_t* o, size_t len); + explicit String16(const String8& o); + explicit String16(const char* o); + explicit String16(const char* o, size_t len); + + ~String16(); + + inline const char16_t* c_str() const; + + size_t size() const; + inline bool empty() const; + + inline size_t length() const; + + void setTo(const String16& other); + status_t setTo(const char16_t* other); + status_t setTo(const char16_t* other, size_t len); + status_t setTo(const String16& other, + size_t len, + size_t begin=0); + + status_t append(const String16& other); + status_t append(const char16_t* other, size_t len); + + inline String16& operator=(const String16& other); + String16& operator=(String16&& other) noexcept; + + inline String16& operator+=(const String16& other); + inline String16 operator+(const String16& other) const; + + status_t insert(size_t pos, const char16_t* chrs); + status_t insert(size_t pos, + const char16_t* chrs, size_t len); + + ssize_t findFirst(char16_t c) const; + ssize_t findLast(char16_t c) const; + + bool startsWith(const String16& prefix) const; + bool startsWith(const char16_t* prefix) const; + + bool contains(const char16_t* chrs) const; + inline bool contains(const String16& other) const; + + status_t replaceAll(char16_t replaceThis, + char16_t withThis); + + inline int compare(const String16& other) const; + + inline bool operator<(const String16& other) const; + inline bool operator<=(const String16& other) const; + inline bool operator==(const String16& other) const; + inline bool operator!=(const String16& other) const; + inline bool operator>=(const String16& other) const; + inline bool operator>(const String16& other) const; +#if __cplusplus >= 202002L + inline std::strong_ordering operator<=>(const String16& other) const; +#endif + + inline bool operator<(const char16_t* other) const; + inline bool operator<=(const char16_t* other) const; + inline bool operator==(const char16_t* other) const; + inline bool operator!=(const char16_t* other) const; + inline bool operator>=(const char16_t* other) const; + inline bool operator>(const char16_t* other) const; +#if __cplusplus >= 202002L + inline std::strong_ordering operator<=>(const char16_t* other) const; +#endif + + inline operator const char16_t*() const; + + // Implicit cast to std::u16string is not implemented on purpose - u16string_view is much + // lighter and if one needs, they can still create u16string from u16string_view. + inline operator std::u16string_view() const; + + // Static and non-static String16 behave the same for the users, so + // this method isn't of much use for the users. It is public for testing. + bool isStaticString() const; + + private: + /* + * A flag indicating the type of underlying buffer. + */ + static constexpr uint32_t kIsSharedBufferAllocated = 0x80000000; + + /* + * alloc() returns void* so that SharedBuffer class is not exposed. + */ + static void* alloc(size_t size); + static char16_t* allocFromUTF8(const char* u8str, size_t u8len); + static char16_t* allocFromUTF16(const char16_t* u16str, size_t u16len); + + /* + * edit() and editResize() return void* so that SharedBuffer class + * is not exposed. + */ + void* edit(); + void* editResize(size_t new_size); + + void acquire(); + void release(); + + size_t staticStringSize() const; + + const char16_t* mString; + +protected: + /* + * Data structure used to allocate static storage for static String16. + * + * Note that this data structure and SharedBuffer are used interchangably + * as the underlying data structure for a String16. Therefore, the layout + * of this data structure must match the part in SharedBuffer that is + * visible to String16. + */ + template + struct StaticData { + // The high bit of 'size' is used as a flag. + static_assert(N - 1 < kIsSharedBufferAllocated, "StaticString16 too long!"); + constexpr StaticData() : size(N - 1), data{0} {} + const uint32_t size; + char16_t data[N]; + + constexpr StaticData(const StaticData&) = default; + }; + + /* + * Helper function for constructing a StaticData object. + */ + template + static constexpr const StaticData makeStaticData(const char16_t (&s)[N]) { + StaticData r; + // The 'size' field is at the same location where mClientMetadata would + // be for a SharedBuffer. We do NOT set kIsSharedBufferAllocated flag + // here. + for (size_t i = 0; i < N - 1; ++i) r.data[i] = s[i]; + return r; + } + + template + explicit constexpr String16(const StaticData& s) : mString(s.data) {} + +// These symbols are for potential backward compatibility with prebuilts. To be removed. +#ifdef ENABLE_STRING16_OBSOLETE_METHODS +public: +#else +private: +#endif + inline const char16_t* string() const; +}; + +// String16 can be trivially moved using memcpy() because moving does not +// require any change to the underlying SharedBuffer contents or reference count. +ANDROID_TRIVIAL_MOVE_TRAIT(String16) + +static inline std::ostream& operator<<(std::ostream& os, const String16& str) { + os << String8(str); + return os; +} + +// --------------------------------------------------------------------------- + +/* + * A StaticString16 object is a specialized String16 object. Instead of holding + * the string data in a ref counted SharedBuffer object, it holds data in a + * buffer within StaticString16 itself. Note that this buffer is NOT ref + * counted and is assumed to be available for as long as there is at least a + * String16 object using it. Therefore, one must be extra careful to NEVER + * assign a StaticString16 to a String16 that outlives the StaticString16 + * object. + * + * THE SAFEST APPROACH IS TO USE StaticString16 ONLY AS GLOBAL VARIABLES. + * + * A StaticString16 SHOULD NEVER APPEAR IN APIs. USE String16 INSTEAD. + */ +template +class StaticString16 : public String16 { +public: + constexpr StaticString16(const char16_t (&s)[N]) : String16(mData), mData(makeStaticData(s)) {} + + constexpr StaticString16(const StaticString16& other) + : String16(mData), mData(other.mData) {} + + constexpr StaticString16(const StaticString16&&) = delete; + + // There is no reason why one would want to 'new' a StaticString16. Delete + // it to discourage misuse. + static void* operator new(std::size_t) = delete; + +private: + const StaticData mData; +}; + +template +StaticString16(const F&)->StaticString16; + +// --------------------------------------------------------------------------- +// No user servicable parts below. + +inline int compare_type(const String16& lhs, const String16& rhs) +{ + return lhs.compare(rhs); +} + +inline int strictly_order_type(const String16& lhs, const String16& rhs) +{ + return compare_type(lhs, rhs) < 0; +} + +inline const char16_t* String16::c_str() const +{ + return mString; +} + +inline const char16_t* String16::string() const +{ + return mString; +} + +inline bool String16::empty() const +{ + return length() == 0; +} + +inline size_t String16::length() const +{ + return size(); +} + +inline bool String16::contains(const String16& other) const +{ + return contains(other.c_str()); +} + +inline String16& String16::operator=(const String16& other) +{ + setTo(other); + return *this; +} + +inline String16& String16::operator+=(const String16& other) +{ + append(other); + return *this; +} + +inline String16 String16::operator+(const String16& other) const +{ + String16 tmp(*this); + tmp += other; + return tmp; +} + +inline int String16::compare(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()); +} + +inline bool String16::operator<(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) < 0; +} + +inline bool String16::operator<=(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) <= 0; +} + +inline bool String16::operator==(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) == 0; +} + +inline bool String16::operator!=(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) != 0; +} + +inline bool String16::operator>=(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) >= 0; +} + +inline bool String16::operator>(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) > 0; +} + +#if __cplusplus >= 202002L +inline std::strong_ordering String16::operator<=>(const String16& other) const { + int result = strzcmp16(mString, size(), other.mString, other.size()); + if (result == 0) { + return std::strong_ordering::equal; + } else if (result < 0) { + return std::strong_ordering::less; + } else { + return std::strong_ordering::greater; + } +} +#endif + +inline bool String16::operator<(const char16_t* other) const +{ + return strcmp16(mString, other) < 0; +} + +inline bool String16::operator<=(const char16_t* other) const +{ + return strcmp16(mString, other) <= 0; +} + +inline bool String16::operator==(const char16_t* other) const +{ + return strcmp16(mString, other) == 0; +} + +inline bool String16::operator!=(const char16_t* other) const +{ + return strcmp16(mString, other) != 0; +} + +inline bool String16::operator>=(const char16_t* other) const +{ + return strcmp16(mString, other) >= 0; +} + +inline bool String16::operator>(const char16_t* other) const +{ + return strcmp16(mString, other) > 0; +} + +#if __cplusplus >= 202002L +inline std::strong_ordering String16::operator<=>(const char16_t* other) const { + int result = strcmp16(mString, other); + if (result == 0) { + return std::strong_ordering::equal; + } else if (result < 0) { + return std::strong_ordering::less; + } else { + return std::strong_ordering::greater; + } +} +#endif + +inline String16::operator const char16_t*() const +{ + return mString; +} + +inline String16::operator std::u16string_view() const +{ + return {mString, length()}; +} + +} // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_STRING16_H diff --git a/sysbridge/src/main/cpp/android/utils/String8.h b/sysbridge/src/main/cpp/android/utils/String8.h new file mode 100644 index 0000000000..482e95eeb5 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/String8.h @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_STRING8_H +#define ANDROID_STRING8_H + +#include +#include +#include + +#include // for strcmp +#include +#include "Errors.h" +#include "Unicode.h" +#include "TypeHelpers.h" + +#if __cplusplus >= 202002L +#include +#endif + +// --------------------------------------------------------------------------- + +namespace android { + + class String16; + +// DO NOT USE: please use std::string + +//! This is a string holding UTF-8 characters. Does not allow the value more +// than 0x10FFFF, which is not valid unicode codepoint. + class String8 { + public: + String8(); + + String8(const String8 &o); + + explicit String8(const char *o); + + explicit String8(const char *o, size_t numChars); + + explicit String8(std::string_view o); + + explicit String8(const String16 &o); + + explicit String8(const char16_t *o); + + explicit String8(const char16_t *o, size_t numChars); + + explicit String8(const char32_t *o); + + explicit String8(const char32_t *o, size_t numChars); + + ~String8(); + + static String8 format(const char *fmt, ...) __attribute__((format (printf, 1, 2))); + + static String8 formatV(const char *fmt, va_list args); + + inline const char *c_str() const; + + inline size_t size() const; + + inline size_t bytes() const; + + inline bool empty() const; + + size_t length() const; + + void clear(); + + void setTo(const String8 &other); + + status_t setTo(const char *other); + + status_t setTo(const char *other, size_t numChars); + + status_t setTo(const char16_t *other, size_t numChars); + + status_t setTo(const char32_t *other, + size_t length); + + status_t append(const String8 &other); + + status_t append(const char *other); + + status_t append(const char *other, size_t numChars); + + status_t appendFormat(const char *fmt, ...) + __attribute__((format (printf, 2, 3))); + + status_t appendFormatV(const char *fmt, va_list args); + + inline String8 &operator=(const String8 &other); + + inline String8 &operator=(const char *other); + + inline String8 &operator+=(const String8 &other); + + inline String8 operator+(const String8 &other) const; + + inline String8 &operator+=(const char *other); + + inline String8 operator+(const char *other) const; + + inline int compare(const String8 &other) const; + + inline bool operator<(const String8 &other) const; + + inline bool operator<=(const String8 &other) const; + + inline bool operator==(const String8 &other) const; + + inline bool operator!=(const String8 &other) const; + + inline bool operator>=(const String8 &other) const; + + inline bool operator>(const String8 &other) const; + +#if __cplusplus >= 202002L + inline std::strong_ordering operator<=>(const String8& other) const; +#endif + + inline bool operator<(const char *other) const; + + inline bool operator<=(const char *other) const; + + inline bool operator==(const char *other) const; + + inline bool operator!=(const char *other) const; + + inline bool operator>=(const char *other) const; + + inline bool operator>(const char *other) const; + +#if __cplusplus >= 202002L + inline std::strong_ordering operator<=>(const char* other) const; +#endif + + inline operator const char *() const; + + inline explicit operator std::string_view() const; + + char *lockBuffer(size_t size); + + void unlockBuffer(); + + status_t unlockBuffer(size_t size); + + // return the index of the first byte of other in this at or after + // start, or -1 if not found + ssize_t find(const char *other, size_t start = 0) const; + + inline ssize_t find(const String8 &other, size_t start = 0) const; + + // return true if this string contains the specified substring + inline bool contains(const char *other) const; + + inline bool contains(const String8 &other) const; + + // removes all occurrence of the specified substring + // returns true if any were found and removed + bool removeAll(const char *other); + + inline bool removeAll(const String8 &other); + + void toLower(); + + private: + String8 getPathDir(void) const; + + String8 getPathExtension(void) const; + + status_t real_append(const char *other, size_t numChars); + + const char *mString; + +// These symbols are for potential backward compatibility with prebuilts. To be removed. +#ifdef ENABLE_STRING8_OBSOLETE_METHODS + public: +#else + private: +#endif + + inline const char *string() const; + + inline bool isEmpty() const; + }; + +// String8 can be trivially moved using memcpy() because moving does not +// require any change to the underlying SharedBuffer contents or reference count. + ANDROID_TRIVIAL_MOVE_TRAIT(String8) + + static inline std::ostream &operator<<(std::ostream &os, const String8 &str) { + os << str.c_str(); + return os; + } + +// --------------------------------------------------------------------------- +// No user servicable parts below. + + inline int compare_type(const String8 &lhs, const String8 &rhs) { + return lhs.compare(rhs); + } + + inline int strictly_order_type(const String8 &lhs, const String8 &rhs) { + return compare_type(lhs, rhs) < 0; + } + + inline const char *String8::c_str() const { + return mString; + } + + inline const char *String8::string() const { + return mString; + } + + inline size_t String8::size() const { + return length(); + } + + inline bool String8::empty() const { + return length() == 0; + } + + inline bool String8::isEmpty() const { + return length() == 0; + } + + inline size_t String8::bytes() const { + return length(); + } + + inline ssize_t String8::find(const String8 &other, size_t start) const { + return find(other.c_str(), start); + } + + inline bool String8::contains(const char *other) const { + return find(other) >= 0; + } + + inline bool String8::contains(const String8 &other) const { + return contains(other.c_str()); + } + + inline bool String8::removeAll(const String8 &other) { + return removeAll(other.c_str()); + } + + inline String8 &String8::operator=(const String8 &other) { + setTo(other); + return *this; + } + + inline String8 &String8::operator=(const char *other) { + setTo(other); + return *this; + } + + inline String8 &String8::operator+=(const String8 &other) { + append(other); + return *this; + } + + inline String8 String8::operator+(const String8 &other) const { + String8 tmp(*this); + tmp += other; + return tmp; + } + + inline String8 &String8::operator+=(const char *other) { + append(other); + return *this; + } + + inline String8 String8::operator+(const char *other) const { + String8 tmp(*this); + tmp += other; + return tmp; + } + + inline int String8::compare(const String8 &other) const { + return strcmp(mString, other.mString); + } + + inline bool String8::operator<(const String8 &other) const { + return strcmp(mString, other.mString) < 0; + } + + inline bool String8::operator<=(const String8 &other) const { + return strcmp(mString, other.mString) <= 0; + } + + inline bool String8::operator==(const String8 &other) const { + return strcmp(mString, other.mString) == 0; + } + + inline bool String8::operator!=(const String8 &other) const { + return strcmp(mString, other.mString) != 0; + } + + inline bool String8::operator>=(const String8 &other) const { + return strcmp(mString, other.mString) >= 0; + } + + inline bool String8::operator>(const String8 &other) const { + return strcmp(mString, other.mString) > 0; + } + +#if __cplusplus >= 202002L + inline std::strong_ordering String8::operator<=>(const String8& other) const { + int result = strcmp(mString, other.mString); + if (result == 0) { + return std::strong_ordering::equal; + } else if (result < 0) { + return std::strong_ordering::less; + } else { + return std::strong_ordering::greater; + } + } +#endif + + inline bool String8::operator<(const char *other) const { + return strcmp(mString, other) < 0; + } + + inline bool String8::operator<=(const char *other) const { + return strcmp(mString, other) <= 0; + } + + inline bool String8::operator==(const char *other) const { + return strcmp(mString, other) == 0; + } + + inline bool String8::operator!=(const char *other) const { + return strcmp(mString, other) != 0; + } + + inline bool String8::operator>=(const char *other) const { + return strcmp(mString, other) >= 0; + } + + inline bool String8::operator>(const char *other) const { + return strcmp(mString, other) > 0; + } + +#if __cplusplus >= 202002L + inline std::strong_ordering String8::operator<=>(const char* other) const { + int result = strcmp(mString, other); + if (result == 0) { + return std::strong_ordering::equal; + } else if (result < 0) { + return std::strong_ordering::less; + } else { + return std::strong_ordering::greater; + } + } +#endif + + inline String8::operator const char *() const { + return mString; + } + + inline String8::String8(std::string_view o) : String8(o.data(), o.length()) {} + + inline String8::operator std::string_view() const { + return {mString, length()}; + } + +} // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_STRING8_H diff --git a/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp new file mode 100644 index 0000000000..2a6aae0c7c --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "Tokenizer" + +#include "Tokenizer.h" +#include "Errors.h" +#include "FileMap.h" +#include "String8.h" +#include +#include +#include +#include +#include + +#ifndef DEBUG_TOKENIZER +// Enables debug output for the tokenizer. +#define DEBUG_TOKENIZER 0 +#endif + +namespace android { + + static inline bool isDelimiter(char ch, const char *delimiters) { + return strchr(delimiters, ch) != nullptr; + } + + Tokenizer::Tokenizer(const String8 &filename, FileMap *fileMap, char *buffer, + bool ownBuffer, size_t length) : + mFilename(filename), mFileMap(fileMap), + mBuffer(buffer), mOwnBuffer(ownBuffer), mLength(length), + mCurrent(buffer), mLineNumber(1) { + } + + Tokenizer::~Tokenizer() { + delete mFileMap; + if (mOwnBuffer) { + delete[] mBuffer; + } + } + + status_t Tokenizer::open(const String8 &filename, Tokenizer **outTokenizer) { + *outTokenizer = nullptr; + + int result = OK; + int fd = ::open(filename.c_str(), O_RDONLY); + if (fd < 0) { + result = -errno; + __android_log_print(ANDROID_LOG_ERROR, "Error opening file '%s': %s", filename.c_str(), + strerror(errno)); + } else { + struct stat stat; + if (fstat(fd, &stat)) { + result = -errno; + __android_log_print(ANDROID_LOG_ERROR, "Error getting size of file '%s': %s", filename.c_str(), strerror(errno)); + } else { + size_t length = size_t(stat.st_size); + + FileMap *fileMap = new FileMap(); + bool ownBuffer = false; + char *buffer; + if (fileMap->create(nullptr, fd, 0, length, true)) { + fileMap->advise(FileMap::SEQUENTIAL); + buffer = static_cast(fileMap->getDataPtr()); + } else { + delete fileMap; + fileMap = nullptr; + + // Fall back to reading into a buffer since we can't mmap files in sysfs. + // The length we obtained from stat is wrong too (it will always be 4096) + // so we must trust that read will read the entire file. + buffer = new char[length]; + ownBuffer = true; + ssize_t nrd = read(fd, buffer, length); + if (nrd < 0) { + result = -errno; + __android_log_print(ANDROID_LOG_ERROR, "Error reading file '%s': %s", + filename.c_str(), strerror(errno)); + delete[] buffer; + buffer = nullptr; + } else { + length = size_t(nrd); + } + } + + if (!result) { + *outTokenizer = new Tokenizer(filename, fileMap, buffer, ownBuffer, length); + } + } + close(fd); + } + return result; + } + + status_t Tokenizer::fromContents(const String8 &filename, + const char *contents, Tokenizer **outTokenizer) { + *outTokenizer = new Tokenizer(filename, nullptr, + const_cast(contents), false, strlen(contents)); + return OK; + } + + String8 Tokenizer::getLocation() const { + String8 result; + result.appendFormat("%s:%d", mFilename.c_str(), mLineNumber); + return result; + } + + String8 Tokenizer::peekRemainderOfLine() const { + const char *end = getEnd(); + const char *eol = mCurrent; + while (eol != end) { + char ch = *eol; + if (ch == '\n') { + break; + } + eol += 1; + } + return String8(mCurrent, eol - mCurrent); + } + + String8 Tokenizer::nextToken(const char *delimiters) { +#if DEBUG_TOKENIZER + ALOGD("nextToken"); +#endif + const char *end = getEnd(); + const char *tokenStart = mCurrent; + while (mCurrent != end) { + char ch = *mCurrent; + if (ch == '\n' || isDelimiter(ch, delimiters)) { + break; + } + mCurrent += 1; + } + return String8(tokenStart, mCurrent - tokenStart); + } + + void Tokenizer::nextLine() { +#if DEBUG_TOKENIZER + ALOGD("nextLine"); +#endif + const char *end = getEnd(); + while (mCurrent != end) { + char ch = *(mCurrent++); + if (ch == '\n') { + mLineNumber += 1; + break; + } + } + } + + void Tokenizer::skipDelimiters(const char *delimiters) { +#if DEBUG_TOKENIZER + ALOGD("skipDelimiters"); +#endif + const char *end = getEnd(); + while (mCurrent != end) { + char ch = *mCurrent; + if (ch == '\n' || !isDelimiter(ch, delimiters)) { + break; + } + mCurrent += 1; + } + } + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/Tokenizer.h b/sysbridge/src/main/cpp/android/utils/Tokenizer.h new file mode 100644 index 0000000000..dee36a7bb0 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Tokenizer.h @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _UTILS_TOKENIZER_H +#define _UTILS_TOKENIZER_H + +#include +#include +#include "FileMap.h" +#include "Errors.h" +#include "String8.h" + +namespace android { + +/** + * A simple tokenizer for loading and parsing ASCII text files line by line. + */ +class Tokenizer { + Tokenizer(const String8& filename, FileMap* fileMap, char* buffer, + bool ownBuffer, size_t length); + +public: + ~Tokenizer(); + + /** + * Opens a file and maps it into memory. + * + * Returns OK and a tokenizer for the file, if successful. + * Otherwise returns an error and sets outTokenizer to NULL. + */ + static status_t open(const String8& filename, Tokenizer** outTokenizer); + + /** + * Prepares to tokenize the contents of a string. + * + * Returns OK and a tokenizer for the string, if successful. + * Otherwise returns an error and sets outTokenizer to NULL. + */ + static status_t fromContents(const String8& filename, + const char* contents, Tokenizer** outTokenizer); + + /** + * Returns true if at the end of the file. + */ + inline bool isEof() const { return mCurrent == getEnd(); } + + /** + * Returns true if at the end of the line or end of the file. + */ + inline bool isEol() const { return isEof() || *mCurrent == '\n'; } + + /** + * Gets the name of the file. + */ + inline String8 getFilename() const { return mFilename; } + + /** + * Gets a 1-based line number index for the current position. + */ + inline int32_t getLineNumber() const { return mLineNumber; } + + /** + * Formats a location string consisting of the filename and current line number. + * Returns a string like "MyFile.txt:33". + */ + String8 getLocation() const; + + /** + * Gets the character at the current position. + * Returns null at end of file. + */ + inline char peekChar() const { return isEof() ? '\0' : *mCurrent; } + + /** + * Gets the remainder of the current line as a string, excluding the newline character. + */ + String8 peekRemainderOfLine() const; + + /** + * Gets the character at the current position and advances past it. + * Returns null at end of file. + */ + inline char nextChar() { return isEof() ? '\0' : *(mCurrent++); } + + /** + * Gets the next token on this line stopping at the specified delimiters + * or the end of the line whichever comes first and advances past it. + * Also stops at embedded nulls. + * Returns the token or an empty string if the current character is a delimiter + * or is at the end of the line. + */ + String8 nextToken(const char* delimiters); + + /** + * Advances to the next line. + * Does nothing if already at the end of the file. + */ + void nextLine(); + + /** + * Skips over the specified delimiters in the line. + * Also skips embedded nulls. + */ + void skipDelimiters(const char* delimiters); + +private: + Tokenizer(const Tokenizer& other); // not copyable + + String8 mFilename; + FileMap* mFileMap; + char* mBuffer; + bool mOwnBuffer; + size_t mLength; + + const char* mCurrent; + int32_t mLineNumber; + + inline const char* getEnd() const { return mBuffer + mLength; } + +}; + +} // namespace android + +#endif // _UTILS_TOKENIZER_H diff --git a/sysbridge/src/main/cpp/android/utils/TypeHelpers.h b/sysbridge/src/main/cpp/android/utils/TypeHelpers.h new file mode 100644 index 0000000000..007036bbdb --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/TypeHelpers.h @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_TYPE_HELPERS_H +#define ANDROID_TYPE_HELPERS_H + +#include +#include + +#include +#include +#include + +// --------------------------------------------------------------------------- + +namespace android { + +/* + * Types traits + */ + +template struct trait_trivial_ctor { enum { value = false }; }; +template struct trait_trivial_dtor { enum { value = false }; }; +template struct trait_trivial_copy { enum { value = false }; }; +template struct trait_trivial_move { enum { value = false }; }; +template struct trait_pointer { enum { value = false }; }; +template struct trait_pointer { enum { value = true }; }; + +template +struct traits { + enum { + // whether this type is a pointer + is_pointer = trait_pointer::value, + // whether this type's constructor is a no-op + has_trivial_ctor = is_pointer || trait_trivial_ctor::value, + // whether this type's destructor is a no-op + has_trivial_dtor = is_pointer || trait_trivial_dtor::value, + // whether this type type can be copy-constructed with memcpy + has_trivial_copy = is_pointer || trait_trivial_copy::value, + // whether this type can be moved with memmove + has_trivial_move = is_pointer || trait_trivial_move::value + }; +}; + +template +struct aggregate_traits { + enum { + is_pointer = false, + has_trivial_ctor = + traits::has_trivial_ctor && traits::has_trivial_ctor, + has_trivial_dtor = + traits::has_trivial_dtor && traits::has_trivial_dtor, + has_trivial_copy = + traits::has_trivial_copy && traits::has_trivial_copy, + has_trivial_move = + traits::has_trivial_move && traits::has_trivial_move + }; +}; + +#define ANDROID_TRIVIAL_CTOR_TRAIT( T ) \ + template<> struct trait_trivial_ctor< T > { enum { value = true }; }; + +#define ANDROID_TRIVIAL_DTOR_TRAIT( T ) \ + template<> struct trait_trivial_dtor< T > { enum { value = true }; }; + +#define ANDROID_TRIVIAL_COPY_TRAIT( T ) \ + template<> struct trait_trivial_copy< T > { enum { value = true }; }; + +#define ANDROID_TRIVIAL_MOVE_TRAIT( T ) \ + template<> struct trait_trivial_move< T > { enum { value = true }; }; + +#define ANDROID_BASIC_TYPES_TRAITS( T ) \ + ANDROID_TRIVIAL_CTOR_TRAIT( T ) \ + ANDROID_TRIVIAL_DTOR_TRAIT( T ) \ + ANDROID_TRIVIAL_COPY_TRAIT( T ) \ + ANDROID_TRIVIAL_MOVE_TRAIT( T ) + +// --------------------------------------------------------------------------- + +/* + * basic types traits + */ + +ANDROID_BASIC_TYPES_TRAITS( void ) +ANDROID_BASIC_TYPES_TRAITS( bool ) +ANDROID_BASIC_TYPES_TRAITS( char ) +ANDROID_BASIC_TYPES_TRAITS( unsigned char ) +ANDROID_BASIC_TYPES_TRAITS( short ) +ANDROID_BASIC_TYPES_TRAITS( unsigned short ) +ANDROID_BASIC_TYPES_TRAITS( int ) +ANDROID_BASIC_TYPES_TRAITS( unsigned int ) +ANDROID_BASIC_TYPES_TRAITS( long ) +ANDROID_BASIC_TYPES_TRAITS( unsigned long ) +ANDROID_BASIC_TYPES_TRAITS( long long ) +ANDROID_BASIC_TYPES_TRAITS( unsigned long long ) +ANDROID_BASIC_TYPES_TRAITS( float ) +ANDROID_BASIC_TYPES_TRAITS( double ) + +template struct trait_trivial_ctor { enum { value = true }; }; +template struct trait_trivial_dtor { enum { value = true }; }; +template struct trait_trivial_copy { enum { value = true }; }; +template struct trait_trivial_move { enum { value = true }; }; + +// --------------------------------------------------------------------------- + + +/* + * compare and order types + */ + +template inline +int strictly_order_type(const TYPE& lhs, const TYPE& rhs) { + return (lhs < rhs) ? 1 : 0; +} + +template inline +int compare_type(const TYPE& lhs, const TYPE& rhs) { + return strictly_order_type(rhs, lhs) - strictly_order_type(lhs, rhs); +} + +/* + * create, destroy, copy and move types... + */ + +template inline +void construct_type(TYPE* p, size_t n) { + if (!traits::has_trivial_ctor) { + while (n > 0) { + n--; + new(p++) TYPE; + } + } +} + +template inline +void destroy_type(TYPE* p, size_t n) { + if (!traits::has_trivial_dtor) { + while (n > 0) { + n--; + p->~TYPE(); + p++; + } + } +} + +template +typename std::enable_if::has_trivial_copy>::type +inline +copy_type(TYPE* d, const TYPE* s, size_t n) { + memcpy(d,s,n*sizeof(TYPE)); +} + +template +typename std::enable_if::has_trivial_copy>::type +inline +copy_type(TYPE* d, const TYPE* s, size_t n) { + while (n > 0) { + n--; + new(d) TYPE(*s); + d++, s++; + } +} + +template inline +void splat_type(TYPE* where, const TYPE* what, size_t n) { + if (!traits::has_trivial_copy) { + while (n > 0) { + n--; + new(where) TYPE(*what); + where++; + } + } else { + while (n > 0) { + n--; + *where++ = *what; + } + } +} + +template +struct use_trivial_move : public std::integral_constant::has_trivial_dtor && traits::has_trivial_copy) + || traits::has_trivial_move +> {}; + +template +typename std::enable_if::value>::type +inline +move_forward_type(TYPE* d, const TYPE* s, size_t n = 1) { + memmove(reinterpret_cast(d), s, n * sizeof(TYPE)); +} + +template +typename std::enable_if::value>::type +inline +move_forward_type(TYPE* d, const TYPE* s, size_t n = 1) { + d += n; + s += n; + while (n > 0) { + n--; + --d, --s; + if (!traits::has_trivial_copy) { + new(d) TYPE(*s); + } else { + *d = *s; + } + if (!traits::has_trivial_dtor) { + s->~TYPE(); + } + } +} + +template +typename std::enable_if::value>::type +inline +move_backward_type(TYPE* d, const TYPE* s, size_t n = 1) { + memmove(reinterpret_cast(d), s, n * sizeof(TYPE)); +} + +template +typename std::enable_if::value>::type +inline +move_backward_type(TYPE* d, const TYPE* s, size_t n = 1) { + while (n > 0) { + n--; + if (!traits::has_trivial_copy) { + new(d) TYPE(*s); + } else { + *d = *s; + } + if (!traits::has_trivial_dtor) { + s->~TYPE(); + } + d++, s++; + } +} + +// --------------------------------------------------------------------------- + +/* + * a key/value pair + */ + +template +struct key_value_pair_t { + typedef KEY key_t; + typedef VALUE value_t; + + KEY key; + VALUE value; + key_value_pair_t() { } + key_value_pair_t(const key_value_pair_t& o) : key(o.key), value(o.value) { } + key_value_pair_t& operator=(const key_value_pair_t& o) { + key = o.key; + value = o.value; + return *this; + } + key_value_pair_t(const KEY& k, const VALUE& v) : key(k), value(v) { } + explicit key_value_pair_t(const KEY& k) : key(k) { } + inline bool operator < (const key_value_pair_t& o) const { + return strictly_order_type(key, o.key); + } + inline const KEY& getKey() const { + return key; + } + inline const VALUE& getValue() const { + return value; + } +}; + +template +struct trait_trivial_ctor< key_value_pair_t > +{ enum { value = aggregate_traits::has_trivial_ctor }; }; +template +struct trait_trivial_dtor< key_value_pair_t > +{ enum { value = aggregate_traits::has_trivial_dtor }; }; +template +struct trait_trivial_copy< key_value_pair_t > +{ enum { value = aggregate_traits::has_trivial_copy }; }; +template +struct trait_trivial_move< key_value_pair_t > +{ enum { value = aggregate_traits::has_trivial_move }; }; + +// --------------------------------------------------------------------------- + +/* + * Hash codes. + */ +typedef uint32_t hash_t; + +template +hash_t hash_type(const TKey& key); + +/* Built-in hash code specializations */ +#define ANDROID_INT32_HASH(T) \ + template <> inline hash_t hash_type(const T& value) { return hash_t(value); } +#define ANDROID_INT64_HASH(T) \ + template <> inline hash_t hash_type(const T& value) { \ + return hash_t((value >> 32) ^ value); } +#define ANDROID_REINTERPRET_HASH(T, R) \ + template <> inline hash_t hash_type(const T& value) { \ + R newValue; \ + static_assert(sizeof(newValue) == sizeof(value), "size mismatch"); \ + memcpy(&newValue, &value, sizeof(newValue)); \ + return hash_type(newValue); \ + } + +ANDROID_INT32_HASH(bool) +ANDROID_INT32_HASH(int8_t) +ANDROID_INT32_HASH(uint8_t) +ANDROID_INT32_HASH(int16_t) +ANDROID_INT32_HASH(uint16_t) +ANDROID_INT32_HASH(int32_t) +ANDROID_INT32_HASH(uint32_t) +ANDROID_INT64_HASH(int64_t) +ANDROID_INT64_HASH(uint64_t) +ANDROID_REINTERPRET_HASH(float, uint32_t) +ANDROID_REINTERPRET_HASH(double, uint64_t) + +template inline hash_t hash_type(T* const & value) { + return hash_type(uintptr_t(value)); +} + +} // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_TYPE_HELPERS_H diff --git a/sysbridge/src/main/cpp/android/utils/Unicode.h b/sysbridge/src/main/cpp/android/utils/Unicode.h new file mode 100644 index 0000000000..d60d5d6ba6 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Unicode.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UNICODE_H +#define ANDROID_UNICODE_H + +#include +#include + +extern "C" { + +// Standard string functions on char16_t strings. +int strcmp16(const char16_t *, const char16_t *); +int strncmp16(const char16_t *s1, const char16_t *s2, size_t n); +size_t strlen16(const char16_t *); +size_t strnlen16(const char16_t *, size_t); +char16_t *strstr16(const char16_t*, const char16_t*); + +// Version of comparison that supports embedded NULs. +// This is different than strncmp() because we don't stop +// at a nul character and consider the strings to be different +// if the lengths are different (thus we need to supply the +// lengths of both strings). This can also be used when +// your string is not nul-terminated as it will have the +// equivalent result as strcmp16 (unlike strncmp16). +int strzcmp16(const char16_t *s1, size_t n1, const char16_t *s2, size_t n2); + +/** + * Measure the length of a UTF-32 string in UTF-8. If the string is invalid + * such as containing a surrogate character, -1 will be returned. + */ +ssize_t utf32_to_utf8_length(const char32_t *src, size_t src_len); + +/** + * Stores a UTF-8 string converted from "src" in "dst", if "dst_length" is not + * large enough to store the string, the part of the "src" string is stored + * into "dst" as much as possible. See the examples for more detail. + * Returns the size actually used for storing the string. + * dst" is not nul-terminated when dst_len is fully used (like strncpy). + * + * \code + * Example 1 + * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84) + * "src_len" == 2 + * "dst_len" >= 7 + * -> + * Returned value == 6 + * "dst" becomes \xE3\x81\x82\xE3\x81\x84\0 + * (note that "dst" is nul-terminated) + * + * Example 2 + * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84) + * "src_len" == 2 + * "dst_len" == 5 + * -> + * Returned value == 3 + * "dst" becomes \xE3\x81\x82\0 + * (note that "dst" is nul-terminated, but \u3044 is not stored in "dst" + * since "dst" does not have enough size to store the character) + * + * Example 3 + * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84) + * "src_len" == 2 + * "dst_len" == 6 + * -> + * Returned value == 6 + * "dst" becomes \xE3\x81\x82\xE3\x81\x84 + * (note that "dst" is NOT nul-terminated, like strncpy) + * \endcode + */ +void utf32_to_utf8(const char32_t* src, size_t src_len, char* dst, size_t dst_len); + +/** + * Returns the unicode value at "index". + * Returns -1 when the index is invalid (equals to or more than "src_len"). + * If returned value is positive, it is able to be converted to char32_t, which + * is unsigned. Then, if "next_index" is not NULL, the next index to be used is + * stored in "next_index". "next_index" can be NULL. + */ +int32_t utf32_from_utf8_at(const char *src, size_t src_len, size_t index, size_t *next_index); + + +/** + * Returns the UTF-8 length of UTF-16 string "src". + */ +ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len); + +/** + * Converts a UTF-16 string to UTF-8. The destination buffer must be large + * enough to fit the UTF-16 as measured by utf16_to_utf8_length with an added + * NUL terminator. + */ +void utf16_to_utf8(const char16_t* src, size_t src_len, char* dst, size_t dst_len); + +/** + * Returns the UTF-16 length of UTF-8 string "src". Returns -1 in case + * it's invalid utf8. No buffer over-read occurs because of bound checks. Using overreadIsFatal you + * can ask to log a message and fail in case the invalid utf8 could have caused an override if no + * bound checks were used (otherwise -1 is returned). + */ +ssize_t utf8_to_utf16_length(const uint8_t* src, size_t srcLen, bool overreadIsFatal = false); + +/** + * Convert UTF-8 to UTF-16 including surrogate pairs. + * Returns a pointer to the end of the string (where a NUL terminator might go + * if you wanted to add one). At most dstLen characters are written; it won't emit half a surrogate + * pair. If dstLen == 0 nothing is written and dst is returned. If dstLen > SSIZE_MAX it aborts + * (this being probably a negative number returned as an error and casted to unsigned). + */ +char16_t* utf8_to_utf16_no_null_terminator( + const uint8_t* src, size_t srcLen, char16_t* dst, size_t dstLen); + +/** + * Convert UTF-8 to UTF-16 including surrogate pairs. At most dstLen - 1 + * characters are written; it won't emit half a surrogate pair; and a NUL terminator is appended + * after. dstLen - 1 can be measured beforehand using utf8_to_utf16_length. Aborts if dstLen == 0 + * (at least one character is needed for the NUL terminator) or dstLen > SSIZE_MAX (the latter + * case being likely a negative number returned as an error and casted to unsigned) . Returns a + * pointer to the NUL terminator. + */ +char16_t *utf8_to_utf16( + const uint8_t* src, size_t srcLen, char16_t* dst, size_t dstLen); + +} + +#endif diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 8a9ccded67..b85adb9ffa 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -50,10 +50,11 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI 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", + __android_log_print(ANDROID_LOG_ERROR, "Key Mapper", "Event: %s %s %d, Event code: %d\n", libevdev_event_type_get_name(ev.type), libevdev_event_code_get_name(ev.type, ev.code), - ev.value); + ev.value, + ev.code); } while (rc == 1 || rc == 0 || rc == -EAGAIN); return env->NewStringUTF("Hello!"); From 058ee7b26f550adf8d6efa264b51655e684df238 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 22 Jul 2025 21:51:57 -0600 Subject: [PATCH 023/215] #1394 replace uses of fmtlib with std::format --- .../src/main/cpp/android/libbase/format.h | 33 ------------------- .../src/main/cpp/android/libbase/result.h | 18 +++++----- 2 files changed, 9 insertions(+), 42 deletions(-) delete mode 100644 sysbridge/src/main/cpp/android/libbase/format.h diff --git a/sysbridge/src/main/cpp/android/libbase/format.h b/sysbridge/src/main/cpp/android/libbase/format.h deleted file mode 100644 index 065051351b..0000000000 --- a/sysbridge/src/main/cpp/android/libbase/format.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -// We include fmtlib here as an alias, since libbase will have fmtlib statically linked already. -// It is accessed through its normal fmt:: namespace. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wshadow" -#include -#pragma clang diagnostic pop -#include -#include -#include -#include - -#ifndef _WIN32 -#include -#include -#endif // _WIN32 diff --git a/sysbridge/src/main/cpp/android/libbase/result.h b/sysbridge/src/main/cpp/android/libbase/result.h index 04c15d772a..edbb56e8f2 100644 --- a/sysbridge/src/main/cpp/android/libbase/result.h +++ b/sysbridge/src/main/cpp/android/libbase/result.h @@ -98,10 +98,10 @@ #include #include #include +#include #include "errors.h" #include "expected.h" -#include "format.h" namespace android { namespace base { @@ -265,10 +265,10 @@ class Error { Error& operator=(Error&&) = delete; template - friend Error ErrorfImpl(fmt::format_string fmt, const Args&... args); + friend Error ErrorfImpl(const std::string &fmt, const Args &... args); template - friend Error ErrnoErrorfImpl(fmt::format_string fmt, const Args&... args); + friend Error ErrnoErrorfImpl(const std::string &fmt, const Args &... args); private: Error(bool has_code, E code, const std::string& message) : code_(code), has_code_(has_code) { @@ -303,19 +303,19 @@ __attribute__((noinline)) ResultError MakeResultErrorWithCode(std::string Errno code); template -inline ResultError ErrorfImpl(fmt::format_string fmt, const Args&... args) { - return ResultError(fmt::vformat(fmt.get(), fmt::make_format_args(args...)), +inline ResultError ErrorfImpl(const std::string &fmt, const Args &... args) { + return ResultError(std::vformat(fmt, std::make_format_args(args...)), ErrorCode(Errno{}, args...)); } template -inline ResultError ErrnoErrorfImpl(fmt::format_string fmt, const Args&... args) { +inline ResultError ErrnoErrorfImpl(const std::string &fmt, const Args &... args) { Errno code{errno}; - return MakeResultErrorWithCode(fmt::vformat(fmt.get(), fmt::make_format_args(args...)), code); + return MakeResultErrorWithCode(std::vformat(fmt, std::make_format_args(args...)), code); } -#define Errorf(fmt, ...) android::base::ErrorfImpl(FMT_STRING(fmt), ##__VA_ARGS__) -#define ErrnoErrorf(fmt, ...) android::base::ErrnoErrorfImpl(FMT_STRING(fmt), ##__VA_ARGS__) +#define Errorf(fmt, ...) android::base::ErrorfImpl(fmt, ##__VA_ARGS__) +#define ErrnoErrorf(fmt, ...) android::base::ErrnoErrorfImpl(fmt, ##__VA_ARGS__) template using Result = android::base::expected>; From 725f5eae6ae7225525fab5503baf7644a5c2a826 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 22 Jul 2025 22:17:19 -0600 Subject: [PATCH 024/215] #1394 use log macros in logging.h in android files --- .../main/cpp/android/input/KeyLayoutMap.cpp | 102 +++++++----------- .../src/main/cpp/android/utils/Tokenizer.cpp | 12 +-- 2 files changed, 41 insertions(+), 73 deletions(-) diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp index 56468309f2..0d385b93c7 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -14,9 +14,7 @@ * limitations under the License. */ -#define LOG_TAG "KeyLayoutMap" - -#include +#include "../logging.h" #include #include "../utils/String8.h" #include "KeyLayoutMap.h" @@ -29,23 +27,9 @@ #include #include "../libbase/result.h" -/** - * Log debug output for the parser. - * Enable this via "adb shell setprop log.tag.KeyLayoutMapParser DEBUG" (requires restart) - */ -const bool DEBUG_PARSER = - __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "Parser", ANDROID_LOG_INFO); - // Enables debug output for parser performance. #define DEBUG_PARSER_PERFORMANCE 0 -/** - * Log debug output for mapping. - * Enable this via "adb shell setprop log.tag.KeyLayoutMapMapping DEBUG" (requires restart) - */ -const bool DEBUG_MAPPING = - __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "Mapping", ANDROID_LOG_INFO); - namespace android { namespace { @@ -88,8 +72,7 @@ namespace android { status = Tokenizer::fromContents(String8(filename.c_str()), contents, &tokenizer); } if (status) { - __android_log_print(ANDROID_LOG_ERROR, "Error %d opening key layout map file %s.", - status, filename.c_str()); + LOGE("Error %d opening key layout map file %s.", status, filename.c_str()); return Errorf("Error {} opening key layout map file {}.", status, filename.c_str()); } std::unique_ptr t(tokenizer); @@ -108,7 +91,7 @@ namespace android { std::shared_ptr map = std::shared_ptr(new KeyLayoutMap()); status_t status = OK; if (!map.get()) { - __android_log_print(ANDROID_LOG_ERROR, "Error allocating key layout map."); + LOGE("Error allocating key layout map."); return Errorf("Error allocating key layout map."); } else { #if DEBUG_PARSER_PERFORMANCE @@ -241,18 +224,16 @@ namespace android { status_t status = parseRequiredKernelConfig(); if (status) return status; } else { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected keyword, got '%s'.", - mTokenizer->getLocation().c_str(), - keywordToken.c_str()); + LOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().c_str(), + keywordToken.c_str()); return BAD_VALUE; } mTokenizer->skipDelimiters(WHITESPACE); if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') { - __android_log_print(ANDROID_LOG_ERROR, - "%s: Expected end of line or trailing comment, got '%s'.", - mTokenizer->getLocation().c_str(), - mTokenizer->peekRemainderOfLine().c_str()); + LOGE("%s: Expected end of line or trailing comment, got '%s'.", + mTokenizer->getLocation().c_str(), + mTokenizer->peekRemainderOfLine().c_str()); return BAD_VALUE; } } @@ -273,17 +254,15 @@ namespace android { std::optional code = parseInt(codeToken.c_str()); if (!code) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected key %s number, got '%s'.", - mTokenizer->getLocation().c_str(), - mapUsage ? "usage" : "scan code", codeToken.c_str()); + LOGE("%s: Expected key %s number, got '%s'.", mTokenizer->getLocation().c_str(), + mapUsage ? "usage" : "scan code", codeToken.c_str()); return BAD_VALUE; } std::unordered_map &map = mapUsage ? mMap->mKeysByUsageCode : mMap->mKeysByScanCode; if (map.find(*code) != map.end()) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Duplicate entry for key %s '%s'.", - mTokenizer->getLocation().c_str(), - mapUsage ? "usage" : "scan code", codeToken.c_str()); + LOGE("%s: Duplicate entry for key %s '%s'.", mTokenizer->getLocation().c_str(), + mapUsage ? "usage" : "scan code", codeToken.c_str()); return BAD_VALUE; } @@ -291,9 +270,8 @@ namespace android { String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE); std::optional keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.c_str()); if (!keyCode) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected key code label, got '%s'.", - mTokenizer->getLocation().c_str(), - keyCodeToken.c_str()); + LOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().c_str(), + keyCodeToken.c_str()); return BAD_VALUE; } @@ -305,15 +283,13 @@ namespace android { String8 flagToken = mTokenizer->nextToken(WHITESPACE); std::optional flag = InputEventLookup::getKeyFlagByLabel(flagToken.c_str()); if (!flag) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected key flag label, got '%s'.", - mTokenizer->getLocation().c_str(), - flagToken.c_str()); + LOGE("%s: Expected key flag label, got '%s'.", mTokenizer->getLocation().c_str(), + flagToken.c_str()); return BAD_VALUE; } if (flags & *flag) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Duplicate key flag '%s'.", - mTokenizer->getLocation().c_str(), - flagToken.c_str()); + LOGE("%s: Duplicate key flag '%s'.", mTokenizer->getLocation().c_str(), + flagToken.c_str()); return BAD_VALUE; } flags |= *flag; @@ -333,15 +309,13 @@ namespace android { String8 scanCodeToken = mTokenizer->nextToken(WHITESPACE); std::optional scanCode = parseInt(scanCodeToken.c_str()); if (!scanCode) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected axis scan code number, got '%s'.", - mTokenizer->getLocation().c_str(), - scanCodeToken.c_str()); + LOGE("%s: Expected axis scan code number, got '%s'.", mTokenizer->getLocation().c_str(), + scanCodeToken.c_str()); return BAD_VALUE; } if (mMap->mAxes.find(*scanCode) != mMap->mAxes.end()) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Duplicate entry for axis scan code '%s'.", - mTokenizer->getLocation().c_str(), - scanCodeToken.c_str()); + LOGE("%s: Duplicate entry for axis scan code '%s'.", mTokenizer->getLocation().c_str(), + scanCodeToken.c_str()); return BAD_VALUE; } @@ -356,9 +330,9 @@ namespace android { String8 axisToken = mTokenizer->nextToken(WHITESPACE); std::optional axis = InputEventLookup::getAxisByLabel(axisToken.c_str()); if (!axis) { - __android_log_print(ANDROID_LOG_ERROR, - "%s: Expected inverted axis label, got '%s'.", - mTokenizer->getLocation().c_str(), axisToken.c_str()); + LOGE("%s: Expected inverted axis label, got '%s'.", + mTokenizer->getLocation().c_str(), + axisToken.c_str()); return BAD_VALUE; } axisInfo.axis = *axis; @@ -369,8 +343,8 @@ namespace android { String8 splitToken = mTokenizer->nextToken(WHITESPACE); std::optional splitValue = parseInt(splitToken.c_str()); if (!splitValue) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected split value, got '%s'.", - mTokenizer->getLocation().c_str(), splitToken.c_str()); + LOGE("%s: Expected split value, got '%s'.", mTokenizer->getLocation().c_str(), + splitToken.c_str()); return BAD_VALUE; } axisInfo.splitValue = *splitValue; @@ -379,8 +353,8 @@ namespace android { String8 lowAxisToken = mTokenizer->nextToken(WHITESPACE); std::optional axis = InputEventLookup::getAxisByLabel(lowAxisToken.c_str()); if (!axis) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected low axis label, got '%s'.", - mTokenizer->getLocation().c_str(), lowAxisToken.c_str()); + LOGE("%s: Expected low axis label, got '%s'.", mTokenizer->getLocation().c_str(), + lowAxisToken.c_str()); return BAD_VALUE; } axisInfo.axis = *axis; @@ -389,17 +363,16 @@ namespace android { String8 highAxisToken = mTokenizer->nextToken(WHITESPACE); std::optional highAxis = InputEventLookup::getAxisByLabel(highAxisToken.c_str()); if (!highAxis) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected high axis label, got '%s'.", - mTokenizer->getLocation().c_str(), highAxisToken.c_str()); + LOGE("%s: Expected high axis label, got '%s'.", mTokenizer->getLocation().c_str(), + highAxisToken.c_str()); return BAD_VALUE; } axisInfo.highAxis = *highAxis; } else { std::optional axis = InputEventLookup::getAxisByLabel(token.c_str()); if (!axis) { - __android_log_print(ANDROID_LOG_ERROR, - "%s: Expected axis label, 'split' or 'invert', got '%s'.", - mTokenizer->getLocation().c_str(), token.c_str()); + LOGE("%s: Expected axis label, 'split' or 'invert', got '%s'.", + mTokenizer->getLocation().c_str(), token.c_str()); return BAD_VALUE; } axisInfo.axis = *axis; @@ -416,15 +389,14 @@ namespace android { String8 flatToken = mTokenizer->nextToken(WHITESPACE); std::optional flatOverride = parseInt(flatToken.c_str()); if (!flatOverride) { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected flat value, got '%s'.", - mTokenizer->getLocation().c_str(), flatToken.c_str()); + LOGE("%s: Expected flat value, got '%s'.", mTokenizer->getLocation().c_str(), + flatToken.c_str()); return BAD_VALUE; } axisInfo.flatOverride = *flatOverride; } else { - __android_log_print(ANDROID_LOG_ERROR, "%s: Expected keyword 'flat', got '%s'.", - mTokenizer->getLocation().c_str(), - keywordToken.c_str()); + LOGE("%s: Expected keyword 'flat', got '%s'.", mTokenizer->getLocation().c_str(), + keywordToken.c_str()); return BAD_VALUE; } } diff --git a/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp index 2a6aae0c7c..2bf4b33881 100644 --- a/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp +++ b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp @@ -14,15 +14,13 @@ * limitations under the License. */ -#define LOG_TAG "Tokenizer" - +#include "logging.h" #include "Tokenizer.h" #include "Errors.h" #include "FileMap.h" #include "String8.h" #include #include -#include #include #include @@ -58,13 +56,12 @@ namespace android { int fd = ::open(filename.c_str(), O_RDONLY); if (fd < 0) { result = -errno; - __android_log_print(ANDROID_LOG_ERROR, "Error opening file '%s': %s", filename.c_str(), - strerror(errno)); + LOGE("Error opening file '%s': %s", filename.c_str(), strerror(errno)); } else { struct stat stat; if (fstat(fd, &stat)) { result = -errno; - __android_log_print(ANDROID_LOG_ERROR, "Error getting size of file '%s': %s", filename.c_str(), strerror(errno)); + LOGE("Error getting size of file '%s': %s", filename.c_str(), strerror(errno)); } else { size_t length = size_t(stat.st_size); @@ -86,8 +83,7 @@ namespace android { ssize_t nrd = read(fd, buffer, length); if (nrd < 0) { result = -errno; - __android_log_print(ANDROID_LOG_ERROR, "Error reading file '%s': %s", - filename.c_str(), strerror(errno)); + LOGE("Error reading file '%s': %s", filename.c_str(), strerror(errno)); delete[] buffer; buffer = nullptr; } else { From fadc600a3724af91fb46699638ae7a9f18d4f3bb Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 22 Jul 2025 22:21:59 -0600 Subject: [PATCH 025/215] #1394 fix remaining compilation errors --- .../src/main/cpp/android/input/KeyLayoutMap.h | 149 +++++++++--------- .../src/main/cpp/android/libbase/result.h | 39 ++++- 2 files changed, 112 insertions(+), 76 deletions(-) diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h index ff811b8570..bc87a0ab1b 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h @@ -16,103 +16,110 @@ #pragma once -#include -#include #include #include #include "../utils/Tokenizer.h" +#include "../libbase/result.h" #include namespace android { -struct AxisInfo { - enum Mode { - // Axis value is reported directly. - MODE_NORMAL = 0, - // Axis value should be inverted before reporting. - MODE_INVERT = 1, - // Axis value should be split into two axes - MODE_SPLIT = 2, - }; + struct AxisInfo { + enum Mode { + // Axis value is reported directly. + MODE_NORMAL = 0, + // Axis value should be inverted before reporting. + MODE_INVERT = 1, + // Axis value should be split into two axes + MODE_SPLIT = 2, + }; - // Axis mode. - Mode mode; + // Axis mode. + Mode mode; - // Axis id. - // When split, this is the axis used for values smaller than the split position. - int32_t axis; + // Axis id. + // When split, this is the axis used for values smaller than the split position. + int32_t axis; - // When split, this is the axis used for values after higher than the split position. - int32_t highAxis; + // When split, this is the axis used for values after higher than the split position. + int32_t highAxis; - // The split value, or 0 if not split. - int32_t splitValue; + // The split value, or 0 if not split. + int32_t splitValue; - // The flat value, or -1 if none. - int32_t flatOverride; + // The flat value, or -1 if none. + int32_t flatOverride; - AxisInfo() : mode(MODE_NORMAL), axis(-1), highAxis(-1), splitValue(0), flatOverride(-1) { - } -}; + AxisInfo() : mode(MODE_NORMAL), axis(-1), highAxis(-1), splitValue(0), flatOverride(-1) { + } + }; /** * Describes a mapping from keyboard scan codes and joystick axes to Android key codes and axes. * * This object is immutable after it has been loaded. */ -class KeyLayoutMap { -public: - static base::Result> load(const std::string& filename, - const char* contents = nullptr); - static base::Result> loadContents(const std::string& filename, - const char* contents); - - status_t mapKey(int32_t scanCode, int32_t usageCode, - int32_t* outKeyCode, uint32_t* outFlags) const; - std::vector findScanCodesForKey(int32_t keyCode) const; - std::optional findScanCodeForLed(int32_t ledCode) const; - std::vector findUsageCodesForKey(int32_t keyCode) const; - std::optional findUsageCodeForLed(int32_t ledCode) const; - - std::optional mapAxis(int32_t scanCode) const; - const std::string getLoadFileName() const; - // Return pair of sensor type and sensor data index, for the input device abs code - base::Result> mapSensor(int32_t absCode) const; - - virtual ~KeyLayoutMap(); - -private: - static base::Result> load(Tokenizer* tokenizer); - - struct Key { - int32_t keyCode; - uint32_t flags; - }; + class KeyLayoutMap { + public: + static base::Result> load(const std::string &filename, + const char *contents = nullptr); - std::unordered_map mKeysByScanCode; - std::unordered_map mKeysByUsageCode; - std::unordered_map mAxes; - std::set mRequiredKernelConfigs; - std::string mLoadFileName; + static base::Result> loadContents(const std::string &filename, + const char *contents); - KeyLayoutMap(); + status_t mapKey(int32_t scanCode, int32_t usageCode, + int32_t *outKeyCode, uint32_t *outFlags) const; - const Key* getKey(int32_t scanCode, int32_t usageCode) const; + std::vector findScanCodesForKey(int32_t keyCode) const; - class Parser { - KeyLayoutMap* mMap; - Tokenizer* mTokenizer; + std::optional findScanCodeForLed(int32_t ledCode) const; - public: - Parser(KeyLayoutMap* map, Tokenizer* tokenizer); - ~Parser(); - status_t parse(); + std::vector findUsageCodesForKey(int32_t keyCode) const; + + std::optional findUsageCodeForLed(int32_t ledCode) const; + + std::optional mapAxis(int32_t scanCode) const; + + const std::string getLoadFileName() const; + + virtual ~KeyLayoutMap(); private: - status_t parseKey(); - status_t parseAxis(); - status_t parseRequiredKernelConfig(); + static base::Result> load(Tokenizer *tokenizer); + + struct Key { + int32_t keyCode; + uint32_t flags; + }; + + std::unordered_map mKeysByScanCode; + std::unordered_map mKeysByUsageCode; + std::unordered_map mAxes; + std::set mRequiredKernelConfigs; + std::string mLoadFileName; + + KeyLayoutMap(); + + const Key *getKey(int32_t scanCode, int32_t usageCode) const; + + class Parser { + KeyLayoutMap *mMap; + Tokenizer *mTokenizer; + + public: + Parser(KeyLayoutMap *map, Tokenizer *tokenizer); + + ~Parser(); + + status_t parse(); + + private: + status_t parseKey(); + + status_t parseAxis(); + + status_t parseRequiredKernelConfig(); + }; }; -}; } // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/result.h b/sysbridge/src/main/cpp/android/libbase/result.h index edbb56e8f2..045a426c80 100644 --- a/sysbridge/src/main/cpp/android/libbase/result.h +++ b/sysbridge/src/main/cpp/android/libbase/result.h @@ -98,7 +98,7 @@ #include #include #include -#include +#include #include "errors.h" #include "expected.h" @@ -304,14 +304,43 @@ __attribute__((noinline)) ResultError MakeResultErrorWithCode(std::string template inline ResultError ErrorfImpl(const std::string &fmt, const Args &... args) { - return ResultError(std::vformat(fmt, std::make_format_args(args...)), - ErrorCode(Errno{}, args...)); + std::ostringstream oss; + formatHelper(oss, fmt, args...); + return ResultError(oss.str(), ErrorCode(Errno{}, args...)); +} + + template + void formatHelper(std::ostringstream &oss, const std::string &fmt, const T &arg) { + size_t pos = fmt.find("{}"); + if (pos != std::string::npos) { + oss << fmt.substr(0, pos) << arg << fmt.substr(pos + 2); + } else { + oss << fmt; + } + } + + template + void formatHelper(std::ostringstream &oss, const std::string &fmt, const T &arg, + const Args &... args) { + size_t pos = fmt.find("{}"); + if (pos != std::string::npos) { + oss << fmt.substr(0, pos) << arg; + formatHelper(oss, fmt.substr(pos + 2), args...); + } else { + oss << fmt; + } + } + + void formatHelper(std::ostringstream &oss, const std::string &fmt) { + oss << fmt; } template inline ResultError ErrnoErrorfImpl(const std::string &fmt, const Args &... args) { - Errno code{errno}; - return MakeResultErrorWithCode(std::vformat(fmt, std::make_format_args(args...)), code); + Errno code{errno}; + std::ostringstream oss; + formatHelper(oss, fmt, args...); + return MakeResultErrorWithCode(oss.str(), code); } #define Errorf(fmt, ...) android::base::ErrorfImpl(fmt, ##__VA_ARGS__) From 2c18cbb8f9d7c62b3dffe58347b995f7523424e6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 23 Jul 2025 18:22:18 -0600 Subject: [PATCH 026/215] #1394 WIP: bunch of fixes and use static libc++ library --- sysbridge/build.gradle.kts | 5 +- sysbridge/src/main/cpp/CMakeLists.txt | 35 ++-- .../main/cpp/android/input/KeyLayoutMap.cpp | 3 +- .../src/main/cpp/android/input/KeyLayoutMap.h | 1 + .../src/main/cpp/android/libbase/errors.h | 2 +- .../src/main/cpp/android/libbase/result.h | 4 +- .../main/cpp/android/utils/SharedBuffer.cpp | 122 ++++++++++++++ .../src/main/cpp/android/utils/SharedBuffer.h | 153 ++++++++++++++++++ .../src/main/cpp/android/utils/String16.cpp | 13 +- .../src/main/cpp/android/utils/String8.h | 4 +- .../src/main/cpp/android/utils/Tokenizer.cpp | 2 +- .../src/main/cpp/android/utils/TypeHelpers.h | 5 +- sysbridge/src/main/cpp/cgroup.cpp | 4 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 1 + 14 files changed, 320 insertions(+), 34 deletions(-) create mode 100644 sysbridge/src/main/cpp/android/utils/SharedBuffer.cpp create mode 100644 sysbridge/src/main/cpp/android/utils/SharedBuffer.h diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index e7db6690a3..ba67624a3d 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -17,9 +17,8 @@ android { 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") + arguments("-DANDROID_STL=c++_static", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON") } } } @@ -81,7 +80,7 @@ dependencies { // From Shizuku :manager module build.gradle file. implementation("io.github.vvb2060.ndk:boringssl:20250114") - implementation("dev.rikka.ndk.thirdparty:cxx:1.2.0") +// 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") diff --git a/sysbridge/src/main/cpp/CMakeLists.txt b/sysbridge/src/main/cpp/CMakeLists.txt index 4bc7668977..1893f62685 100644 --- a/sysbridge/src/main/cpp/CMakeLists.txt +++ b/sysbridge/src/main/cpp/CMakeLists.txt @@ -11,19 +11,18 @@ cmake_minimum_required(VERSION 3.22.1) # build script scope). project("sysbridge") -# FROM SHIZUKU -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) 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...") + message("Building 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...") + message("Building Debug...") add_definitions(-DDEBUG) endif () @@ -36,12 +35,21 @@ 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(libsysbridge.so - starter.cpp misc.cpp selinux.cpp cgroup.cpp android.cpp) - -target_link_libraries(libsysbridge.so ${log-lib} cxx::cxx) + starter.cpp + misc.cpp + selinux.cpp + cgroup.cpp + android.cpp + adb_pairing.cpp + android/input/KeyLayoutMap.cpp + android/input/InputEventLabels.cpp + android/libbase/result.cpp + android/utils/Tokenizer.cpp + android/utils/String16.cpp) + +target_link_libraries(libsysbridge.so ${log-lib} boringssl::crypto_static) if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") add_custom_command(TARGET libsysbridge.so POST_BUILD @@ -51,7 +59,7 @@ endif () add_library(adb SHARED adb_pairing.cpp misc.cpp) -target_link_libraries(adb ${log-lib} boringssl::crypto_static cxx::cxx) +target_link_libraries(adb ${log-lib} boringssl::crypto_static) if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") add_custom_command(TARGET adb POST_BUILD @@ -87,3 +95,12 @@ target_link_libraries(evdev android log) +# Add include directories for header files +target_include_directories(evdev PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/android + ${CMAKE_CURRENT_SOURCE_DIR}/android/input + ${CMAKE_CURRENT_SOURCE_DIR}/android/libbase + ${CMAKE_CURRENT_SOURCE_DIR}/android/utils + ${CMAKE_CURRENT_SOURCE_DIR}/libevdev) + diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp index 0d385b93c7..3190600ec1 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "../logging.h" +#include "../../logging.h" #include #include "../utils/String8.h" #include "KeyLayoutMap.h" @@ -71,6 +71,7 @@ namespace android { } else { status = Tokenizer::fromContents(String8(filename.c_str()), contents, &tokenizer); } + std::format("sfd") if (status) { LOGE("Error %d opening key layout map file %s.", status, filename.c_str()); return Errorf("Error {} opening key layout map file {}.", status, filename.c_str()); diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h index bc87a0ab1b..1fdf91852c 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h @@ -21,6 +21,7 @@ #include "../utils/Tokenizer.h" #include "../libbase/result.h" #include +#include namespace android { diff --git a/sysbridge/src/main/cpp/android/libbase/errors.h b/sysbridge/src/main/cpp/android/libbase/errors.h index cca4e887bb..0ad4ecabd8 100644 --- a/sysbridge/src/main/cpp/android/libbase/errors.h +++ b/sysbridge/src/main/cpp/android/libbase/errors.h @@ -31,7 +31,7 @@ #include -#include +#include namespace android { namespace base { diff --git a/sysbridge/src/main/cpp/android/libbase/result.h b/sysbridge/src/main/cpp/android/libbase/result.h index 045a426c80..81a1bcc968 100644 --- a/sysbridge/src/main/cpp/android/libbase/result.h +++ b/sysbridge/src/main/cpp/android/libbase/result.h @@ -93,12 +93,12 @@ #include #include -#include #include -#include +#include #include #include +#include #include "errors.h" #include "expected.h" diff --git a/sysbridge/src/main/cpp/android/utils/SharedBuffer.cpp b/sysbridge/src/main/cpp/android/utils/SharedBuffer.cpp new file mode 100644 index 0000000000..8ee62e3bb3 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/SharedBuffer.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "sharedbuffer" + +#include "SharedBuffer.h" + +#include +#include + +// --------------------------------------------------------------------------- + +namespace android { + + SharedBuffer *SharedBuffer::alloc(size_t size) { + SharedBuffer *sb = static_cast(malloc(sizeof(SharedBuffer) + size)); + if (sb) { + // Should be std::atomic_init(&sb->mRefs, 1); + // But that generates a warning with some compilers. + // The following is OK on Android-supported platforms. + sb->mRefs.store(1, std::memory_order_relaxed); + sb->mSize = size; + sb->mClientMetadata = 0; + } + return sb; + } + + + void SharedBuffer::dealloc(const SharedBuffer *released) { + free(const_cast(released)); + } + + SharedBuffer *SharedBuffer::edit() const { + if (onlyOwner()) { + return const_cast(this); + } + SharedBuffer *sb = alloc(mSize); + if (sb) { + memcpy(sb->data(), data(), size()); + release(); + } + return sb; + } + + SharedBuffer *SharedBuffer::editResize(size_t newSize) const { + if (onlyOwner()) { + SharedBuffer *buf = const_cast(this); + if (buf->mSize == newSize) return buf; + buf = (SharedBuffer *) realloc(reinterpret_cast(buf), + sizeof(SharedBuffer) + newSize); + if (buf != nullptr) { + buf->mSize = newSize; + return buf; + } + } + SharedBuffer *sb = alloc(newSize); + if (sb) { + const size_t mySize = mSize; + memcpy(sb->data(), data(), newSize < mySize ? newSize : mySize); + release(); + } + return sb; + } + + SharedBuffer *SharedBuffer::attemptEdit() const { + if (onlyOwner()) { + return const_cast(this); + } + return nullptr; + } + + SharedBuffer *SharedBuffer::reset(size_t new_size) const { + // cheap-o-reset. + SharedBuffer *sb = alloc(new_size); + if (sb) { + release(); + } + return sb; + } + + void SharedBuffer::acquire() const { + mRefs.fetch_add(1, std::memory_order_relaxed); + } + + int32_t SharedBuffer::release(uint32_t flags) const { + const bool useDealloc = ((flags & eKeepStorage) == 0); + if (onlyOwner()) { + // Since we're the only owner, our reference count goes to zero. + mRefs.store(0, std::memory_order_relaxed); + if (useDealloc) { + dealloc(this); + } + // As the only owner, our previous reference count was 1. + return 1; + } + // There's multiple owners, we need to use an atomic decrement. + int32_t prevRefCount = mRefs.fetch_sub(1, std::memory_order_release); + if (prevRefCount == 1) { + // We're the last reference, we need the acquire fence. + atomic_thread_fence(std::memory_order_acquire); + if (useDealloc) { + dealloc(this); + } + } + return prevRefCount; + } + + +}; // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/SharedBuffer.h b/sysbridge/src/main/cpp/android/utils/SharedBuffer.h new file mode 100644 index 0000000000..c170df00e1 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/SharedBuffer.h @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * DEPRECATED. DO NOT USE FOR NEW CODE. + */ + +#ifndef ANDROID_SHARED_BUFFER_H +#define ANDROID_SHARED_BUFFER_H + +#include +#include +#include + +// --------------------------------------------------------------------------- + +namespace android { + + class SharedBuffer { + public: + + /* flags to use with release() */ + enum { + eKeepStorage = 0x00000001 + }; + + /*! allocate a buffer of size 'size' and acquire() it. + * call release() to free it. + */ + static SharedBuffer *alloc(size_t size); + + /*! free the memory associated with the SharedBuffer. + * Fails if there are any users associated with this SharedBuffer. + * In other words, the buffer must have been release by all its + * users. + */ + static void dealloc(const SharedBuffer *released); + + //! access the data for read + inline const void *data() const; + + //! access the data for read/write + inline void *data(); + + //! get size of the buffer + inline size_t size() const; + + //! get back a SharedBuffer object from its data + static inline SharedBuffer *bufferFromData(void *data); + + //! get back a SharedBuffer object from its data + static inline const SharedBuffer *bufferFromData(const void *data); + + //! get the size of a SharedBuffer object from its data + static inline size_t sizeFromData(const void *data); + + //! edit the buffer (get a writtable, or non-const, version of it) + SharedBuffer *edit() const; + + //! edit the buffer, resizing if needed + SharedBuffer *editResize(size_t size) const; + + //! like edit() but fails if a copy is required + SharedBuffer *attemptEdit() const; + + //! resize and edit the buffer, loose it's content. + SharedBuffer *reset(size_t size) const; + + //! acquire/release a reference on this buffer + void acquire() const; + + /*! release a reference on this buffer, with the option of not + * freeing the memory associated with it if it was the last reference + * returns the previous reference count + */ + int32_t release(uint32_t flags = 0) const; + + //! returns wether or not we're the only owner + inline bool onlyOwner() const; + + + private: + inline SharedBuffer() {} + + inline ~SharedBuffer() {} + + SharedBuffer(const SharedBuffer &); + + SharedBuffer &operator=(const SharedBuffer &); + + // Must be sized to preserve correct alignment. + mutable std::atomic mRefs; + size_t mSize; + uint32_t mReserved; + public: + // mClientMetadata is reserved for client use. It is initialized to 0 + // and the clients can do whatever they want with it. Note that this is + // placed last so that it is adjcent to the buffer allocated. + uint32_t mClientMetadata; + }; + + static_assert(sizeof(SharedBuffer) % 8 == 0 + && (sizeof(size_t) > 4 || sizeof(SharedBuffer) == 16), + "SharedBuffer has unexpected size"); + +// --------------------------------------------------------------------------- + + const void *SharedBuffer::data() const { + return this + 1; + } + + void *SharedBuffer::data() { + return this + 1; + } + + size_t SharedBuffer::size() const { + return mSize; + } + + SharedBuffer *SharedBuffer::bufferFromData(void *data) { + return data ? static_cast(data) - 1 : nullptr; + } + + const SharedBuffer *SharedBuffer::bufferFromData(const void *data) { + return data ? static_cast(data) - 1 : nullptr; + } + + size_t SharedBuffer::sizeFromData(const void *data) { + return data ? bufferFromData(data)->mSize : 0; + } + + bool SharedBuffer::onlyOwner() const { + return (mRefs.load(std::memory_order_acquire) == 1); + } + +} // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_VECTOR_H diff --git a/sysbridge/src/main/cpp/android/utils/String16.cpp b/sysbridge/src/main/cpp/android/utils/String16.cpp index 96e1477215..aa75f8ad82 100644 --- a/sysbridge/src/main/cpp/android/utils/String16.cpp +++ b/sysbridge/src/main/cpp/android/utils/String16.cpp @@ -14,11 +14,9 @@ * limitations under the License. */ -#include +#include "String16.h" -#include - -#include +#include #include "SharedBuffer.h" @@ -81,12 +79,10 @@ char16_t* String16::allocFromUTF8(const char* u8str, size_t u8len) char16_t* String16::allocFromUTF16(const char16_t* u16str, size_t u16len) { if (u16len >= SIZE_MAX / sizeof(char16_t)) { - android_errorWriteLog(0x534e4554, "73826242"); abort(); } SharedBuffer* buf = static_cast(alloc((u16len + 1) * sizeof(char16_t))); - ALOG_ASSERT(buf, "Unable to allocate shared buffer"); if (buf) { char16_t* str = (char16_t*)buf->data(); memcpy(str, u16str, u16len * sizeof(char16_t)); @@ -179,10 +175,6 @@ status_t String16::setTo(const String16& other, size_t len, size_t begin) return OK; } - if (&other == this) { - LOG_ALWAYS_FATAL("Not implemented"); - } - return setTo(other.c_str() + begin, len); } @@ -194,7 +186,6 @@ status_t String16::setTo(const char16_t* other) status_t String16::setTo(const char16_t* other, size_t len) { if (len >= SIZE_MAX / sizeof(char16_t)) { - android_errorWriteLog(0x534e4554, "73826242"); abort(); } diff --git a/sysbridge/src/main/cpp/android/utils/String8.h b/sysbridge/src/main/cpp/android/utils/String8.h index 482e95eeb5..bcc71e260a 100644 --- a/sysbridge/src/main/cpp/android/utils/String8.h +++ b/sysbridge/src/main/cpp/android/utils/String8.h @@ -21,8 +21,8 @@ #include #include -#include // for strcmp -#include +#include // for strcmp +#include #include "Errors.h" #include "Unicode.h" #include "TypeHelpers.h" diff --git a/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp index 2bf4b33881..7c46ac9b12 100644 --- a/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp +++ b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "logging.h" +#include "../../logging.h" #include "Tokenizer.h" #include "Errors.h" #include "FileMap.h" diff --git a/sysbridge/src/main/cpp/android/utils/TypeHelpers.h b/sysbridge/src/main/cpp/android/utils/TypeHelpers.h index 007036bbdb..d867a9a46c 100644 --- a/sysbridge/src/main/cpp/android/utils/TypeHelpers.h +++ b/sysbridge/src/main/cpp/android/utils/TypeHelpers.h @@ -20,9 +20,10 @@ #include #include -#include -#include +#include +#include #include +#include // --------------------------------------------------------------------------- diff --git a/sysbridge/src/main/cpp/cgroup.cpp b/sysbridge/src/main/cpp/cgroup.cpp index c1015b72f5..2fe9af242b 100644 --- a/sysbridge/src/main/cpp/cgroup.cpp +++ b/sysbridge/src/main/cpp/cgroup.cpp @@ -1,7 +1,7 @@ -#include #include #include #include +#include namespace cgroup { @@ -23,7 +23,7 @@ namespace cgroup { return len; } - int get_cgroup(int pid, int* cuid, int *cpid) { + int get_cgroup(int pid, int *cuid, int *cpid) { char buf[PATH_MAX]; snprintf(buf, PATH_MAX, "/proc/%d/cgroup", pid); diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index b85adb9ffa..8bba5f2ad5 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -7,6 +7,7 @@ #define LOG_TAG "KeyMapperSystemBridge" #include "logging.h" +#include "android/input/KeyLayoutMap.h" extern "C" JNIEXPORT jstring JNICALL From 7207e2569bd94583c4e4c9b53b6eb94a45377346 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 23 Jul 2025 21:14:33 -0600 Subject: [PATCH 027/215] #1394 key layout map files compile and run on device --- sysbridge/src/main/cpp/CMakeLists.txt | 19 +- sysbridge/src/main/cpp/android/input/Input.h | 67 ++ .../main/cpp/android/input/KeyLayoutMap.cpp | 30 +- .../src/main/cpp/android/input/KeyLayoutMap.h | 1 - .../src/main/cpp/android/libbase/result.h | 691 +++++++++--------- .../src/main/cpp/android/liblog/log_main.h | 369 ++++++++++ .../src/main/cpp/android/utils/FileMap.cpp | 270 +++++++ .../src/main/cpp/android/utils/String16.cpp | 1 + .../src/main/cpp/android/utils/String8.cpp | 453 ++++++++++++ .../src/main/cpp/android/utils/String8.h | 7 +- .../src/main/cpp/android/utils/Tokenizer.cpp | 6 +- .../src/main/cpp/android/utils/Unicode.cpp | 538 ++++++++++++++ sysbridge/src/main/cpp/libevdev_jni.cpp | 15 +- 13 files changed, 2103 insertions(+), 364 deletions(-) create mode 100644 sysbridge/src/main/cpp/android/input/Input.h create mode 100644 sysbridge/src/main/cpp/android/liblog/log_main.h create mode 100644 sysbridge/src/main/cpp/android/utils/FileMap.cpp create mode 100644 sysbridge/src/main/cpp/android/utils/String8.cpp create mode 100644 sysbridge/src/main/cpp/android/utils/Unicode.cpp diff --git a/sysbridge/src/main/cpp/CMakeLists.txt b/sysbridge/src/main/cpp/CMakeLists.txt index 1893f62685..58cc1fd1d8 100644 --- a/sysbridge/src/main/cpp/CMakeLists.txt +++ b/sysbridge/src/main/cpp/CMakeLists.txt @@ -42,12 +42,7 @@ add_executable(libsysbridge.so selinux.cpp cgroup.cpp android.cpp - adb_pairing.cpp - android/input/KeyLayoutMap.cpp - android/input/InputEventLabels.cpp - android/libbase/result.cpp - android/utils/Tokenizer.cpp - android/utils/String16.cpp) + adb_pairing.cpp) target_link_libraries(libsysbridge.so ${log-lib} boringssl::crypto_static) @@ -66,7 +61,6 @@ if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") 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. @@ -85,7 +79,16 @@ add_library(evdev SHARED libevdev_jni.cpp libevdev/libevdev.c libevdev/libevdev-names.c - libevdev/libevdev-uinput.c) + libevdev/libevdev-uinput.c + android/input/KeyLayoutMap.cpp + android/input/InputEventLabels.cpp + android/libbase/result.cpp + android/utils/Tokenizer.cpp + android/utils/String16.cpp + android/utils/String8.cpp + android/utils/SharedBuffer.cpp + android/utils/FileMap.cpp + android/utils/Unicode.cpp) # Specifies libraries CMake should link to your target library. You # can link libraries from various origins, such as libraries defined in this diff --git a/sysbridge/src/main/cpp/android/input/Input.h b/sysbridge/src/main/cpp/android/input/Input.h new file mode 100644 index 0000000000..f6f86a83b9 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/Input.h @@ -0,0 +1,67 @@ + +/* + * Flags that flow alongside events in the input dispatch system to help with certain + * policy decisions such as waking from device sleep. + * + * These flags are also defined in frameworks/base/core/java/android/view/WindowManagerPolicy.java. + */ +enum { + /* These flags originate in RawEvents and are generally set in the key map. + * NOTE: If you want a flag to be able to set in a keylayout file, then you must add it to + * InputEventLabels.h as well. */ + + // Indicates that the event should wake the device. + POLICY_FLAG_WAKE = 0x00000001, + + // Indicates that the key is virtual, such as a capacitive button, and should + // generate haptic feedback. Virtual keys may be suppressed for some time + // after a recent touch to prevent accidental activation of virtual keys adjacent + // to the touch screen during an edge swipe. + POLICY_FLAG_VIRTUAL = 0x00000002, + + // Indicates that the key is the special function modifier. + POLICY_FLAG_FUNCTION = 0x00000004, + + // Indicates that the key represents a special gesture that has been detected by + // the touch firmware or driver. Causes touch events from the same device to be canceled. + // This policy flag prevents key events from changing touch mode state. + POLICY_FLAG_GESTURE = 0x00000008, + + // Indicates that key usage mapping represents a fallback mapping. + // Fallback mappings cannot be used to definitively determine whether a device + // supports a key code. For example, a HID device can report a key press + // as a HID usage code if it is not mapped to any linux key code in the kernel. + // However, we cannot know which HID usage codes that device supports from + // userspace through the evdev. We can use fallback mappings to convert HID + // usage codes to Android key codes without needing to know if a device can + // actually report the usage code. + POLICY_FLAG_FALLBACK_USAGE_MAPPING = 0x00000010, + + POLICY_FLAG_RAW_MASK = 0x0000ffff, + + /* These flags are set by the input dispatcher. */ + + // Indicates that the input event was injected. + POLICY_FLAG_INJECTED = 0x01000000, + + // Indicates that the input event is from a trusted source such as a directly attached + // input device or an application with system-wide event injection permission. + POLICY_FLAG_TRUSTED = 0x02000000, + + // Indicates that the input event has passed through an input filter. + POLICY_FLAG_FILTERED = 0x04000000, + + // Disables automatic key repeating behavior. + POLICY_FLAG_DISABLE_KEY_REPEAT = 0x08000000, + + /* These flags are set by the input reader policy as it intercepts each event. */ + + // Indicates that the device was in an interactive state when the + // event was intercepted. + POLICY_FLAG_INTERACTIVE = 0x20000000, + + // Indicates that the event should be dispatched to applications. + // The input event should still be sent to the InputDispatcher so that it can see all + // input events received include those that it will not deliver. + POLICY_FLAG_PASS_TO_USER = 0x40000000, +}; diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp index 3190600ec1..73e2447cbc 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -26,11 +26,17 @@ #include #include #include "../libbase/result.h" +#include "../liblog/log_main.h" +#include "Input.h" + +#define DEBUG_MAPPING false +#define DEBUG_PARSER false // Enables debug output for parser performance. #define DEBUG_PARSER_PERFORMANCE 0 namespace android { + namespace { std::optional parseInt(const char *str) { @@ -38,11 +44,11 @@ namespace android { errno = 0; const int value = strtol(str, &end, 0); if (end == str) { - LOG(ERROR) << "Could not parse " << str; + LOGE("Could not parse %s", str); return {}; } if (errno == ERANGE) { - LOG(ERROR) << "Out of bounds: " << str; + LOGE("Out of bounds: %s", str); return {}; } return value; @@ -71,7 +77,7 @@ namespace android { } else { status = Tokenizer::fromContents(String8(filename.c_str()), contents, &tokenizer); } - std::format("sfd") + if (status) { LOGE("Error %d opening key layout map file %s.", status, filename.c_str()); return Errorf("Error {} opening key layout map file {}.", status, filename.c_str()); @@ -411,4 +417,22 @@ namespace android { return NO_ERROR; } +// Parse the name of a required kernel config. +// The layout won't be used if the specified kernel config is not present +// Examples: +// requires_kernel_config CONFIG_HID_PLAYSTATION + status_t KeyLayoutMap::Parser::parseRequiredKernelConfig() { + String8 codeToken = mTokenizer->nextToken(WHITESPACE); + std::string configName = codeToken.c_str(); + + const auto result = mMap->mRequiredKernelConfigs.emplace(configName); + if (!result.second) { + LOGE("%s: Duplicate entry for required kernel config %s.", + mTokenizer->getLocation().c_str(), configName.c_str()); + return BAD_VALUE; + } + +// ALOGD_IF(DEBUG_PARSER, "Parsed required kernel config: name=%s", configName.c_str()); + return NO_ERROR; + } } // namespace android diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h index 1fdf91852c..bc87a0ab1b 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h @@ -21,7 +21,6 @@ #include "../utils/Tokenizer.h" #include "../libbase/result.h" #include -#include namespace android { diff --git a/sysbridge/src/main/cpp/android/libbase/result.h b/sysbridge/src/main/cpp/android/libbase/result.h index 81a1bcc968..68c93344f8 100644 --- a/sysbridge/src/main/cpp/android/libbase/result.h +++ b/sysbridge/src/main/cpp/android/libbase/result.h @@ -104,397 +104,408 @@ #include "expected.h" namespace android { -namespace base { + namespace base { // Errno is a wrapper class for errno(3). Use this type instead of `int` when instantiating // `Result` and `Error` template classes. This is required to distinguish errno from other // integer-based error code types like `status_t`. -struct Errno { - Errno() : val_(0) {} - Errno(int e) : val_(e) {} - int value() const { return val_; } - operator int() const { return value(); } - const char* print() const { return strerror(value()); } - - int val_; - - // TODO(b/209929099): remove this conversion operator. This currently is needed to not break - // existing places where error().code() is used to construct enum values. - template >> - operator E() const { - return E(val_); - } -}; - -static_assert(std::is_trivially_copyable_v == true); - -template -struct ResultError { - template >> - ResultError(T&& message, P&& code) - : message_(std::forward(message)), code_(E(std::forward

(code))) {} - - ResultError(const ResultError& other) = default; - ResultError(ResultError&& other) = default; - ResultError& operator=(const ResultError& other) = default; - ResultError& operator=(ResultError&& other) = default; - - template - // NOLINTNEXTLINE(google-explicit-constructor) - operator android::base::expected>() && { - return android::base::unexpected(std::move(*this)); - } - - template - // NOLINTNEXTLINE(google-explicit-constructor) - operator android::base::expected>() const& { - return android::base::unexpected(*this); - } - - const std::string& message() const { return message_; } - const E& code() const { return code_; } - - private: - std::string message_; - E code_; -}; - -template -auto format_as(ResultError error) { - return error.message(); -} - -template -struct ResultError { - template >> - ResultError(P&& code) : code_(E(std::forward

(code))) {} - - template - operator android::base::expected>() const { - return android::base::unexpected(ResultError(code_)); - } - - const E& code() const { return code_; } - - private: - E code_; -}; - -template -inline bool operator==(const ResultError& lhs, const ResultError& rhs) { - return lhs.message() == rhs.message() && lhs.code() == rhs.code(); -} - -template -inline bool operator!=(const ResultError& lhs, const ResultError& rhs) { - return !(lhs == rhs); -} - -template -inline std::ostream& operator<<(std::ostream& os, const ResultError& t) { - os << t.message(); - return os; -} - -namespace internal { + struct Errno { + Errno() : val_(0) {} + + Errno(int e) : val_(e) {} + + int value() const { return val_; } + + operator int() const { return value(); } + + const char *print() const { return strerror(value()); } + + int val_; + + // TODO(b/209929099): remove this conversion operator. This currently is needed to not break + // existing places where error().code() is used to construct enum values. + template>> + operator E() const { + return E(val_); + } + }; + + static_assert(std::is_trivially_copyable_v == true); + + template + struct ResultError { + template>> + ResultError(T &&message, P &&code) + : message_(std::forward(message)), code_(E(std::forward

(code))) {} + + ResultError(const ResultError &other) = default; + + ResultError(ResultError &&other) = default; + + ResultError &operator=(const ResultError &other) = default; + + ResultError &operator=(ResultError &&other) = default; + + template + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() &&{ + return android::base::unexpected(std::move(*this)); + } + + template + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const &{ + return android::base::unexpected(*this); + } + + const std::string &message() const { return message_; } + + const E &code() const { return code_; } + + private: + std::string message_; + E code_; + }; + + template + auto format_as(ResultError error) { + return error.message(); + } + + template + struct ResultError { + template>> + ResultError(P &&code) : code_(E(std::forward

(code))) {} + + template + operator android::base::expected>() const { + return android::base::unexpected(ResultError(code_)); + } + + const E &code() const { return code_; } + + private: + E code_; + }; + + template + inline bool operator==(const ResultError &lhs, const ResultError &rhs) { + return lhs.message() == rhs.message() && lhs.code() == rhs.code(); + } + + template + inline bool operator!=(const ResultError &lhs, const ResultError &rhs) { + return !(lhs == rhs); + } + + template + inline std::ostream &operator<<(std::ostream &os, const ResultError &t) { + os << t.message(); + return os; + } + + namespace internal { // Stream class that does nothing and is has zero (actually 1) size. It is used instead of // std::stringstream when include_message is false so that we use less on stack. // sizeof(std::stringstream) is 280 on arm64. -struct DoNothingStream { - template - DoNothingStream& operator<<(T&&) { - return *this; - } - - std::string str() const { return ""; } -}; -} // namespace internal - -template >> -class Error { - public: - Error() : code_(0), has_code_(false) {} - template >> - // NOLINTNEXTLINE(google-explicit-constructor) - Error(P&& code) : code_(std::forward

(code)), has_code_(true) {} - - template >> - // NOLINTNEXTLINE(google-explicit-constructor) - operator android::base::expected>() const { - return android::base::unexpected(ResultError

(str(), static_cast

(code_))); - } - - template >> - // NOLINTNEXTLINE(google-explicit-constructor) - operator android::base::expected>() const { - return android::base::unexpected(ResultError(static_cast

(code_))); - } - - template - Error& operator<<(T&& t) { - static_assert(include_message, "<< not supported when include_message = false"); - // NOLINTNEXTLINE(bugprone-suspicious-semicolon) - if constexpr (std::is_same_v>, ResultError>) { - if (!has_code_) { - code_ = t.code(); - } - return (*this) << t.message(); - } - int saved = errno; - ss_ << t; - errno = saved; - return *this; - } - - const std::string str() const { - static_assert(include_message, "str() not supported when include_message = false"); - std::string str = ss_.str(); - if (has_code_) { - if (str.empty()) { - return code_.print(); - } - return std::move(str) + ": " + code_.print(); - } - return str; - } - - Error(const Error&) = delete; - Error(Error&&) = delete; - Error& operator=(const Error&) = delete; - Error& operator=(Error&&) = delete; - - template - friend Error ErrorfImpl(const std::string &fmt, const Args &... args); - - template - friend Error ErrnoErrorfImpl(const std::string &fmt, const Args &... args); - - private: - Error(bool has_code, E code, const std::string& message) : code_(code), has_code_(has_code) { - (*this) << message; - } - - std::conditional_t ss_; - E code_; - const bool has_code_; -}; - -inline Error ErrnoError() { - return Error(Errno{errno}); -} - -template -inline E ErrorCode(E code) { - return code; -} + struct DoNothingStream { + template + DoNothingStream &operator<<(T &&) { + return *this; + } + + std::string str() const { return ""; } + }; + } // namespace internal + + template>> + class Error { + public: + Error() : code_(0), has_code_(false) {} + + template>> + // NOLINTNEXTLINE(google-explicit-constructor) + Error(P &&code) : code_(std::forward

(code)), has_code_(true) {} + + template>> + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const { + return android::base::unexpected(ResultError

(str(), static_cast

(code_))); + } + + template>> + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const { + return android::base::unexpected(ResultError(static_cast

(code_))); + } + + template + Error &operator<<(T &&t) { + static_assert(include_message, "<< not supported when include_message = false"); + // NOLINTNEXTLINE(bugprone-suspicious-semicolon) + if constexpr (std::is_same_v>, ResultError>) { + if (!has_code_) { + code_ = t.code(); + } + return (*this) << t.message(); + } + int saved = errno; + ss_ << t; + errno = saved; + return *this; + } + + const std::string str() const { + static_assert(include_message, "str() not supported when include_message = false"); + std::string str = ss_.str(); + if (has_code_) { + if (str.empty()) { + return code_.print(); + } + return std::move(str) + ": " + code_.print(); + } + return str; + } + + Error(const Error &) = delete; + + Error(Error &&) = delete; + + Error &operator=(const Error &) = delete; + + Error &operator=(Error &&) = delete; + + template + friend Error ErrorfImpl(const std::string &fmt, const Args &... args); + + template + friend Error ErrnoErrorfImpl(const std::string &fmt, const Args &... args); + + private: + Error(bool has_code, E code, const std::string &message) : code_(code), + has_code_(has_code) { + (*this) << message; + } + + std::conditional_t ss_; + E code_; + const bool has_code_; + }; + + inline Error ErrnoError() { + return Error(Errno{errno}); + } + + template + inline E ErrorCode(E code) { + return code; + } // Return the error code of the last ResultError object, if any. // Otherwise, return `code` as it is. -template -inline E ErrorCode(E code, T&& t, const Args&... args) { - if constexpr (std::is_same_v>, ResultError>) { - return ErrorCode(t.code(), args...); - } - return ErrorCode(code, args...); -} - -__attribute__((noinline)) ResultError MakeResultErrorWithCode(std::string&& message, - Errno code); - -template -inline ResultError ErrorfImpl(const std::string &fmt, const Args &... args) { - std::ostringstream oss; - formatHelper(oss, fmt, args...); - return ResultError(oss.str(), ErrorCode(Errno{}, args...)); -} - - template - void formatHelper(std::ostringstream &oss, const std::string &fmt, const T &arg) { - size_t pos = fmt.find("{}"); - if (pos != std::string::npos) { - oss << fmt.substr(0, pos) << arg << fmt.substr(pos + 2); - } else { - oss << fmt; + template + inline E ErrorCode(E code, T &&t, const Args &... args) { + if constexpr (std::is_same_v>, ResultError>) { + return ErrorCode(t.code(), args...); + } + return ErrorCode(code, args...); } - } - - template - void formatHelper(std::ostringstream &oss, const std::string &fmt, const T &arg, - const Args &... args) { - size_t pos = fmt.find("{}"); - if (pos != std::string::npos) { - oss << fmt.substr(0, pos) << arg; - formatHelper(oss, fmt.substr(pos + 2), args...); - } else { - oss << fmt; - } - } - void formatHelper(std::ostringstream &oss, const std::string &fmt) { - oss << fmt; -} + __attribute__((noinline)) ResultError MakeResultErrorWithCode(std::string &&message, + Errno code); + + template + inline ResultError ErrorfImpl(const std::string &fmt, const Args &... args) { + return ResultError(fmt, ErrorCode(Errno{}, args...)); + } -template -inline ResultError ErrnoErrorfImpl(const std::string &fmt, const Args &... args) { - Errno code{errno}; - std::ostringstream oss; - formatHelper(oss, fmt, args...); - return MakeResultErrorWithCode(oss.str(), code); -} + template + inline ResultError ErrnoErrorfImpl(const std::string &fmt, const Args &... args) { + Errno code{errno}; + return MakeResultErrorWithCode(std::string(fmt), code); + } #define Errorf(fmt, ...) android::base::ErrorfImpl(fmt, ##__VA_ARGS__) #define ErrnoErrorf(fmt, ...) android::base::ErrnoErrorfImpl(fmt, ##__VA_ARGS__) -template -using Result = android::base::expected>; + template + using Result = android::base::expected>; // Specialization of android::base::OkOrFail for V = Result. See android-base/errors.h // for the contract. -namespace impl { -template -using Code = std::decay_t().error().code())>; + namespace impl { + template + using Code = std::decay_t().error().code())>; -template -using ErrorType = std::decay_t().error())>; + template + using ErrorType = std::decay_t().error())>; -template -constexpr bool IsNumeric = std::is_integral_v || std::is_floating_point_v || - (std::is_enum_v && std::is_convertible_v); + template + constexpr bool IsNumeric = std::is_integral_v || std::is_floating_point_v || + (std::is_enum_v && std::is_convertible_v); // This base class exists to take advantage of shadowing // We include the conversion in this base class so that if the conversion in NumericConversions // overlaps, we (arbitrarily) choose the implementation in NumericConversions due to shadowing. -template -struct ConversionBase { - ErrorType error_; - // T is a expected>. - operator T() const& { return unexpected(error_); } - operator T() && { return unexpected(std::move(error_)); } + template + struct ConversionBase { + ErrorType error_; - operator Code() const { return error_.code(); } -}; + // T is a expected>. + operator T() const & { return unexpected(error_); } + + operator T() && { return unexpected(std::move(error_)); } + + operator Code() const { return error_.code(); } + }; // User defined conversions can be followed by numeric conversions // Although we template specialize for the exact code type, we need // specializations for conversions to all numeric types to avoid an // ambiguous conversion sequence. -template -struct NumericConversions : public ConversionBase {}; -template -struct NumericConversions>> - > : public ConversionBase -{ + template + struct NumericConversions : public ConversionBase { + }; + + template + struct NumericConversions>> + > : public ConversionBase { #pragma push_macro("SPECIALIZED_CONVERSION") #define SPECIALIZED_CONVERSION(type) \ operator expected>() const& { return unexpected(this->error_); } \ operator expected>()&& { return unexpected(std::move(this->error_)); } - SPECIALIZED_CONVERSION(int) - SPECIALIZED_CONVERSION(short int) - SPECIALIZED_CONVERSION(unsigned short int) - SPECIALIZED_CONVERSION(unsigned int) - SPECIALIZED_CONVERSION(long int) - SPECIALIZED_CONVERSION(unsigned long int) - SPECIALIZED_CONVERSION(long long int) - SPECIALIZED_CONVERSION(unsigned long long int) - SPECIALIZED_CONVERSION(bool) - SPECIALIZED_CONVERSION(char) - SPECIALIZED_CONVERSION(unsigned char) - SPECIALIZED_CONVERSION(signed char) - SPECIALIZED_CONVERSION(wchar_t) - SPECIALIZED_CONVERSION(char16_t) - SPECIALIZED_CONVERSION(char32_t) - SPECIALIZED_CONVERSION(float) - SPECIALIZED_CONVERSION(double) - SPECIALIZED_CONVERSION(long double) + SPECIALIZED_CONVERSION(int) + + SPECIALIZED_CONVERSION(short int) + + SPECIALIZED_CONVERSION(unsigned short int) + + SPECIALIZED_CONVERSION(unsigned int) + + SPECIALIZED_CONVERSION(long int) + + SPECIALIZED_CONVERSION(unsigned long int) + + SPECIALIZED_CONVERSION(long long int) + + SPECIALIZED_CONVERSION(unsigned long long int) + + SPECIALIZED_CONVERSION(bool) + + SPECIALIZED_CONVERSION(char) + + SPECIALIZED_CONVERSION(unsigned char) + + SPECIALIZED_CONVERSION(signed char) + + SPECIALIZED_CONVERSION(wchar_t) + + SPECIALIZED_CONVERSION(char16_t) + + SPECIALIZED_CONVERSION(char32_t) + + SPECIALIZED_CONVERSION(float) + + SPECIALIZED_CONVERSION(double) + + SPECIALIZED_CONVERSION(long double) #undef SPECIALIZED_CONVERSION #pragma pop_macro("SPECIALIZED_CONVERSION") - // For debugging purposes - using IsNumericT = std::true_type; -}; + // For debugging purposes + using IsNumericT = std::true_type; + }; #ifdef __cpp_concepts -template + template // Define a concept which **any** type matches to -concept Universal = std::is_same_v; + concept Universal = std::is_same_v; #endif // A type that is never used. -struct Never {}; -} // namespace impl - -template -struct OkOrFail> - : public impl::NumericConversions> { - using V = Result; - using Err = impl::ErrorType; - using C = impl::Code; -private: - OkOrFail(Err&& v): impl::NumericConversions{std::move(v)} {} - OkOrFail(const OkOrFail& other) = delete; - OkOrFail(const OkOrFail&& other) = delete; -public: - // Checks if V is ok or fail - static bool IsOk(const V& val) { return val.ok(); } - - // Turns V into a success value - static T Unwrap(V&& val) { - if constexpr (std::is_same_v) { - assert(IsOk(val)); - return; - } else { - return std::move(val.value()); - } - } - - // Consumes V when it's a fail value - static OkOrFail Fail(V&& v) { - assert(!IsOk(v)); - return OkOrFail{std::move(v.error())}; - } - - // We specialize as much as possible to avoid ambiguous conversion with templated expected ctor. - // We don't need this specialization if `C` is numeric because that case is already covered by - // `NumericConversions`. - operator Result, impl::Never, C>, E, include_message>() - const& { - return unexpected(this->error_); - } - operator Result, impl::Never, C>, E, include_message>() && { - return unexpected(std::move(this->error_)); - } + struct Never { + }; + } // namespace impl + + template + struct OkOrFail> + : public impl::NumericConversions> { + using V = Result; + using Err = impl::ErrorType; + using C = impl::Code; + private: + OkOrFail(Err &&v) : impl::NumericConversions{std::move(v)} {} + + OkOrFail(const OkOrFail &other) = delete; + + OkOrFail(const OkOrFail &&other) = delete; + + public: + // Checks if V is ok or fail + static bool IsOk(const V &val) { return val.ok(); } + + // Turns V into a success value + static T Unwrap(V &&val) { + if constexpr (std::is_same_v) { + assert(IsOk(val)); + return; + } else { + return std::move(val.value()); + } + } + + // Consumes V when it's a fail value + static OkOrFail Fail(V &&v) { + assert(!IsOk(v)); + return OkOrFail{std::move(v.error())}; + } + + // We specialize as much as possible to avoid ambiguous conversion with templated expected ctor. + // We don't need this specialization if `C` is numeric because that case is already covered by + // `NumericConversions`. + operator Result, impl::Never, C>, E, include_message>() + const & { + return unexpected(this->error_); + } + + operator Result, impl::Never, C>, E, include_message>() && { + return unexpected(std::move(this->error_)); + } #ifdef __cpp_concepts - // The idea here is to match this template method to any type (not simply trivial types). - // The reason for including a constraint is to take advantage of the fact that a constrained - // method always has strictly lower precedence than a non-constrained method in template - // specialization rules (thus avoiding ambiguity). So we use a universally matching constraint to - // mark this function as less preferable (but still accepting of all types). - template - operator Result() const& { - return unexpected(this->error_); - } - template - operator Result() && { - return unexpected(std::move(this->error_)); - } + + // The idea here is to match this template method to any type (not simply trivial types). + // The reason for including a constraint is to take advantage of the fact that a constrained + // method always has strictly lower precedence than a non-constrained method in template + // specialization rules (thus avoiding ambiguity). So we use a universally matching constraint to + // mark this function as less preferable (but still accepting of all types). + template + operator Result() const &{ + return unexpected(this->error_); + } + + template + operator Result() &&{ + return unexpected(std::move(this->error_)); + } + #else - template - operator Result() const& { - return unexpected(this->error_); - } - template - operator Result() && { - return unexpected(std::move(this->error_)); - } + template + operator Result() const& { + return unexpected(this->error_); + } + template + operator Result() && { + return unexpected(std::move(this->error_)); + } #endif - static const std::string& ErrorMessage(const V& val) { return val.error().message(); } -}; + static const std::string &ErrorMessage(const V &val) { return val.error().message(); } + }; // Macros for testing the results of functions that return android::base::Result. These also work // with base::android::expected. They assume the user depends on libgmock and includes @@ -512,5 +523,5 @@ struct OkOrFail> << " Actual: " << tmp.error().message() << "\n" \ << "Expected: is ok\n" -} // namespace base + } // namespace base } // namespace android diff --git a/sysbridge/src/main/cpp/android/liblog/log_main.h b/sysbridge/src/main/cpp/android/liblog/log_main.h new file mode 100644 index 0000000000..8fc5ff4e79 --- /dev/null +++ b/sysbridge/src/main/cpp/android/liblog/log_main.h @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2005-2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + + +#include +#include + +#include + +__BEGIN_DECLS + +/* + * Normally we strip the effects of ALOGV (VERBOSE messages), + * LOG_FATAL and LOG_FATAL_IF (FATAL assert messages) from the + * release builds by defining NDEBUG. You can modify this (for + * example with "#define LOG_NDEBUG 0" at the top of your source + * file) to change that behavior. + */ + +#ifndef LOG_NDEBUG +#ifdef NDEBUG +#define LOG_NDEBUG 1 +#else +#define LOG_NDEBUG 0 +#endif +#endif + +/* --------------------------------------------------------------------- */ + +/* + * This file uses ", ## __VA_ARGS__" zero-argument token pasting to + * work around issues with debug-only syntax errors in assertions + * that are missing format strings. See commit + * 19299904343daf191267564fe32e6cd5c165cd42 + */ +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" +#endif + +/* + * Use __VA_ARGS__ if running a static analyzer, + * to avoid warnings of unused variables in __VA_ARGS__. + * Use constexpr function in C++ mode, so these macros can be used + * in other constexpr functions without warning. + */ +#ifdef __clang_analyzer__ +#ifdef __cplusplus +extern "C++" { +template +constexpr int __fake_use_va_args(Ts...) { + return 0; +} +} +#else +extern int __fake_use_va_args(int, ...); +#endif /* __cplusplus */ +#define __FAKE_USE_VA_ARGS(...) ((void)__fake_use_va_args(0, ##__VA_ARGS__)) +#else +#define __FAKE_USE_VA_ARGS(...) ((void)(0)) +#endif /* __clang_analyzer__ */ + +#ifndef __predict_false +#define __predict_false(exp) __builtin_expect((exp) != 0, 0) +#endif + +#define android_writeLog(prio, tag, text) __android_log_write(prio, tag, text) + +#define android_printLog(prio, tag, ...) \ + __android_log_print(prio, tag, __VA_ARGS__) + +#define android_vprintLog(prio, cond, tag, ...) \ + __android_log_vprint(prio, tag, __VA_ARGS__) + +/* + * Log macro that allows you to specify a number for the priority. + */ +#ifndef LOG_PRI +#define LOG_PRI(priority, tag, ...) android_printLog(priority, tag, __VA_ARGS__) +#endif + +/* + * Log macro that allows you to pass in a varargs ("args" is a va_list). + */ +#ifndef LOG_PRI_VA +#define LOG_PRI_VA(priority, tag, fmt, args) \ + android_vprintLog(priority, NULL, tag, fmt, args) +#endif + +/* --------------------------------------------------------------------- */ + +/* XXX Macros to work around syntax errors in places where format string + * arg is not passed to ALOG_ASSERT, LOG_ALWAYS_FATAL or LOG_ALWAYS_FATAL_IF + * (happens only in debug builds). + */ + +/* Returns 2nd arg. Used to substitute default value if caller's vararg list + * is empty. + */ +#define __android_second(dummy, second, ...) second + +/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise + * returns nothing. + */ +#define __android_rest(first, ...) , ##__VA_ARGS__ + +#define android_printAssert(cond, tag, ...) \ + __android_log_assert(cond, tag, \ + __android_second(0, ##__VA_ARGS__, NULL) \ + __android_rest(__VA_ARGS__)) + +/* + * Log a fatal error. If the given condition fails, this stops program + * execution like a normal assertion, but also generating the given message. + * It is NOT stripped from release builds. Note that the condition test + * is -inverted- from the normal assert() semantics. + */ +#ifndef LOG_ALWAYS_FATAL_IF +#define LOG_ALWAYS_FATAL_IF(cond, ...) \ + ((__predict_false(cond)) ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), \ + ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__))) \ + : ((void)0)) +#endif + +#ifndef LOG_ALWAYS_FATAL +#define LOG_ALWAYS_FATAL(...) \ + (((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__))) +#endif + +/* + * Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that + * are stripped out of release builds. + */ + +#if LOG_NDEBUG + +#ifndef LOG_FATAL_IF +#define LOG_FATAL_IF(cond, ...) __FAKE_USE_VA_ARGS(__VA_ARGS__) +#endif +#ifndef LOG_FATAL +#define LOG_FATAL(...) __FAKE_USE_VA_ARGS(__VA_ARGS__) +#endif + +#else + +#ifndef LOG_FATAL_IF +#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__) +#endif +#ifndef LOG_FATAL +#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__) +#endif + +#endif + +/* + * Assertion that generates a log message when the assertion fails. + * Stripped out of release builds. Uses the current LOG_TAG. + */ +#ifndef ALOG_ASSERT +#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * C/C++ logging functions. See the logging documentation for API details. + * + * We'd like these to be available from C code (in case we import some from + * somewhere), so this has a C interface. + * + * The output will be correct when the log file is shared between multiple + * threads and/or multiple processes so long as the operating system + * supports O_APPEND. These calls have mutex-protected data structures + * and so are NOT reentrant. Do not use LOG in a signal handler. + */ + +/* --------------------------------------------------------------------- */ + +/* + * Simplified macro to send a verbose log message using the current LOG_TAG. + */ +#ifndef ALOGV +#define __ALOGV(...) ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) +#if LOG_NDEBUG +#define ALOGV(...) \ + do { \ + __FAKE_USE_VA_ARGS(__VA_ARGS__); \ + if (false) { \ + __ALOGV(__VA_ARGS__); \ + } \ + } while (false) +#else +#define ALOGV(...) __ALOGV(__VA_ARGS__) +#endif +#endif + +#ifndef ALOGV_IF +#if LOG_NDEBUG +#define ALOGV_IF(cond, ...) __FAKE_USE_VA_ARGS(__VA_ARGS__) +#else +#define ALOGV_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif +#endif + +/* + * Simplified macro to send a debug log message using the current LOG_TAG. + */ +#ifndef ALOGD +#define ALOGD(...) ((void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGD_IF +#define ALOGD_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif + +/* + * Simplified macro to send an info log message using the current LOG_TAG. + */ +#ifndef ALOGI +#define ALOGI(...) ((void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGI_IF +#define ALOGI_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif + +/* + * Simplified macro to send a warning log message using the current LOG_TAG. + */ +#ifndef ALOGW +#define ALOGW(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGW_IF +#define ALOGW_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif + +/* + * Simplified macro to send an error log message using the current LOG_TAG. + */ +#ifndef ALOGE +#define ALOGE(...) ((void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGE_IF +#define ALOGE_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * verbose priority. + */ +#ifndef IF_ALOGV +#if LOG_NDEBUG +#define IF_ALOGV() if (false) +#else +#define IF_ALOGV() IF_ALOG(LOG_VERBOSE, LOG_TAG) +#endif +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * debug priority. + */ +#ifndef IF_ALOGD +#define IF_ALOGD() IF_ALOG(LOG_DEBUG, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * info priority. + */ +#ifndef IF_ALOGI +#define IF_ALOGI() IF_ALOG(LOG_INFO, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * warn priority. + */ +#ifndef IF_ALOGW +#define IF_ALOGW() IF_ALOG(LOG_WARN, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * error priority. + */ +#ifndef IF_ALOGE +#define IF_ALOGE() IF_ALOG(LOG_ERROR, LOG_TAG) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * Basic log message macro. + * + * Example: + * ALOG(LOG_WARN, NULL, "Failed with error %d", errno); + * + * The second argument may be NULL or "" to indicate the "global" tag. + */ +#ifndef ALOG +#define ALOG(priority, tag, ...) LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__) +#endif + +/* + * Conditional given a desired logging priority and tag. + */ +#ifndef IF_ALOG +#define IF_ALOG(priority, tag) if (android_testLog(ANDROID_##priority, tag)) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * IF_ALOG uses android_testLog, but IF_ALOG can be overridden. + * android_testLog will remain constant in its purpose as a wrapper + * for Android logging filter policy, and can be subject to + * change. It can be reused by the developers that override + * IF_ALOG as a convenient means to reimplement their policy + * over Android. + */ + +#if LOG_NDEBUG /* Production */ +#define android_testLog(prio, tag) \ + (__android_log_is_loggable_len(prio, tag, (tag) ? strlen(tag) : 0, ANDROID_LOG_DEBUG) != 0) +#else +#define android_testLog(prio, tag) \ + (__android_log_is_loggable_len(prio, tag, (tag) ? strlen(tag) : 0, ANDROID_LOG_VERBOSE) != 0) +#endif + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +__END_DECLS diff --git a/sysbridge/src/main/cpp/android/utils/FileMap.cpp b/sysbridge/src/main/cpp/android/utils/FileMap.cpp new file mode 100644 index 0000000000..21b7c65574 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/FileMap.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Shared file mapping class. +// + +#define LOG_TAG "filemap" + +#include "FileMap.h" +#include "../../logging.h" +#include + +#if defined(__MINGW32__) && !defined(__USE_MINGW_ANSI_STDIO) +# define PRId32 "I32d" +# define PRIx32 "I32x" +# define PRId64 "I64d" +#else + +#include + +#endif + +#include +#include + +#if !defined(__MINGW32__) + +#include + +#endif + +#include +#include +#include +#include +#include + +using namespace android; + +/*static*/ long FileMap::mPageSize = -1; + +// Constructor. Create an empty object. +FileMap::FileMap(void) + : mFileName(nullptr), + mBasePtr(nullptr), + mBaseLength(0), + mDataPtr(nullptr), + mDataLength(0) +#if defined(__MINGW32__) +, + mFileHandle(INVALID_HANDLE_VALUE), + mFileMapping(NULL) +#endif +{ +} + +// Move Constructor. +FileMap::FileMap(FileMap &&other) noexcept + : mFileName(other.mFileName), + mBasePtr(other.mBasePtr), + mBaseLength(other.mBaseLength), + mDataOffset(other.mDataOffset), + mDataPtr(other.mDataPtr), + mDataLength(other.mDataLength) +#if defined(__MINGW32__) +, + mFileHandle(other.mFileHandle), + mFileMapping(other.mFileMapping) +#endif +{ + other.mFileName = nullptr; + other.mBasePtr = nullptr; + other.mDataPtr = nullptr; +#if defined(__MINGW32__) + other.mFileHandle = INVALID_HANDLE_VALUE; + other.mFileMapping = NULL; +#endif +} + +// Move assign operator. +FileMap &FileMap::operator=(FileMap &&other) noexcept { + mFileName = other.mFileName; + mBasePtr = other.mBasePtr; + mBaseLength = other.mBaseLength; + mDataOffset = other.mDataOffset; + mDataPtr = other.mDataPtr; + mDataLength = other.mDataLength; + other.mFileName = nullptr; + other.mBasePtr = nullptr; + other.mDataPtr = nullptr; +#if defined(__MINGW32__) + mFileHandle = other.mFileHandle; + mFileMapping = other.mFileMapping; + other.mFileHandle = INVALID_HANDLE_VALUE; + other.mFileMapping = NULL; +#endif + return *this; +} + +// Destructor. +FileMap::~FileMap(void) { + if (mFileName != nullptr) { + free(mFileName); + } +#if defined(__MINGW32__) + if (mBasePtr && UnmapViewOfFile(mBasePtr) == 0) { + LOGD("UnmapViewOfFile(%p) failed, error = %lu\n", mBasePtr, + GetLastError() ); + } + if (mFileMapping != NULL) { + CloseHandle(mFileMapping); + } +#else + if (mBasePtr && munmap(mBasePtr, mBaseLength) != 0) { + LOGD("munmap(%p, %zu) failed\n", mBasePtr, mBaseLength); + } +#endif +} + + +// Create a new mapping on an open file. +// +// Closing the file descriptor does not unmap the pages, so we don't +// claim ownership of the fd. +// +// Returns "false" on failure. +bool FileMap::create(const char *origFileName, int fd, off64_t offset, size_t length, + bool readOnly) { +#if defined(__MINGW32__) + int adjust; + off64_t adjOffset; + size_t adjLength; + + if (mPageSize == -1) { + SYSTEM_INFO si; + + GetSystemInfo( &si ); + mPageSize = si.dwAllocationGranularity; + } + + DWORD protect = readOnly ? PAGE_READONLY : PAGE_READWRITE; + + mFileHandle = (HANDLE) _get_osfhandle(fd); + mFileMapping = CreateFileMapping( mFileHandle, NULL, protect, 0, 0, NULL); + if (mFileMapping == NULL) { + LOGE("CreateFileMapping(%p, %lx) failed with error %lu\n", + mFileHandle, protect, GetLastError() ); + return false; + } + + adjust = offset % mPageSize; + adjOffset = offset - adjust; + adjLength = length + adjust; + + mBasePtr = MapViewOfFile( mFileMapping, + readOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, + 0, + (DWORD)(adjOffset), + adjLength ); + if (mBasePtr == NULL) { + LOGE("MapViewOfFile(%" PRId64 ", %zu) failed with error %lu\n", + adjOffset, adjLength, GetLastError() ); + CloseHandle(mFileMapping); + mFileMapping = NULL; + return false; + } +#else // !defined(__MINGW32__) + assert(fd >= 0); + assert(offset >= 0); + assert(length > 0); + + // init on first use + if (mPageSize == -1) { + mPageSize = sysconf(_SC_PAGESIZE); + if (mPageSize == -1) { + LOGE("could not get _SC_PAGESIZE\n"); + return false; + } + } + + int adjust = offset % mPageSize; + off64_t adjOffset = offset - adjust; + size_t adjLength; + if (__builtin_add_overflow(length, adjust, &adjLength)) { + LOGE("adjusted length overflow: length %zu adjust %d", length, adjust); + return false; + } + + int flags = MAP_SHARED; + int prot = PROT_READ; + if (!readOnly) prot |= PROT_WRITE; + + void *ptr = mmap64(nullptr, adjLength, prot, flags, fd, adjOffset); + if (ptr == MAP_FAILED) { + if (errno == EINVAL && length == 0) { + ptr = nullptr; + adjust = 0; + } else { + LOGE("mmap(%lld,%zu) failed: %s\n", (long long) adjOffset, adjLength, strerror(errno)); + return false; + } + } + mBasePtr = ptr; +#endif // !defined(__MINGW32__) + + mFileName = origFileName != nullptr ? strdup(origFileName) : nullptr; + mBaseLength = adjLength; + mDataOffset = offset; + mDataPtr = (char *) mBasePtr + adjust; + mDataLength = length; + + LOGV("MAP: base %p/%zu data %p/%zu\n", + mBasePtr, mBaseLength, mDataPtr, mDataLength); + + return true; +} + +// Provide guidance to the system. +#if !defined(_WIN32) + +int FileMap::advise(MapAdvice advice) { + int cc, sysAdvice; + + switch (advice) { + case NORMAL: + sysAdvice = MADV_NORMAL; + break; + case RANDOM: + sysAdvice = MADV_RANDOM; + break; + case SEQUENTIAL: + sysAdvice = MADV_SEQUENTIAL; + break; + case WILLNEED: + sysAdvice = MADV_WILLNEED; + break; + case DONTNEED: + sysAdvice = MADV_DONTNEED; + break; + default: + assert(false); + return -1; + } + + cc = madvise(mBasePtr, mBaseLength, sysAdvice); + if (cc != 0) + LOGW("madvise(%d) failed: %s\n", sysAdvice, strerror(errno)); + return cc; +} + +#else +int FileMap::advise(MapAdvice /* advice */) +{ + return -1; +} +#endif diff --git a/sysbridge/src/main/cpp/android/utils/String16.cpp b/sysbridge/src/main/cpp/android/utils/String16.cpp index aa75f8ad82..0d61425d7c 100644 --- a/sysbridge/src/main/cpp/android/utils/String16.cpp +++ b/sysbridge/src/main/cpp/android/utils/String16.cpp @@ -17,6 +17,7 @@ #include "String16.h" #include +#include #include "SharedBuffer.h" diff --git a/sysbridge/src/main/cpp/android/utils/String8.cpp b/sysbridge/src/main/cpp/android/utils/String8.cpp new file mode 100644 index 0000000000..3f8d9d634a --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/String8.cpp @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "String8.h" + +#include +#include "String16.h" + +#include +#include + +#include +#include +#include + +#include "SharedBuffer.h" + +/* + * Functions outside android is below the namespace android, since they use + * functions and constants in android namespace. + */ + +// --------------------------------------------------------------------------- + +namespace android { + + static inline char *getEmptyString() { + static SharedBuffer *gEmptyStringBuf = [] { + SharedBuffer *buf = SharedBuffer::alloc(1); + char *str = static_cast(buf->data()); + *str = 0; + return buf; + }(); + + gEmptyStringBuf->acquire(); + return static_cast(gEmptyStringBuf->data()); + } + +// --------------------------------------------------------------------------- + + static char *allocFromUTF8(const char *in, size_t len) { + if (len > 0) { + if (len == SIZE_MAX) { + return nullptr; + } + SharedBuffer *buf = SharedBuffer::alloc(len + 1); +// ALOG_ASSERT(buf, "Unable to allocate shared buffer"); + if (buf) { + char *str = (char *) buf->data(); + memcpy(str, in, len); + str[len] = 0; + return str; + } + return nullptr; + } + + return getEmptyString(); + } + + static char *allocFromUTF16(const char16_t *in, size_t len) { + if (len == 0) return getEmptyString(); + + // Allow for closing '\0' + const ssize_t resultStrLen = utf16_to_utf8_length(in, len) + 1; + if (resultStrLen < 1) { + return getEmptyString(); + } + + SharedBuffer *buf = SharedBuffer::alloc(resultStrLen); +// ALOG_ASSERT(buf, "Unable to allocate shared buffer"); + if (!buf) { + return getEmptyString(); + } + + char *resultStr = (char *) buf->data(); + utf16_to_utf8(in, len, resultStr, resultStrLen); + return resultStr; + } + + static char *allocFromUTF32(const char32_t *in, size_t len) { + if (len == 0) { + return getEmptyString(); + } + + const ssize_t resultStrLen = utf32_to_utf8_length(in, len) + 1; + if (resultStrLen < 1) { + return getEmptyString(); + } + + SharedBuffer *buf = SharedBuffer::alloc(resultStrLen); +// ALOG_ASSERT(buf, "Unable to allocate shared buffer"); + if (!buf) { + return getEmptyString(); + } + + char *resultStr = (char *) buf->data(); + utf32_to_utf8(in, len, resultStr, resultStrLen); + + return resultStr; + } + +// --------------------------------------------------------------------------- + + String8::String8() + : mString(getEmptyString()) { + } + + String8::String8(const String8 &o) + : mString(o.mString) { + SharedBuffer::bufferFromData(mString)->acquire(); + } + + String8::String8(const char *o) + : mString(allocFromUTF8(o, strlen(o))) { + if (mString == nullptr) { + mString = getEmptyString(); + } + } + + String8::String8(const char *o, size_t len) + : mString(allocFromUTF8(o, len)) { + if (mString == nullptr) { + mString = getEmptyString(); + } + } + + String8::String8(const String16 &o) : mString(allocFromUTF16(o.c_str(), o.size())) {} + + String8::String8(const char16_t *o) + : mString(allocFromUTF16(o, strlen16(o))) { + } + + String8::String8(const char16_t *o, size_t len) + : mString(allocFromUTF16(o, len)) { + } + + String8::String8(const char32_t *o) + : mString(allocFromUTF32(o, std::char_traits::length(o))) {} + + String8::String8(const char32_t *o, size_t len) + : mString(allocFromUTF32(o, len)) { + } + + String8::~String8() { + SharedBuffer::bufferFromData(mString)->release(); + } + + size_t String8::length() const { + return SharedBuffer::sizeFromData(mString) - 1; + } + + String8 String8::format(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + + String8 result(formatV(fmt, args)); + + va_end(args); + return result; + } + + String8 String8::formatV(const char *fmt, va_list args) { + String8 result; + result.appendFormatV(fmt, args); + return result; + } + + void String8::clear() { + SharedBuffer::bufferFromData(mString)->release(); + mString = getEmptyString(); + } + + void String8::setTo(const String8 &other) { + SharedBuffer::bufferFromData(other.mString)->acquire(); + SharedBuffer::bufferFromData(mString)->release(); + mString = other.mString; + } + + status_t String8::setTo(const char *other) { + const char *newString = allocFromUTF8(other, strlen(other)); + SharedBuffer::bufferFromData(mString)->release(); + mString = newString; + if (mString) return OK; + + mString = getEmptyString(); + return NO_MEMORY; + } + + status_t String8::setTo(const char *other, size_t len) { + const char *newString = allocFromUTF8(other, len); + SharedBuffer::bufferFromData(mString)->release(); + mString = newString; + if (mString) return OK; + + mString = getEmptyString(); + return NO_MEMORY; + } + + status_t String8::setTo(const char16_t *other, size_t len) { + const char *newString = allocFromUTF16(other, len); + SharedBuffer::bufferFromData(mString)->release(); + mString = newString; + if (mString) return OK; + + mString = getEmptyString(); + return NO_MEMORY; + } + + status_t String8::setTo(const char32_t *other, size_t len) { + const char *newString = allocFromUTF32(other, len); + SharedBuffer::bufferFromData(mString)->release(); + mString = newString; + if (mString) return OK; + + mString = getEmptyString(); + return NO_MEMORY; + } + + status_t String8::append(const String8 &other) { + const size_t otherLen = other.bytes(); + if (bytes() == 0) { + setTo(other); + return OK; + } else if (otherLen == 0) { + return OK; + } + + return real_append(other.c_str(), otherLen); + } + + status_t String8::append(const char *other) { + return append(other, strlen(other)); + } + + status_t String8::append(const char *other, size_t otherLen) { + if (bytes() == 0) { + return setTo(other, otherLen); + } else if (otherLen == 0) { + return OK; + } + + return real_append(other, otherLen); + } + + status_t String8::appendFormat(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + + status_t result = appendFormatV(fmt, args); + + va_end(args); + return result; + } + + status_t String8::appendFormatV(const char *fmt, va_list args) { + int n, result = OK; + va_list tmp_args; + + /* args is undefined after vsnprintf. + * So we need a copy here to avoid the + * second vsnprintf access undefined args. + */ + va_copy(tmp_args, args); + n = vsnprintf(nullptr, 0, fmt, tmp_args); + va_end(tmp_args); + + if (n < 0) return UNKNOWN_ERROR; + + if (n > 0) { + size_t oldLength = length(); + if (static_cast(n) > std::numeric_limits::max() - 1 || + oldLength > std::numeric_limits::max() - n - 1) { + return NO_MEMORY; + } + char *buf = lockBuffer(oldLength + n); + if (buf) { + vsnprintf(buf + oldLength, n + 1, fmt, args); + } else { + result = NO_MEMORY; + } + } + return result; + } + + status_t String8::real_append(const char *other, size_t otherLen) { + const size_t myLen = bytes(); + + SharedBuffer *buf; + size_t newLen; + if (__builtin_add_overflow(myLen, otherLen, &newLen) || + __builtin_add_overflow(newLen, 1, &newLen) || + (buf = SharedBuffer::bufferFromData(mString)->editResize(newLen)) == nullptr) { + return NO_MEMORY; + } + + char *str = (char *) buf->data(); + mString = str; + str += myLen; + memcpy(str, other, otherLen); + str[otherLen] = '\0'; + return OK; + } + + char *String8::lockBuffer(size_t size) { + SharedBuffer *buf = SharedBuffer::bufferFromData(mString) + ->editResize(size + 1); + if (buf) { + char *str = (char *) buf->data(); + mString = str; + return str; + } + return nullptr; + } + + void String8::unlockBuffer() { + unlockBuffer(strlen(mString)); + } + + status_t String8::unlockBuffer(size_t size) { + if (size != this->size()) { + SharedBuffer *buf = SharedBuffer::bufferFromData(mString) + ->editResize(size + 1); + if (!buf) { + return NO_MEMORY; + } + + char *str = (char *) buf->data(); + str[size] = 0; + mString = str; + } + + return OK; + } + + ssize_t String8::find(const char *other, size_t start) const { + size_t len = size(); + if (start >= len) { + return -1; + } + const char *s = mString + start; + const char *p = strstr(s, other); + return p ? p - mString : -1; + } + + bool String8::removeAll(const char *other) { +// ALOG_ASSERT(other, "String8::removeAll() requires a non-NULL string"); + + if (*other == '\0') + return true; + + ssize_t index = find(other); + if (index < 0) return false; + + char *buf = lockBuffer(size()); + if (!buf) return false; // out of memory + + size_t skip = strlen(other); + size_t len = size(); + size_t tail = index; + while (size_t(index) < len) { + ssize_t next = find(other, index + skip); + if (next < 0) { + next = len; + } + + memmove(buf + tail, buf + index + skip, next - index - skip); + tail += next - index - skip; + index = next; + } + unlockBuffer(tail); + return true; + } + + void String8::toLower() { + const size_t length = size(); + if (length == 0) return; + + char *buf = lockBuffer(length); + for (size_t i = length; i > 0; --i) { + *buf = static_cast(tolower(*buf)); + buf++; + } + unlockBuffer(length); + } + +// --------------------------------------------------------------------------- +// Path functions + +// TODO: we should remove all the path functions from String8 +#if defined(_WIN32) +#define OS_PATH_SEPARATOR '\\' +#else +#define OS_PATH_SEPARATOR '/' +#endif + + String8 String8::getPathDir(void) const { + const char *cp; + const char *const str = mString; + + cp = strrchr(str, OS_PATH_SEPARATOR); + if (cp == nullptr) + return String8(""); + else + return String8(str, cp - str); + } + +/* + * Helper function for finding the start of an extension in a pathname. + * + * Returns a pointer inside mString, or NULL if no extension was found. + */ + static const char *find_extension(const char *str) { + const char *lastSlash; + const char *lastDot; + + // only look at the filename + lastSlash = strrchr(str, OS_PATH_SEPARATOR); + if (lastSlash == nullptr) + lastSlash = str; + else + lastSlash++; + + // find the last dot + lastDot = strrchr(lastSlash, '.'); + if (lastDot == nullptr) + return nullptr; + + // looks good, ship it + return lastDot; + } + + String8 String8::getPathExtension(void) const { + auto ext = find_extension(mString); + if (ext != nullptr) + return String8(ext); + else + return String8(""); + } + +}; // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/String8.h b/sysbridge/src/main/cpp/android/utils/String8.h index bcc71e260a..8875750834 100644 --- a/sysbridge/src/main/cpp/android/utils/String8.h +++ b/sysbridge/src/main/cpp/android/utils/String8.h @@ -18,19 +18,14 @@ #define ANDROID_STRING8_H #include -#include #include - #include // for strcmp #include +#include #include "Errors.h" #include "Unicode.h" #include "TypeHelpers.h" -#if __cplusplus >= 202002L -#include -#endif - // --------------------------------------------------------------------------- namespace android { diff --git a/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp index 7c46ac9b12..e9ad7bebf3 100644 --- a/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp +++ b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp @@ -128,7 +128,7 @@ namespace android { String8 Tokenizer::nextToken(const char *delimiters) { #if DEBUG_TOKENIZER - ALOGD("nextToken"); + LOGD("nextToken"); #endif const char *end = getEnd(); const char *tokenStart = mCurrent; @@ -144,7 +144,7 @@ namespace android { void Tokenizer::nextLine() { #if DEBUG_TOKENIZER - ALOGD("nextLine"); + LOGD("nextLine"); #endif const char *end = getEnd(); while (mCurrent != end) { @@ -158,7 +158,7 @@ namespace android { void Tokenizer::skipDelimiters(const char *delimiters) { #if DEBUG_TOKENIZER - ALOGD("skipDelimiters"); + LOGD("skipDelimiters"); #endif const char *end = getEnd(); while (mCurrent != end) { diff --git a/sysbridge/src/main/cpp/android/utils/Unicode.cpp b/sysbridge/src/main/cpp/android/utils/Unicode.cpp new file mode 100644 index 0000000000..49179cc181 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Unicode.cpp @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "unicode" + +#include +#include "Unicode.h" +#include "../../logging.h" +#include "../liblog/log_main.h" + +#include + +extern "C" { + +static const char32_t kByteMask = 0x000000BF; +static const char32_t kByteMark = 0x00000080; + +// Surrogates aren't valid for UTF-32 characters, so define some +// constants that will let us screen them out. +static const char32_t kUnicodeSurrogateHighStart = 0x0000D800; +// Unused, here for completeness: +// static const char32_t kUnicodeSurrogateHighEnd = 0x0000DBFF; +// static const char32_t kUnicodeSurrogateLowStart = 0x0000DC00; +static const char32_t kUnicodeSurrogateLowEnd = 0x0000DFFF; +static const char32_t kUnicodeSurrogateStart = kUnicodeSurrogateHighStart; +static const char32_t kUnicodeSurrogateEnd = kUnicodeSurrogateLowEnd; +static const char32_t kUnicodeMaxCodepoint = 0x0010FFFF; + +// Mask used to set appropriate bits in first byte of UTF-8 sequence, +// indexed by number of bytes in the sequence. +// 0xxxxxxx +// -> (00-7f) 7bit. Bit mask for the first byte is 0x00000000 +// 110yyyyx 10xxxxxx +// -> (c0-df)(80-bf) 11bit. Bit mask is 0x000000C0 +// 1110yyyy 10yxxxxx 10xxxxxx +// -> (e0-ef)(80-bf)(80-bf) 16bit. Bit mask is 0x000000E0 +// 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx +// -> (f0-f7)(80-bf)(80-bf)(80-bf) 21bit. Bit mask is 0x000000F0 +static const char32_t kFirstByteMark[] = { + 0x00000000, 0x00000000, 0x000000C0, 0x000000E0, 0x000000F0 +}; + +// -------------------------------------------------------------------------- +// UTF-32 +// -------------------------------------------------------------------------- + +/** + * Return number of UTF-8 bytes required for the character. If the character + * is invalid, return size of 0. + */ +static inline size_t utf32_codepoint_utf8_length(char32_t srcChar) { + // Figure out how many bytes the result will require. + if (srcChar < 0x00000080) { + return 1; + } else if (srcChar < 0x00000800) { + return 2; + } else if (srcChar < 0x00010000) { + if ((srcChar < kUnicodeSurrogateStart) || (srcChar > kUnicodeSurrogateEnd)) { + return 3; + } else { + // Surrogates are invalid UTF-32 characters. + return 0; + } + } + // Max code point for Unicode is 0x0010FFFF. + else if (srcChar <= kUnicodeMaxCodepoint) { + return 4; + } else { + // Invalid UTF-32 character. + return 0; + } +} + +// Write out the source character to . + +static inline void utf32_codepoint_to_utf8(uint8_t *dstP, char32_t srcChar, size_t bytes) { + dstP += bytes; + switch (bytes) { /* note: everything falls through. */ + case 4: + *--dstP = (uint8_t) ((srcChar | kByteMark) & kByteMask); + srcChar >>= 6; + [[fallthrough]]; + case 3: + *--dstP = (uint8_t) ((srcChar | kByteMark) & kByteMask); + srcChar >>= 6; + [[fallthrough]]; + case 2: + *--dstP = (uint8_t) ((srcChar | kByteMark) & kByteMask); + srcChar >>= 6; + [[fallthrough]]; + case 1: + *--dstP = (uint8_t) (srcChar | kFirstByteMark[bytes]); + } +} + +static inline int32_t utf32_at_internal(const char *cur, size_t *num_read) { + const char first_char = *cur; + if ((first_char & 0x80) == 0) { // ASCII + *num_read = 1; + return *cur; + } + cur++; + char32_t mask, to_ignore_mask; + size_t num_to_read = 0; + char32_t utf32 = first_char; + for (num_to_read = 1, mask = 0x40, to_ignore_mask = 0xFFFFFF80; + (first_char & mask); + num_to_read++, to_ignore_mask |= mask, mask >>= 1) { + // 0x3F == 00111111 + utf32 = (utf32 << 6) + (*cur++ & 0x3F); + } + to_ignore_mask |= mask; + utf32 &= ~(to_ignore_mask << (6 * (num_to_read - 1))); + + *num_read = num_to_read; + return static_cast(utf32); +} + +int32_t utf32_from_utf8_at(const char *src, size_t src_len, size_t index, size_t *next_index) { + if (index >= src_len) { + return -1; + } + size_t unused_index; + if (next_index == nullptr) { + next_index = &unused_index; + } + size_t num_read; + int32_t ret = utf32_at_internal(src + index, &num_read); + if (ret >= 0) { + *next_index = index + num_read; + } + + return ret; +} + +ssize_t utf32_to_utf8_length(const char32_t *src, size_t src_len) { + if (src == nullptr || src_len == 0) { + return -1; + } + + size_t ret = 0; + const char32_t *end = src + src_len; + while (src < end) { + size_t char_len = utf32_codepoint_utf8_length(*src++); + if (SSIZE_MAX - char_len < ret) { + // If this happens, we would overflow the ssize_t type when + // returning from this function, so we cannot express how + // long this string is in an ssize_t. +// android_errorWriteLog(0x534e4554, "37723026"); + return -1; + } + ret += char_len; + } + return ret; +} + +void utf32_to_utf8(const char32_t *src, size_t src_len, char *dst, size_t dst_len) { + if (src == nullptr || src_len == 0 || dst == nullptr) { + return; + } + + const char32_t *cur_utf32 = src; + const char32_t *end_utf32 = src + src_len; + char *cur = dst; + while (cur_utf32 < end_utf32) { + size_t len = utf32_codepoint_utf8_length(*cur_utf32); + LOG_ALWAYS_FATAL_IF(dst_len < len, "%zu < %zu", dst_len, len); + utf32_codepoint_to_utf8((uint8_t *) cur, *cur_utf32++, len); + cur += len; + dst_len -= len; + } + LOG_ALWAYS_FATAL_IF(dst_len < 1, "dst_len < 1: %zu < 1", dst_len); + *cur = '\0'; +} + +// -------------------------------------------------------------------------- +// UTF-16 +// -------------------------------------------------------------------------- + +int strcmp16(const char16_t *s1, const char16_t *s2) { + char16_t ch; + int d = 0; + + while (1) { + d = (int) (ch = *s1++) - (int) *s2++; + if (d || !ch) + break; + } + + return d; +} + +int strncmp16(const char16_t *s1, const char16_t *s2, size_t n) { + char16_t ch; + int d = 0; + + if (n == 0) { + return 0; + } + + do { + d = (int) (ch = *s1++) - (int) *s2++; + if (d || !ch) { + break; + } + } while (--n); + + return d; +} + +size_t strlen16(const char16_t *s) { + const char16_t *ss = s; + while (*ss) + ss++; + return ss - s; +} + +size_t strnlen16(const char16_t *s, size_t maxlen) { + const char16_t *ss = s; + + /* Important: the maxlen test must precede the reference through ss; + since the byte beyond the maximum may segfault */ + while ((maxlen > 0) && *ss) { + ss++; + maxlen--; + } + return ss - s; +} + +char16_t *strstr16(const char16_t *src, const char16_t *target) { + const char16_t needle = *target; + if (needle == '\0') return (char16_t *) src; + + const size_t target_len = strlen16(++target); + do { + do { + if (*src == '\0') { + return nullptr; + } + } while (*src++ != needle); + } while (strncmp16(src, target, target_len) != 0); + src--; + + return (char16_t *) src; +} + +int strzcmp16(const char16_t *s1, size_t n1, const char16_t *s2, size_t n2) { + const char16_t *e1 = s1 + n1; + const char16_t *e2 = s2 + n2; + + while (s1 < e1 && s2 < e2) { + const int d = (int) *s1++ - (int) *s2++; + if (d) { + return d; + } + } + + return n1 < n2 + ? (0 - (int) *s2) + : (n1 > n2 + ? ((int) *s1 - 0) + : 0); +} + +// is_any_surrogate() returns true if w is either a high or low surrogate +static constexpr bool is_any_surrogate(char16_t w) { + return (w & 0xf800) == 0xd800; +} + +// is_surrogate_pair() returns true if w1 and w2 form a valid surrogate pair +static constexpr bool is_surrogate_pair(char16_t w1, char16_t w2) { + return ((w1 & 0xfc00) == 0xd800) && ((w2 & 0xfc00) == 0xdc00); +} + +// TODO: currently utf16_to_utf8_length() returns -1 if src_len == 0, +// which is inconsistent with utf8_to_utf16_length(), here we keep the +// current behavior as intended not to break compatibility +ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len) { + if (src == nullptr || src_len == 0) + return -1; + + const char16_t *const end = src + src_len; + const char16_t *in = src; + size_t utf8_len = 0; + + while (in < end) { + char16_t w = *in++; + if (w < 0x0080) [[likely]] { + utf8_len += 1; + continue; + } + if (w < 0x0800) [[likely]] { + utf8_len += 2; + continue; + } + if (!is_any_surrogate(w)) [[likely]] { + utf8_len += 3; + continue; + } + if (in < end && is_surrogate_pair(w, *in)) { + utf8_len += 4; + in++; + continue; + } + /* skip if at the end of the string or invalid surrogate pair */ + } + return (in == end && utf8_len < SSIZE_MAX) ? utf8_len : -1; +} + +void utf16_to_utf8(const char16_t *src, size_t src_len, char *dst, size_t dst_len) { + if (src == nullptr || src_len == 0 || dst == nullptr) { + return; + } + + const char16_t *in = src; + const char16_t *const in_end = src + src_len; + char *out = dst; + const char *const out_end = dst + dst_len; + char16_t w2; + + auto err_out = [&out, &out_end, &dst_len]() { + LOG_ALWAYS_FATAL_IF(out >= out_end, + "target utf8 string size %zu too short", dst_len); + }; + + while (in < in_end) { + char16_t w = *in++; + if (w < 0x0080) [[likely]] { + if (out + 1 > out_end) + return err_out(); + *out++ = (char) (w & 0xff); + continue; + } + if (w < 0x0800) [[likely]] { + if (out + 2 > out_end) + return err_out(); + *out++ = (char) (0xc0 | ((w >> 6) & 0x1f)); + *out++ = (char) (0x80 | ((w >> 0) & 0x3f)); + continue; + } + if (!is_any_surrogate(w)) [[likely]] { + if (out + 3 > out_end) + return err_out(); + *out++ = (char) (0xe0 | ((w >> 12) & 0xf)); + *out++ = (char) (0x80 | ((w >> 6) & 0x3f)); + *out++ = (char) (0x80 | ((w >> 0) & 0x3f)); + continue; + } + /* surrogate pair */ + if (in < in_end && (w2 = *in, is_surrogate_pair(w, w2))) { + if (out + 4 > out_end) + return err_out(); + char32_t dw = (char32_t) (0x10000 + ((w - 0xd800) << 10) + (w2 - 0xdc00)); + *out++ = (char) (0xf0 | ((dw >> 18) & 0x07)); + *out++ = (char) (0x80 | ((dw >> 12) & 0x3f)); + *out++ = (char) (0x80 | ((dw >> 6) & 0x3f)); + *out++ = (char) (0x80 | ((dw >> 0) & 0x3f)); + in++; + } + /* We reach here in two cases: + * 1) (in == in_end), which means end of the input string + * 2) (w2 & 0xfc00) != 0xdc00, which means invalid surrogate pair + * In either case, we intentionally do nothing and skip + */ + } + *out = '\0'; + return; +} + +// -------------------------------------------------------------------------- +// UTF-8 +// -------------------------------------------------------------------------- + +static char32_t utf8_4b_to_utf32(uint8_t c1, uint8_t c2, uint8_t c3, uint8_t c4) { + return ((c1 & 0x07) << 18) | ((c2 & 0x3f) << 12) | ((c3 & 0x3f) << 6) | (c4 & 0x3f); +} + +// TODO: current behavior of converting UTF8 to UTF-16 has a few issues below +// +// 1. invalid trailing bytes (i.e. not b'10xxxxxx) are treated as valid trailing +// bytes and follows normal conversion rules +// 2. invalid leading byte (b'10xxxxxx) is treated as a valid single UTF-8 byte +// 3. invalid leading byte (b'11111xxx) is treated as a valid leading byte +// (same as b'11110xxx) for a 4-byte UTF-8 sequence +// 4. an invalid 4-byte UTF-8 sequence that translates to a codepoint < U+10000 +// will be converted as a valid UTF-16 character +// +// We keep the current behavior as is but with warnings logged, so as not to +// break compatibility. However, this needs to be addressed later. + +ssize_t utf8_to_utf16_length(const uint8_t *u8str, size_t u8len, bool overreadIsFatal) { + if (u8str == nullptr) + return -1; + + const uint8_t *const in_end = u8str + u8len; + const uint8_t *in = u8str; + size_t utf16_len = 0; + + while (in < in_end) { + uint8_t c = *in; + utf16_len++; + if ((c & 0x80) == 0) [[likely]] { + in++; + continue; + } + if (c < 0xc0) [[unlikely]] { + LOGW("Invalid UTF-8 leading byte: 0x%02x", c); + in++; + continue; + } + if (c < 0xe0) [[likely]] { + in += 2; + continue; + } + if (c < 0xf0) [[likely]] { + in += 3; + continue; + } else { + uint8_t c2, c3, c4; + if (c >= 0xf8) [[unlikely]] { + LOGW("Invalid UTF-8 leading byte: 0x%02x", c); + } + c2 = in[1]; + c3 = in[2]; + c4 = in[3]; + if (utf8_4b_to_utf32(c, c2, c3, c4) >= 0x10000) { + utf16_len++; + } + in += 4; + continue; + } + } + if (in == in_end) { + return utf16_len < SSIZE_MAX ? utf16_len : -1; + } + if (overreadIsFatal) + LOG_ALWAYS_FATAL("Attempt to overread computing length of utf8 string"); + return -1; +} + +char16_t *utf8_to_utf16(const uint8_t *u8str, size_t u8len, char16_t *u16str, size_t u16len) { + // A value > SSIZE_MAX is probably a negative value returned as an error and casted. + LOG_ALWAYS_FATAL_IF(u16len == 0 || u16len > SSIZE_MAX, "u16len is %zu", u16len); + char16_t *end = utf8_to_utf16_no_null_terminator(u8str, u8len, u16str, u16len - 1); + *end = 0; + return end; +} + +char16_t *utf8_to_utf16_no_null_terminator( + const uint8_t *src, size_t srcLen, char16_t *dst, size_t dstLen) { + if (src == nullptr || srcLen == 0 || dstLen == 0) { + return dst; + } + // A value > SSIZE_MAX is probably a negative value returned as an error and casted. + LOG_ALWAYS_FATAL_IF(dstLen > SSIZE_MAX, "dstLen is %zu", dstLen); + + const uint8_t *const in_end = src + srcLen; + const uint8_t *in = src; + const char16_t *const out_end = dst + dstLen; + char16_t *out = dst; + uint8_t c, c2, c3, c4; + char32_t w; + + auto err_in = [&c, &out]() { + LOGW("Unended UTF-8 byte: 0x%02x", c); + return out; + }; + + while (in < in_end && out < out_end) { + c = *in++; + if ((c & 0x80) == 0) [[likely]] { + *out++ = (char16_t) (c); + continue; + } + if (c < 0xc0) [[unlikely]] { + ALOGW("Invalid UTF-8 leading byte: 0x%02x", c); + *out++ = (char16_t) (c); + continue; + } + if (c < 0xe0) [[likely]] { + if (in + 1 > in_end) [[unlikely]] { + return err_in(); + } + c2 = *in++; + *out++ = (char16_t) (((c & 0x1f) << 6) | (c2 & 0x3f)); + continue; + } + if (c < 0xf0) [[likely]] { + if (in + 2 > in_end) [[unlikely]] { + return err_in(); + } + c2 = *in++; + c3 = *in++; + *out++ = (char16_t) (((c & 0x0f) << 12) | + ((c2 & 0x3f) << 6) | (c3 & 0x3f)); + continue; + } else { + if (in + 3 > in_end) [[unlikely]] { + return err_in(); + } + if (c >= 0xf8) [[unlikely]] { + LOGW("Invalid UTF-8 leading byte: 0x%02x", c); + } + // Multiple UTF16 characters with surrogates + c2 = *in++; + c3 = *in++; + c4 = *in++; + w = utf8_4b_to_utf32(c, c2, c3, c4); + if (w < 0x10000) [[unlikely]] { + *out++ = (char16_t) (w); + } else { + if (out + 2 > out_end) [[unlikely]] { + // Ooops.... not enough room for this surrogate pair. + return out; + } + *out++ = (char16_t) (((w - 0x10000) >> 10) + 0xd800); + *out++ = (char16_t) (((w - 0x10000) & 0x3ff) + 0xdc00); + } + continue; + } + } + return out; +} + +} diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 8bba5f2ad5..8921855184 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -8,11 +8,19 @@ #include "logging.h" #include "android/input/KeyLayoutMap.h" +#include "android/libbase/result.h" extern "C" JNIEXPORT jstring JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, - jobject /* this */) { +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, jobject) { + auto result = android::KeyLayoutMap::load("/system/usr/keylayout/Generic.kl", nullptr); + + if (result.ok()) { + LOGE("RESULT OKAY"); + } else { + LOGE("RESULT FAILED"); + } + char *input_file_path = "/dev/input/event12"; struct libevdev *dev = NULL; int fd; @@ -51,7 +59,8 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI 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, Event code: %d\n", + __android_log_print(ANDROID_LOG_ERROR, "Key Mapper", + "Event: %s %s %d, Event code: %d\n", libevdev_event_type_get_name(ev.type), libevdev_event_code_get_name(ev.type, ev.code), ev.value, From c4cddedff341cfe7ad95ece334f8d39761c7e516 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 23 Jul 2025 22:19:48 -0600 Subject: [PATCH 028/215] #1394 parsing key layout files works --- .../BaseAccessibilityServiceController.kt | 7 --- .../cpp/android/input/InputEventLabels.cpp | 14 ++++-- .../main/cpp/android/input/KeyLayoutMap.cpp | 43 +++++++++++-------- .../src/main/cpp/android/input/KeyLayoutMap.h | 2 - .../service/SystemBridgeSetupController.kt | 7 +++ 5 files changed, 43 insertions(+), 30 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 0e8e8e9803..d8ef0a2a22 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -396,13 +396,6 @@ abstract class BaseAccessibilityServiceController( event: MyKeyEvent, detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ): Boolean { - //TODO remove - if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && event.action == KeyEvent.ACTION_DOWN) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - systemBridgeSetupController.startWithAdb() - } - } - val detailedLogInfo = event.toString() if (recordingTrigger) { diff --git a/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp b/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp index 18a3284cef..d8d3c5b0ca 100644 --- a/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp +++ b/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp @@ -19,10 +19,10 @@ #include #include #include +#include "Input.h" #define DEFINE_KEYCODE(key) { #key, AKEYCODE_##key } #define DEFINE_AXIS(axis) { #axis, AMOTION_EVENT_AXIS_##axis } -#define DEFINE_LED(led) { #led, ALED_##led } #define DEFINE_FLAG(flag) { #flag, POLICY_FLAG_##flag } namespace android { @@ -348,7 +348,7 @@ namespace android { DEFINE_KEYCODE(MACRO_1), \ DEFINE_KEYCODE(MACRO_2), \ DEFINE_KEYCODE(MACRO_3), \ - DEFINE_KEYCODE(MACRO_4), \ + DEFINE_KEYCODE(MACRO_4) // DEFINE_KEYCODE(EMOJI_PICKER), \ // DEFINE_KEYCODE(SCREENSHOT), \ // DEFINE_KEYCODE(DICTATE), \ @@ -429,6 +429,13 @@ namespace android { DEFINE_AXIS(GESTURE_PINCH_SCALE_FACTOR), \ DEFINE_AXIS(GESTURE_SWIPE_FINGER_COUNT) +#define FLAGS_SEQUENCE \ + DEFINE_FLAG(VIRTUAL), \ + DEFINE_FLAG(FUNCTION), \ + DEFINE_FLAG(GESTURE), \ + DEFINE_FLAG(WAKE), \ + DEFINE_FLAG(FALLBACK_USAGE_MAPPING) + // clang-format on // --- InputEventLookup --- @@ -437,7 +444,8 @@ namespace android { : KEYCODES({KEYCODES_SEQUENCE}), KEY_NAMES({KEYCODES_SEQUENCE}), AXES({AXES_SEQUENCE}), - AXES_NAMES({AXES_SEQUENCE}) {} + AXES_NAMES({AXES_SEQUENCE}), + FLAGS({FLAGS_SEQUENCE}) {} std::optional InputEventLookup::lookupValueByLabel( const std::unordered_map &map, const char *literal) { diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp index 73e2447cbc..38877796c4 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -29,8 +29,8 @@ #include "../liblog/log_main.h" #include "Input.h" -#define DEBUG_MAPPING false -#define DEBUG_PARSER false +#define DEBUG_MAPPING true +#define DEBUG_PARSER true // Enables debug output for parser performance. #define DEBUG_PARSER_PERFORMANCE 0 @@ -108,10 +108,11 @@ namespace android { status = parser.parse(); #if DEBUG_PARSER_PERFORMANCE nsecs_t elapsedTime = systemTime(SYSTEM_TIME_MONOTONIC) - startTime; - ALOGD("Parsed key layout map file '%s' %d lines in %0.3fms.", + LOGD("Parsed key layout map file '%s' %d lines in %0.3fms.", tokenizer->getFilename().c_str(), tokenizer->getLineNumber(), elapsedTime / 1000000.0); #endif + LOGE("PARSE STATUS = %d", status); if (!status) { return std::move(map); } @@ -218,14 +219,14 @@ namespace android { mTokenizer->skipDelimiters(WHITESPACE); status_t status = parseAxis(); if (status) return status; -// } else if (keywordToken == "led") { -// mTokenizer->skipDelimiters(WHITESPACE); -// status_t status = parseLed(); -// if (status) return status; -// } else if (keywordToken == "sensor") { -// mTokenizer->skipDelimiters(WHITESPACE); -// status_t status = parseSensor(); -// if (status) return status; + } else if (keywordToken == "led") { + // Skip LEDs, we don't need them for Key Mapper + mTokenizer->nextLine(); + continue; + } else if (keywordToken == "sensor") { + // Skip Sensors, we don't need them for Key Mapper + mTokenizer->nextLine(); + continue; } else if (keywordToken == "requires_kernel_config") { mTokenizer->skipDelimiters(WHITESPACE); status_t status = parseRequiredKernelConfig(); @@ -237,8 +238,9 @@ namespace android { } mTokenizer->skipDelimiters(WHITESPACE); + if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') { - LOGE("%s: Expected end of line or trailing comment, got '%s'.", + LOGW("%s: Expected end of line or trailing comment, got '%s'.", mTokenizer->getLocation().c_str(), mTokenizer->peekRemainderOfLine().c_str()); return BAD_VALUE; @@ -276,10 +278,11 @@ namespace android { mTokenizer->skipDelimiters(WHITESPACE); String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE); std::optional keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.c_str()); + if (!keyCode) { - LOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().c_str(), + LOGW("%s: Unknown key code label %s", mTokenizer->getLocation().c_str(), keyCodeToken.c_str()); - return BAD_VALUE; + // Do not crash at this point because there may be more flags afterwards that need parsing. } uint32_t flags = 0; @@ -305,10 +308,14 @@ namespace android { ALOGD_IF(DEBUG_PARSER, "Parsed key %s: code=%d, keyCode=%d, flags=0x%08x.", mapUsage ? "usage" : "scan code", *code, *keyCode, flags); - Key key; - key.keyCode = *keyCode; - key.flags = flags; - map.insert({*code, key}); + // The key code may be unknown so only insert a key if it is. + if (keyCode) { + Key key; + key.keyCode = *keyCode; + key.flags = flags; + map.insert({*code, key}); + } + return NO_ERROR; } diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h index bc87a0ab1b..f5646f33e3 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h @@ -80,8 +80,6 @@ namespace android { std::optional mapAxis(int32_t scanCode) const; - const std::string getLoadFileName() const; - virtual ~KeyLayoutMap(); private: diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 724b4435e5..ea9a802637 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -37,6 +37,13 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) private val adbConnectMdns: AdbMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) + init { + // TODO remove + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + startWithAdb() + } + } + // TODO clean up // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) From 0e0464a610bcbf39321524d5a69d8a9f8788baad Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 24 Jul 2025 14:31:10 -0600 Subject: [PATCH 029/215] #1394 add InputDevice.cpp and its dependencies --- sysbridge/src/main/cpp/CMakeLists.txt | 5 +- .../src/main/cpp/android/ftl/algorithm.h | 108 ++++ sysbridge/src/main/cpp/android/ftl/cast.h | 86 ++++ sysbridge/src/main/cpp/android/ftl/concat.h | 90 ++++ .../cpp/android/ftl/details/array_traits.h | 181 +++++++ .../src/main/cpp/android/ftl/details/cast.h | 57 +++ .../src/main/cpp/android/ftl/details/concat.h | 91 ++++ .../main/cpp/android/ftl/details/function.h | 137 +++++ .../src/main/cpp/android/ftl/details/future.h | 119 +++++ .../src/main/cpp/android/ftl/details/hash.h | 125 +++++ .../src/main/cpp/android/ftl/details/match.h | 59 +++ .../src/main/cpp/android/ftl/details/mixins.h | 31 ++ .../main/cpp/android/ftl/details/optional.h | 72 +++ .../cpp/android/ftl/details/type_traits.h | 33 ++ sysbridge/src/main/cpp/android/ftl/enum.h | 369 ++++++++++++++ sysbridge/src/main/cpp/android/ftl/expected.h | 144 ++++++ .../src/main/cpp/android/ftl/fake_guard.h | 87 ++++ .../src/main/cpp/android/ftl/finalizer.h | 213 ++++++++ sysbridge/src/main/cpp/android/ftl/flags.h | 246 +++++++++ sysbridge/src/main/cpp/android/ftl/function.h | 313 ++++++++++++ sysbridge/src/main/cpp/android/ftl/future.h | 135 +++++ sysbridge/src/main/cpp/android/ftl/hash.h | 44 ++ sysbridge/src/main/cpp/android/ftl/ignore.h | 43 ++ .../main/cpp/android/ftl/initializer_list.h | 112 +++++ sysbridge/src/main/cpp/android/ftl/match.h | 63 +++ sysbridge/src/main/cpp/android/ftl/mixins.h | 152 ++++++ sysbridge/src/main/cpp/android/ftl/non_null.h | 228 +++++++++ sysbridge/src/main/cpp/android/ftl/optional.h | 147 ++++++ .../src/main/cpp/android/ftl/shared_mutex.h | 49 ++ .../src/main/cpp/android/ftl/small_map.h | 309 ++++++++++++ .../src/main/cpp/android/ftl/small_vector.h | 469 ++++++++++++++++++ .../src/main/cpp/android/ftl/static_vector.h | 431 ++++++++++++++++ sysbridge/src/main/cpp/android/ftl/string.h | 105 ++++ sysbridge/src/main/cpp/android/ftl/unit.h | 79 +++ .../src/main/cpp/android/input/Input.cpp | 26 + sysbridge/src/main/cpp/android/input/Input.h | 4 + .../main/cpp/android/input/InputDevice.cpp | 344 +++++++++++++ .../src/main/cpp/android/input/InputDevice.h | 455 +++++++++++++++++ .../main/cpp/android/libbase/stringprintf.cpp | 85 ++++ .../main/cpp/android/libbase/stringprintf.h | 41 ++ .../main/cpp/android/ui/LogicalDisplayId.h | 59 +++ sysbridge/src/main/cpp/libevdev_jni.cpp | 2 + 42 files changed, 5947 insertions(+), 1 deletion(-) create mode 100644 sysbridge/src/main/cpp/android/ftl/algorithm.h create mode 100644 sysbridge/src/main/cpp/android/ftl/cast.h create mode 100644 sysbridge/src/main/cpp/android/ftl/concat.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/array_traits.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/cast.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/concat.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/function.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/future.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/hash.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/match.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/mixins.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/optional.h create mode 100644 sysbridge/src/main/cpp/android/ftl/details/type_traits.h create mode 100644 sysbridge/src/main/cpp/android/ftl/enum.h create mode 100644 sysbridge/src/main/cpp/android/ftl/expected.h create mode 100644 sysbridge/src/main/cpp/android/ftl/fake_guard.h create mode 100644 sysbridge/src/main/cpp/android/ftl/finalizer.h create mode 100644 sysbridge/src/main/cpp/android/ftl/flags.h create mode 100644 sysbridge/src/main/cpp/android/ftl/function.h create mode 100644 sysbridge/src/main/cpp/android/ftl/future.h create mode 100644 sysbridge/src/main/cpp/android/ftl/hash.h create mode 100644 sysbridge/src/main/cpp/android/ftl/ignore.h create mode 100644 sysbridge/src/main/cpp/android/ftl/initializer_list.h create mode 100644 sysbridge/src/main/cpp/android/ftl/match.h create mode 100644 sysbridge/src/main/cpp/android/ftl/mixins.h create mode 100644 sysbridge/src/main/cpp/android/ftl/non_null.h create mode 100644 sysbridge/src/main/cpp/android/ftl/optional.h create mode 100644 sysbridge/src/main/cpp/android/ftl/shared_mutex.h create mode 100644 sysbridge/src/main/cpp/android/ftl/small_map.h create mode 100644 sysbridge/src/main/cpp/android/ftl/small_vector.h create mode 100644 sysbridge/src/main/cpp/android/ftl/static_vector.h create mode 100644 sysbridge/src/main/cpp/android/ftl/string.h create mode 100644 sysbridge/src/main/cpp/android/ftl/unit.h create mode 100644 sysbridge/src/main/cpp/android/input/Input.cpp create mode 100644 sysbridge/src/main/cpp/android/input/InputDevice.cpp create mode 100644 sysbridge/src/main/cpp/android/input/InputDevice.h create mode 100644 sysbridge/src/main/cpp/android/libbase/stringprintf.cpp create mode 100644 sysbridge/src/main/cpp/android/libbase/stringprintf.h create mode 100644 sysbridge/src/main/cpp/android/ui/LogicalDisplayId.h diff --git a/sysbridge/src/main/cpp/CMakeLists.txt b/sysbridge/src/main/cpp/CMakeLists.txt index 58cc1fd1d8..12c2f39864 100644 --- a/sysbridge/src/main/cpp/CMakeLists.txt +++ b/sysbridge/src/main/cpp/CMakeLists.txt @@ -88,7 +88,10 @@ add_library(evdev SHARED android/utils/String8.cpp android/utils/SharedBuffer.cpp android/utils/FileMap.cpp - android/utils/Unicode.cpp) + android/utils/Unicode.cpp + android/input/InputDevice.cpp + android/input/Input.cpp + android/libbase/stringprintf.cpp) # Specifies libraries CMake should link to your target library. You # can link libraries from various origins, such as libraries defined in this diff --git a/sysbridge/src/main/cpp/android/ftl/algorithm.h b/sysbridge/src/main/cpp/android/ftl/algorithm.h new file mode 100644 index 0000000000..81993de177 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/algorithm.h @@ -0,0 +1,108 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace android::ftl { + +// Determines if a container contains a value. This is a simplified version of the C++23 +// std::ranges::contains function. +// +// const ftl::StaticVector vector = {1, 2, 3}; +// assert(ftl::contains(vector, 1)); +// +// TODO: Remove in C++23. + template + auto contains(const Container &container, const Value &value) -> bool { + return std::find(container.begin(), container.end(), value) != container.end(); + } + +// Adapter for std::find_if that converts the return value from iterator to optional. +// +// const ftl::StaticVector vector = {"upside"sv, "down"sv, "cake"sv}; +// assert(ftl::find_if(vector, [](const auto& str) { return str.front() == 'c'; }) == "cake"sv); +// + template + constexpr auto find_if(const Container &container, Predicate &&predicate) + -> Optional> { + const auto it = std::find_if(std::cbegin(container), std::cend(container), + std::forward(predicate)); + if (it == std::cend(container)) return {}; + return std::cref(*it); + } + +// Transformers for ftl::find_if on a map-like `Container` that contains key-value pairs. +// +// const ftl::SmallMap map = ftl::init::map>( +// 12, "snow"sv, "cone"sv)(13, "tiramisu"sv)(14, "upside"sv, "down"sv, "cake"sv); +// +// using Map = decltype(map); +// +// assert(14 == ftl::find_if(map, [](const auto& pair) { +// return pair.second.size() == 3; +// }).transform(ftl::to_key)); +// +// const auto opt = ftl::find_if(map, [](const auto& pair) { +// return pair.second.size() == 1; +// }).transform(ftl::to_mapped_ref); +// +// assert(opt); +// assert(opt->get() == ftl::StaticVector("tiramisu"sv)); +// + template + constexpr auto to_key(const Pair &pair) -> Key { + return pair.first; + } + + template + constexpr auto to_mapped_ref(const Pair &pair) -> std::reference_wrapper { + return std::cref(pair.second); + } + +// Combinator for ftl::Optional::or_else when T is std::reference_wrapper. Given a +// lambda argument that returns a `constexpr` value, ftl::static_ref binds a reference to a +// static T initialized to that constant. +// +// const ftl::SmallMap map = ftl::init::map(13, "tiramisu"sv)(14, "upside-down cake"sv); +// assert("???"sv == +// map.get(20).or_else(ftl::static_ref([] { return "???"sv; }))->get()); +// +// using Map = decltype(map); +// +// assert("snow cone"sv == +// ftl::find_if(map, [](const auto& pair) { return pair.second.front() == 's'; }) +// .transform(ftl::to_mapped_ref) +// .or_else(ftl::static_ref([] { return "snow cone"sv; })) +// ->get()); +// + template + constexpr auto static_ref(F &&f) { + return [f = std::forward(f)] { + constexpr auto kInitializer = f(); + static const T kValue = kInitializer; + return Optional(std::cref(kValue)); + }; + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/cast.h b/sysbridge/src/main/cpp/android/ftl/cast.h new file mode 100644 index 0000000000..64129a1a58 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/cast.h @@ -0,0 +1,86 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace android::ftl { + + enum class CastSafety { + kSafe, kUnderflow, kOverflow + }; + +// Returns whether static_cast(v) is safe, or would result in underflow or overflow. +// +// static_assert(ftl::cast_safety(-1) == ftl::CastSafety::kUnderflow); +// static_assert(ftl::cast_safety(128u) == ftl::CastSafety::kOverflow); +// +// static_assert(ftl::cast_safety(-.1f) == ftl::CastSafety::kUnderflow); +// static_assert(ftl::cast_safety(static_cast(INT32_MAX)) == +// ftl::CastSafety::kOverflow); +// +// static_assert(ftl::cast_safety(-DBL_MAX) == ftl::CastSafety::kUnderflow); +// + template + constexpr CastSafety cast_safety(T v) { + static_assert(std::is_arithmetic_v); + static_assert(std::is_arithmetic_v); + + constexpr bool kFromSigned = std::is_signed_v; + constexpr bool kToSigned = std::is_signed_v; + + using details::max_exponent; + + // If the R range contains the T range, then casting is always safe. + if constexpr ((kFromSigned == kToSigned && max_exponent >= max_exponent) || + (!kFromSigned && kToSigned && max_exponent > max_exponent)) { + return CastSafety::kSafe; + } + + using C = std::common_type_t; + + if constexpr (kFromSigned) { + using L = details::safe_limits; + + if constexpr (kToSigned) { + // Signed to signed. + if (v < L::lowest()) return CastSafety::kUnderflow; + return v <= L::max() ? CastSafety::kSafe : CastSafety::kOverflow; + } else { + // Signed to unsigned. + if (v < 0) return CastSafety::kUnderflow; + return static_cast(v) <= static_cast(L::max()) ? CastSafety::kSafe + : CastSafety::kOverflow; + } + } else { + using L = std::numeric_limits; + + if constexpr (kToSigned) { + // Unsigned to signed. + return static_cast(v) <= static_cast(L::max()) ? CastSafety::kSafe + : CastSafety::kOverflow; + } else { + // Unsigned to unsigned. + return v <= L::max() ? CastSafety::kSafe : CastSafety::kOverflow; + } + } + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/concat.h b/sysbridge/src/main/cpp/android/ftl/concat.h new file mode 100644 index 0000000000..b826a2e858 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/concat.h @@ -0,0 +1,90 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android::ftl { + +// Lightweight (not allocating nor sprintf-based) concatenation. The variadic arguments can be +// values of integral type (including bool and char), string literals, or strings whose length +// is constrained: +// +// std::string_view name = "Volume"; +// ftl::Concat string(ftl::truncated<3>(name), ": ", -3, " dB"); +// +// assert(string.str() == "Vol: -3 dB"); +// assert(string.c_str()[string.size()] == '\0'); +// + template + struct Concat; + + template + struct Concat : Concat::N, Ts...> { + explicit constexpr Concat(T v, Ts... args) { append(v, args...); } + + protected: + constexpr Concat() = default; + + constexpr void append(T v, Ts... args) { + using Str = details::StaticString; + const Str str(v); + + // TODO: Replace with constexpr std::copy in C++20. + for (auto it = str.view.begin(); it != str.view.end();) { + *this->end_++ = *it++; + } + + using Base = Concat; + this->Base::append(args...); + } + }; + + template + struct Concat { + static constexpr std::size_t max_size() { return N; } + + constexpr std::size_t size() const { return static_cast(end_ - buffer_); } + + constexpr const char *c_str() const { return buffer_; } + + constexpr std::string_view str() const { + // TODO: Replace with {buffer_, end_} in C++20. + return {buffer_, size()}; + } + + protected: + constexpr Concat() : end_(buffer_) {} + + constexpr Concat(const Concat &) = delete; + + constexpr void append() { *end_ = '\0'; } + + char buffer_[N + 1]; + char *end_; + }; + +// Deduction guide. + template + Concat(Ts &&...) -> Concat<0, Ts...>; + + template + constexpr auto truncated(std::string_view v) { + return details::Truncated{v}; + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/details/array_traits.h b/sysbridge/src/main/cpp/android/ftl/details/array_traits.h new file mode 100644 index 0000000000..f66742598c --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/array_traits.h @@ -0,0 +1,181 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#define FTL_ARRAY_TRAIT(T, U) using U = typename details::ArrayTraits::U + +namespace android::ftl::details { + + template + struct ArrayTraits { + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + using pointer = value_type *; + using reference = value_type &; + using iterator = pointer; + using reverse_iterator = std::reverse_iterator; + + using const_pointer = const value_type *; + using const_reference = const value_type &; + using const_iterator = const_pointer; + using const_reverse_iterator = std::reverse_iterator; + + template + static constexpr pointer construct_at(const_iterator it, Args &&... args) { + void *const ptr = const_cast(static_cast(it)); + if constexpr (std::is_constructible_v) { + // TODO: Replace with std::construct_at in C++20. + return new(ptr) value_type(std::forward(args)...); + } else { + // Fall back to list initialization. + return new(ptr) value_type{std::forward(args)...}; + } + } + + // TODO: Make constexpr in C++20. + template + static reference replace_at(const_iterator it, Args &&... args) { + value_type element{std::forward(args)...}; + return replace_at(it, std::move(element)); + } + + // TODO: Make constexpr in C++20. + static reference replace_at(const_iterator it, value_type &&value) { + std::destroy_at(it); + // This is only safe because exceptions are disabled. + return *construct_at(it, std::move(value)); + } + + // TODO: Make constexpr in C++20. + static void in_place_swap(reference a, reference b) { + value_type c{std::move(a)}; + replace_at(&a, std::move(b)); + replace_at(&b, std::move(c)); + } + + // TODO: Make constexpr in C++20. + static void in_place_swap_ranges(iterator first1, iterator last1, iterator first2) { + while (first1 != last1) { + in_place_swap(*first1++, *first2++); + } + } + + // TODO: Replace with std::uninitialized_copy in C++20. + template + static void uninitialized_copy(Iterator first, Iterator last, const_iterator out) { + while (first != last) { + construct_at(out++, *first++); + } + } + }; + +// CRTP mixin to define iterator functions in terms of non-const Self::begin and Self::end. + template + class ArrayIterators { + FTL_ARRAY_TRAIT(T, size_type); + + FTL_ARRAY_TRAIT(T, reference); + FTL_ARRAY_TRAIT(T, iterator); + FTL_ARRAY_TRAIT(T, reverse_iterator); + + FTL_ARRAY_TRAIT(T, const_reference); + FTL_ARRAY_TRAIT(T, const_iterator); + FTL_ARRAY_TRAIT(T, const_reverse_iterator); + + Self &self() const { return *const_cast(static_cast(this)); } + + public: + const_iterator begin() const { return cbegin(); } + + const_iterator cbegin() const { return self().begin(); } + + const_iterator end() const { return cend(); } + + const_iterator cend() const { return self().end(); } + + reverse_iterator rbegin() { return std::make_reverse_iterator(self().end()); } + + const_reverse_iterator rbegin() const { return crbegin(); } + + const_reverse_iterator crbegin() const { return self().rbegin(); } + + reverse_iterator rend() { return std::make_reverse_iterator(self().begin()); } + + const_reverse_iterator rend() const { return crend(); } + + const_reverse_iterator crend() const { return self().rend(); } + + iterator last() { return self().end() - 1; } + + const_iterator last() const { return self().last(); } + + reference front() { return *self().begin(); } + + const_reference front() const { return self().front(); } + + reference back() { return *last(); } + + const_reference back() const { return self().back(); } + + reference operator[](size_type i) { return *(self().begin() + i); } + + const_reference operator[](size_type i) const { return self()[i]; } + }; + +// Mixin to define comparison operators for an array-like template. +// TODO: Replace with operator<=> in C++20. + template class Array> + struct ArrayComparators { + template + friend bool operator==(const Array &lhs, const Array &rhs) { + return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin()); + } + + template + friend bool operator<(const Array &lhs, const Array &rhs) { + return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); + } + + template + friend bool operator>(const Array &lhs, const Array &rhs) { + return rhs < lhs; + } + + template + friend bool operator!=(const Array &lhs, const Array &rhs) { + return !(lhs == rhs); + } + + template + friend bool operator>=(const Array &lhs, const Array &rhs) { + return !(lhs < rhs); + } + + template + friend bool operator<=(const Array &lhs, const Array &rhs) { + return !(lhs > rhs); + } + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/cast.h b/sysbridge/src/main/cpp/android/ftl/details/cast.h new file mode 100644 index 0000000000..8196ba61a7 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/cast.h @@ -0,0 +1,57 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android::ftl::details { + +// Exponent whose power of 2 is the (exclusive) upper bound of T. + template> + constexpr int max_exponent = std::is_floating_point_v ? L::max_exponent : L::digits; + +// Extension of std::numeric_limits that reduces the maximum for integral types T such that it +// has an exact representation for floating-point types F. For example, the maximum int32_t value +// is 2'147'483'647, but casting it to float commonly rounds up to 2'147'483'650.f, which cannot +// be safely converted back lest the signed overflow invokes undefined behavior. This pitfall is +// avoided by clearing the lower (31 - 24 =) 7 bits of precision to 2'147'483'520. Note that the +// minimum is representable. + template + struct safe_limits : std::numeric_limits { + static constexpr T max() { + using Base = std::numeric_limits; + + if constexpr (std::is_integral_v && std::is_floating_point_v) { + // Assume the mantissa is 24 bits for float, or 53 bits for double. + using Float = std::numeric_limits; + static_assert(Float::is_iec559); + + // If the integer is wider than the mantissa, clear the excess bits of precision. + constexpr int kShift = Base::digits - Float::digits; + if constexpr (kShift > 0) { + using U = std::make_unsigned_t; + constexpr U kOne = static_cast(1); + return static_cast(Base::max()) & ~((kOne << kShift) - kOne); + } + } + + return Base::max(); + } + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/concat.h b/sysbridge/src/main/cpp/android/ftl/details/concat.h new file mode 100644 index 0000000000..c1462a658c --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/concat.h @@ -0,0 +1,91 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace android::ftl::details { + + template + struct StaticString; + +// Booleans. + template + struct StaticString>> { + static constexpr std::size_t N = 5; // Length of "false". + + explicit constexpr StaticString(bool b) : view(b ? "true" : "false") {} + + const std::string_view view; + }; + +// Characters. + template + struct StaticString>> { + static constexpr std::size_t N = 1; + + explicit constexpr StaticString(char c) : character(c) {} + + const char character; + const std::string_view view{&character, 1u}; + }; + +// Integers, including the integer value of other character types like char32_t. + template + struct StaticString< + T, std::enable_if_t< + std::is_integral_v> && !is_bool_v && !is_char_v>> { + using U = remove_cvref_t; + static constexpr std::size_t N = to_chars_length_v; + + // TODO: Mark this and to_chars as `constexpr` in C++23. + explicit StaticString(U v) : view(to_chars(buffer, v)) {} + + to_chars_buffer_t buffer; + const std::string_view view; + }; + +// Character arrays. + template + struct StaticString { + static constexpr std::size_t N = M - 1; + + explicit constexpr StaticString(const char (&str)[M]) : view(str, N) {} + + const std::string_view view; + }; + + template + struct Truncated { + std::string_view view; + }; + +// Strings with constrained length. + template + struct StaticString, void> { + static constexpr std::size_t N = M; + + explicit constexpr StaticString(Truncated str) : view(str.view.substr(0, N)) {} + + const std::string_view view; + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/function.h b/sysbridge/src/main/cpp/android/ftl/details/function.h new file mode 100644 index 0000000000..8d7fc9bd58 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/function.h @@ -0,0 +1,137 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace android::ftl::details { + +// The maximum allowed value for the template argument `N` in +// `ftl::Function`. + constexpr size_t kFunctionMaximumN = 14; + +// Converts a member function pointer type `Ret(Class::*)(Args...)` to an equivalent non-member +// function type `Ret(Args...)`. + + template + struct remove_member_function_pointer; + + template + struct remove_member_function_pointer { + using type = Ret(Args...); + }; + + template + struct remove_member_function_pointer { + using type = Ret(Args...); + }; + + template + using remove_member_function_pointer_t = + typename remove_member_function_pointer::type; + +// Helper functions for binding to the supported targets. + + template + auto bind_opaque_no_op() -> Ret (*)(void *, Args...) { + return [](void *, Args...) -> Ret { + if constexpr (!std::is_void_v) { + return Ret{}; + } + }; + } + + template + auto bind_opaque_function_object(const F &) -> Ret (*)(void *, Args...) { + return [](void *opaque, Args... args) -> Ret { + return std::invoke(*static_cast(opaque), std::forward(args)...); + }; + } + + template + auto bind_member_function(Class *instance, Ret (*)(Args...) = nullptr) { + return [instance](Args... args) -> Ret { + return std::invoke(MemberFunction, instance, std::forward(args)...); + }; + } + + template + auto bind_free_function(Ret (*)(Args...) = nullptr) { + return [](Args... args) -> Ret { + return std::invoke(FreeFunction, std::forward(args)...); + }; + } + +// Traits class for the opaque storage used by Function. + + template + struct function_opaque_storage { + // The actual type used for the opaque storage. An `N` of zero specifies the minimum useful size, + // which allows a lambda with zero or one capture args. + using type = std::array; + + template + static constexpr bool require_trivially_copyable = std::is_trivially_copyable_v; + + template + static constexpr bool require_trivially_destructible = std::is_trivially_destructible_v; + + template + static constexpr bool require_will_fit_in_opaque_storage = sizeof(S) <= sizeof(type); + + template + static constexpr bool require_alignment_compatible = + std::alignment_of_v <= std::alignment_of_v; + + // Copies `src` into the opaque storage, and returns that storage. + template + static type opaque_copy(const S &src) { + // TODO: Replace with C++20 concepts/constraints which can give more details. + static_assert(require_trivially_copyable, + "ftl::Function can only store lambdas that capture trivially copyable data."); + static_assert( + require_trivially_destructible, + "ftl::Function can only store lambdas that capture trivially destructible data."); + static_assert(require_will_fit_in_opaque_storage, + "ftl::Function has limited storage for lambda captured state. Maybe you need to " + "increase N?"); + static_assert(require_alignment_compatible); + + type opaque; + std::memcpy(opaque.data(), &src, sizeof(S)); + return opaque; + } + }; + +// Traits class to help determine the template parameters to use for a ftl::Function, given a +// function object. + + template + struct function_traits { + // The function type `F` with which to instantiate the `Function` template. + using type = remove_member_function_pointer_t<&F::operator()>; + + // The (minimum) size `N` with which to instantiate the `Function` template. + static constexpr std::size_t size = + (std::max(sizeof(std::intptr_t), sizeof(F)) - 1) / sizeof(std::intptr_t); + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/future.h b/sysbridge/src/main/cpp/android/ftl/details/future.h new file mode 100644 index 0000000000..ec3926ba8c --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/future.h @@ -0,0 +1,119 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace android::ftl { + + template class> + class Future; + + namespace details { + + template + struct future_result { + using type = T; + }; + + template + struct future_result> { + using type = T; + }; + + template + struct future_result> { + using type = T; +}; + +template class FutureImpl> +struct future_result> { +using type = T; +}; + +template +using future_result_t = typename future_result::type; + +struct ValueTag { +}; + +template class> +class BaseFuture; + +template +class BaseFuture { + using Impl = std::future; + +public: + Future share() { + if (T *value = std::get_if(&self())) { + return {ValueTag{}, std::move(*value)}; + } + + return std::get(self()).share(); + } + +protected: + T get() { + if (T *value = std::get_if(&self())) { + return std::move(*value); + } + + return std::get(self()).get(); + } + + template + std::future_status wait_for(const std::chrono::duration &timeout_duration) const { + if (std::holds_alternative(self())) { + return std::future_status::ready; + } + + return std::get(self()).wait_for(timeout_duration); + } + +private: + auto &self() { return static_cast(*this).future_; } + + const auto &self() const { return static_cast(*this).future_; } +}; + +template +class BaseFuture { + using Impl = std::shared_future; + +protected: + const T &get() const { + if (const T *value = std::get_if(&self())) { + return *value; + } + + return std::get(self()).get(); + } + + template + std::future_status wait_for(const std::chrono::duration &timeout_duration) const { + if (std::holds_alternative(self())) { + return std::future_status::ready; + } + + return std::get(self()).wait_for(timeout_duration); + } + +private: + const auto &self() const { return static_cast(*this).future_; } +}; + +} // namespace details +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/details/hash.h b/sysbridge/src/main/cpp/android/ftl/details/hash.h new file mode 100644 index 0000000000..cb9939036a --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/hash.h @@ -0,0 +1,125 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android::ftl::details { + +// Based on CityHash64 v1.0.1 (http://code.google.com/p/cityhash/), but slightly +// modernized and trimmed for cases with bounded lengths. + + template + inline T read_unaligned(const void *ptr) { + T v; + std::memcpy(&v, ptr, sizeof(T)); + return v; + } + + template + constexpr std::uint64_t rotate(std::uint64_t v, std::uint8_t shift) { + if constexpr (!NonZeroShift) { + if (shift == 0) return v; + } + return (v >> shift) | (v << (64 - shift)); + } + + constexpr std::uint64_t shift_mix(std::uint64_t v) { + return v ^ (v >> 47); + } + + __attribute__((no_sanitize("unsigned-integer-overflow"))) + constexpr std::uint64_t hash_length_16(std::uint64_t u, std::uint64_t v) { + constexpr std::uint64_t kPrime = 0x9ddfea08eb382d69ull; + auto a = (u ^ v) * kPrime; + a ^= (a >> 47); + auto b = (v ^ a) * kPrime; + b ^= (b >> 47); + b *= kPrime; + return b; + } + + constexpr std::uint64_t kPrime0 = 0xc3a5c85c97cb3127ull; + constexpr std::uint64_t kPrime1 = 0xb492b66fbe98f273ull; + constexpr std::uint64_t kPrime2 = 0x9ae16a3b2f90404full; + constexpr std::uint64_t kPrime3 = 0xc949d7c7509e6557ull; + + __attribute__((no_sanitize("unsigned-integer-overflow"))) + inline std::uint64_t hash_length_0_to_16(const char *str, std::uint64_t length) { + if (length > 8) { + const auto a = read_unaligned(str); + const auto b = read_unaligned(str + length - 8); + return hash_length_16(a, rotate(b + length, static_cast(length))) ^ + b; + } + if (length >= 4) { + const auto a = read_unaligned(str); + const auto b = read_unaligned(str + length - 4); + return hash_length_16(length + (a << 3), b); + } + if (length > 0) { + const auto a = static_cast(str[0]); + const auto b = static_cast(str[length >> 1]); + const auto c = static_cast(str[length - 1]); + const auto y = static_cast(a) + (static_cast(b) << 8); + const auto z = + static_cast(length) + (static_cast(c) << 2); + return shift_mix(y * kPrime2 ^ z * kPrime3) * kPrime2; + } + return kPrime2; + } + + __attribute__((no_sanitize("unsigned-integer-overflow"))) + inline std::uint64_t hash_length_17_to_32(const char *str, std::uint64_t length) { + const auto a = read_unaligned(str) * kPrime1; + const auto b = read_unaligned(str + 8); + const auto c = read_unaligned(str + length - 8) * kPrime2; + const auto d = read_unaligned(str + length - 16) * kPrime0; + return hash_length_16(rotate(a - b, 43) + rotate(c, 30) + d, + a + rotate(b ^ kPrime3, 20) - c + length); + } + + __attribute__((no_sanitize("unsigned-integer-overflow"))) + inline std::uint64_t hash_length_33_to_64(const char *str, std::uint64_t length) { + auto z = read_unaligned(str + 24); + auto a = read_unaligned(str) + (length + read_unaligned(str + length - 16)) * kPrime0; + auto b = rotate(a + z, 52); + auto c = rotate(a, 37); + + a += read_unaligned(str + 8); + c += rotate(a, 7); + a += read_unaligned(str + 16); + + const auto vf = a + z; + const auto vs = b + rotate(a, 31) + c; + + a = read_unaligned(str + 16) + read_unaligned(str + length - 32); + z += read_unaligned(str + length - 8); + b = rotate(a + z, 52); + c = rotate(a, 37); + a += read_unaligned(str + length - 24); + c += rotate(a, 7); + a += read_unaligned(str + length - 16); + + const auto wf = a + z; + const auto ws = b + rotate(a, 31) + c; + const auto r = shift_mix((vf + ws) * kPrime2 + (wf + vs) * kPrime0); + return shift_mix(r * kPrime0 + vs) * kPrime2; + } + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/match.h b/sysbridge/src/main/cpp/android/ftl/details/match.h new file mode 100644 index 0000000000..cee77c83ee --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/match.h @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android::ftl::details { + + template + struct Matcher : Ms ... { + using Ms::operator()...; + }; + +// Deduction guide. + template + Matcher(Ms...) -> Matcher; + + template + constexpr bool is_exhaustive_match_v = (std::is_invocable_v && ...); + + template + struct Match; + + template + struct Match { + template + static decltype(auto) match(Variant &variant, const Matcher &matcher) { + if (auto *const ptr = std::get_if(&variant)) { + return matcher(*ptr); + } else { + return Match::match(variant, matcher); + } + } + }; + + template + struct Match { + template + static decltype(auto) match(Variant &variant, const Matcher &matcher) { + return matcher(std::get(variant)); + } + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/mixins.h b/sysbridge/src/main/cpp/android/ftl/details/mixins.h new file mode 100644 index 0000000000..a52d74c504 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/mixins.h @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace android::ftl::details { + + template class> + class Mixin { + protected: + constexpr Self &self() { return *static_cast(this); } + + constexpr const Self &self() const { return *static_cast(this); } + + constexpr auto &mut() { return self().value_; } + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/optional.h b/sysbridge/src/main/cpp/android/ftl/details/optional.h new file mode 100644 index 0000000000..bcbd8826c0 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/optional.h @@ -0,0 +1,72 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace android::ftl { + + template + struct Optional; + + namespace details { + + template + struct is_optional : std::false_type { + }; + + template + struct is_optional> : std::true_type { + }; + + template + struct is_optional> : std::true_type { + }; + + template + struct transform_result { + using type = Optional>>; + }; + + template + using transform_result_t = typename transform_result::type; + + template + struct and_then_result { + using type = remove_cvref_t>; + static_assert(is_optional{}, "and_then function must return an optional"); + }; + + template + using and_then_result_t = typename and_then_result::type; + + template + struct or_else_result { + using type = remove_cvref_t>; + static_assert( + std::is_same_v> || std::is_same_v>, + "or_else function must return an optional T"); + }; + + template + using or_else_result_t = typename or_else_result::type; + + } // namespace details +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/details/type_traits.h b/sysbridge/src/main/cpp/android/ftl/details/type_traits.h new file mode 100644 index 0000000000..70a602c330 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/type_traits.h @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android::ftl::details { + +// TODO: Replace with std::remove_cvref_t in C++20. + template + using remove_cvref_t = std::remove_cv_t>; + + template + constexpr bool is_bool_v = std::is_same_v, bool>; + + template + constexpr bool is_char_v = std::is_same_v, char>; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/enum.h b/sysbridge/src/main/cpp/android/ftl/enum.h new file mode 100644 index 0000000000..e31c0a1322 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/enum.h @@ -0,0 +1,369 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +// Returns the name of enumerator E::V and optionally the class (i.e. "E::V" or "V") as +// std::optional by parsing the compiler-generated string literal for the +// signature of this function. The function is defined in the global namespace with a short name +// and inferred return type to reduce bloat in the read-only data segment. +template +constexpr auto ftl_enum_builder() { + static_assert(std::is_enum_v); + + using R = std::optional; + using namespace std::literals; + + // The "pretty" signature has the following format: + // + // auto ftl_enum() [E = android::test::Enum, V = android::test::Enum::kValue] + // + std::string_view view = __PRETTY_FUNCTION__; + const auto template_begin = view.rfind('['); + const auto template_end = view.rfind(']'); + if (template_begin == view.npos || template_end == view.npos) return R{}; + + // Extract the template parameters without the enclosing brackets. Example (cont'd): + // + // E = android::test::Enum, V = android::test::Enum::kValue + // + view = view.substr(template_begin + 1, template_end - template_begin - 1); + const auto value_begin = view.rfind("V = "sv); + if (value_begin == view.npos) return R{}; + + // Example (cont'd): + // + // V = android::test::Enum::kValue + // + view = view.substr(value_begin); + const auto pos = S ? view.rfind("::"sv) - 2 : view.npos; + + const auto name_begin = view.rfind("::"sv, pos); + if (name_begin == view.npos) return R{}; + + // Chop off the leading "::". + const auto name = view.substr(name_begin + 2); + + // A value that is not enumerated has the format "Enum)42". + return name.find(')') == view.npos ? R{name} : R{}; +} + +// Returns the name of enumerator E::V (i.e. "V") as std::optional +template +constexpr auto ftl_enum() { + return ftl_enum_builder(); +} + +// Returns the name of enumerator and class E::V (i.e. "E::V") as std::optional +template +constexpr auto ftl_enum_full() { + return ftl_enum_builder(); +} + +namespace android::ftl { + +// Trait for determining whether a type is specifically a scoped enum or not. By definition, a +// scoped enum is one that is not implicitly convertible to its underlying type. +// +// TODO: Replace with std::is_scoped_enum in C++23. +// + template> + struct is_scoped_enum : std::false_type { + }; + + template + struct is_scoped_enum + : std::negation>> { + }; + + template + inline constexpr bool is_scoped_enum_v = is_scoped_enum::value; + +// Shorthand for casting an enumerator to its integral value. +// +// TODO: Replace with std::to_underlying in C++23. +// +// enum class E { A, B, C }; +// static_assert(ftl::to_underlying(E::B) == 1); +// + template>> + constexpr auto to_underlying(E v) { + return static_cast>(v); + } + +// Traits for retrieving an enum's range. An enum specifies its range by defining enumerators named +// ftl_first and ftl_last. If omitted, ftl_first defaults to 0, whereas ftl_last defaults to N - 1 +// where N is the bit width of the underlying type, but only if that type is unsigned, assuming the +// enumerators are flags. Also, note that unscoped enums must define both bounds, as casting out-of- +// range values results in undefined behavior if the underlying type is not fixed. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// static_assert(ftl::enum_begin_v == E::A); +// static_assert(ftl::enum_last_v == E::F); +// static_assert(ftl::enum_size_v == 6); +// +// enum class F : std::uint16_t { X = 0b1, Y = 0b10, Z = 0b100 }; +// +// static_assert(ftl::enum_begin_v == F{0}); +// static_assert(ftl::enum_last_v == F{15}); +// static_assert(ftl::enum_size_v == 16); +// + template + struct enum_begin { + static_assert(is_scoped_enum_v, "Missing ftl_first enumerator"); + static constexpr E value{0}; + }; + + template + struct enum_begin> { + static constexpr E value = E::ftl_first; + }; + + template + inline constexpr E enum_begin_v = enum_begin::value; + + template + struct enum_end { + using U = std::underlying_type_t; + static_assert(is_scoped_enum_v && std::is_unsigned_v, "Missing ftl_last enumerator"); + + static constexpr E value{std::numeric_limits::digits}; + }; + + template + struct enum_end> { + static constexpr E value = E{to_underlying(E::ftl_last) + 1}; + }; + + template + inline constexpr E enum_end_v = enum_end::value; + + template + inline constexpr E enum_last_v = E{to_underlying(enum_end_v) - 1}; + + template + struct enum_size { + static constexpr auto kBegin = to_underlying(enum_begin_v); + static constexpr auto kEnd = to_underlying(enum_end_v); + static_assert(kBegin < kEnd, "Invalid range"); + + static constexpr std::size_t value = kEnd - kBegin; + static_assert(value <= 64, "Excessive range size"); + }; + + template + inline constexpr std::size_t enum_size_v = enum_size::value; + + namespace details { + + template + struct Identity { + static constexpr auto value = V; + }; + + template + using make_enum_sequence = std::make_integer_sequence, enum_size_v>; + + template class = Identity, typename = make_enum_sequence> + struct EnumRange; + + template class F, typename T, T... Vs> + struct EnumRange> { + static constexpr auto kBegin = to_underlying(enum_begin_v); + static constexpr auto kSize = enum_size_v; + + using R = decltype(F::value); + const R values[kSize] = {F(Vs + kBegin)>::value...}; + + constexpr const auto *begin() const { return values; } + + constexpr const auto *end() const { return values + kSize; } + }; + + template + struct EnumName { + static constexpr auto value = ftl_enum(); + }; + + template + struct EnumNameFull { + static constexpr auto value = ftl_enum_full(); + }; + + template + struct FlagName { + using E = decltype(I); + using U = std::underlying_type_t; + + static constexpr E V{U{1} << to_underlying(I)}; + static constexpr auto value = ftl_enum(); + }; + + } // namespace details + +// Returns an iterable over the range of an enum. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// std::string string; +// for (E v : ftl::enum_range()) { +// string += ftl::enum_name(v).value_or("?"); +// } +// +// assert(string == "ABC??F"); +// + template + constexpr auto enum_range() { + return details::EnumRange{}; + } + +// Returns a stringified enumerator at compile time. +// +// enum class E { A, B, C }; +// static_assert(ftl::enum_name() == "B"); +// + template + constexpr std::string_view enum_name() { + constexpr auto kName = ftl_enum(); + static_assert(kName, "Unknown enumerator"); + return *kName; + } + +// Returns a stringified enumerator with class at compile time. +// +// enum class E { A, B, C }; +// static_assert(ftl::enum_name() == "E::B"); +// + template + constexpr std::string_view enum_name_full() { + constexpr auto kName = ftl_enum_full(); + static_assert(kName, "Unknown enumerator"); + return *kName; + } + +// Returns a stringified enumerator, possibly at compile time. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// static_assert(ftl::enum_name(E::C).value_or("?") == "C"); +// static_assert(ftl::enum_name(E{3}).value_or("?") == "?"); +// + template + constexpr std::optional enum_name(E v) { + const auto value = to_underlying(v); + + constexpr auto kBegin = to_underlying(enum_begin_v); + constexpr auto kLast = to_underlying(enum_last_v); + if (value < kBegin || value > kLast) return {}; + + constexpr auto kRange = details::EnumRange{}; + return kRange.values[value - kBegin]; + } + +// Returns a stringified enumerator with class, possibly at compile time. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// static_assert(ftl::enum_name(E::C).value_or("?") == "E::C"); +// static_assert(ftl::enum_name(E{3}).value_or("?") == "?"); +// + template + constexpr std::optional enum_name_full(E v) { + const auto value = to_underlying(v); + + constexpr auto kBegin = to_underlying(enum_begin_v); + constexpr auto kLast = to_underlying(enum_last_v); + if (value < kBegin || value > kLast) return {}; + + constexpr auto kRange = details::EnumRange{}; + return kRange.values[value - kBegin]; + } + +// Returns a stringified flag enumerator, possibly at compile time. +// +// enum class F : std::uint16_t { X = 0b1, Y = 0b10, Z = 0b100 }; +// +// static_assert(ftl::flag_name(F::Z).value_or("?") == "Z"); +// static_assert(ftl::flag_name(F{0b111}).value_or("?") == "?"); +// + template + constexpr std::optional flag_name(E v) { + const auto value = to_underlying(v); + + // TODO: Replace with std::popcount and std::countr_zero in C++20. + if (__builtin_popcountll(value) != 1) return {}; + + constexpr auto kRange = details::EnumRange{}; + return kRange.values[__builtin_ctzll(value)]; + } + +// Returns a stringified enumerator, or its integral value if not named. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// assert(ftl::enum_string(E::C) == "C"); +// assert(ftl::enum_string(E{3}) == "3"); +// + template + inline std::string enum_string(E v) { + if (const auto name = enum_name(v)) { + return std::string(*name); + } + return to_string(to_underlying(v)); + } + +// Returns a stringified enumerator with class, or its integral value if not named. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// assert(ftl::enum_string(E::C) == "E::C"); +// assert(ftl::enum_string(E{3}) == "3"); +// + template + inline std::string enum_string_full(E v) { + if (const auto name = enum_name_full(v)) { + return std::string(*name); + } + return to_string(to_underlying(v)); + } + +// Returns a stringified flag enumerator, or its integral value if not named. +// +// enum class F : std::uint16_t { X = 0b1, Y = 0b10, Z = 0b100 }; +// +// assert(ftl::flag_string(F::Z) == "Z"); +// assert(ftl::flag_string(F{7}) == "0b111"); +// + template + inline std::string flag_string(E v) { + if (const auto name = flag_name(v)) { + return std::string(*name); + } + constexpr auto radix = sizeof(E) == 1 ? Radix::kBin : Radix::kHex; + return to_string(to_underlying(v), radix); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/expected.h b/sysbridge/src/main/cpp/android/ftl/expected.h new file mode 100644 index 0000000000..5ed362e536 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/expected.h @@ -0,0 +1,144 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../libbase/expected.h" +#include +#include + +#include + +// Given an expression `expr` that evaluates to an ftl::Expected result (R for short), FTL_TRY +// unwraps T out of R, or bails out of the enclosing function F if R has an error E. The return type +// of F must be R, since FTL_TRY propagates R in the error case. As a special case, ftl::Unit may be +// used as the error E to allow FTL_TRY expressions when F returns `void`. +// +// The non-standard syntax requires `-Wno-gnu-statement-expression-from-macro-expansion` to compile. +// The UnitToVoid conversion allows the macro to be used for early exit from a function that returns +// `void`. +// +// Example usage: +// +// using StringExp = ftl::Expected; +// +// StringExp repeat(StringExp exp) { +// const std::string str = FTL_TRY(exp); +// return StringExp(str + str); +// } +// +// assert(StringExp("haha"s) == repeat(StringExp("ha"s))); +// assert(repeat(ftl::Unexpected(std::errc::bad_message)).has_error([](std::errc e) { +// return e == std::errc::bad_message; +// })); +// +// +// FTL_TRY may be used in void-returning functions by using ftl::Unit as the error type: +// +// void uppercase(char& c, ftl::Optional opt) { +// c = std::toupper(FTL_TRY(std::move(opt).ok_or(ftl::Unit()))); +// } +// +// char c = '?'; +// uppercase(c, std::nullopt); +// assert(c == '?'); +// +// uppercase(c, 'a'); +// assert(c == 'A'); +// +#define FTL_TRY(expr) \ + ({ \ + auto exp_ = (expr); \ + if (!exp_.has_value()) { \ + using E = decltype(exp_)::error_type; \ + return android::ftl::details::UnitToVoid::from(std::move(exp_)); \ + } \ + exp_.value(); \ + }) + +// Given an expression `expr` that evaluates to an ftl::Expected result (R for short), +// FTL_EXPECT unwraps T out of R, or bails out of the enclosing function F if R has an error E. +// While FTL_TRY bails out with R, FTL_EXPECT bails out with E, which is useful when F does not +// need to propagate R because T is not relevant to the caller. +// +// Example usage: +// +// using StringExp = ftl::Expected; +// +// std::errc repeat(StringExp exp, std::string& out) { +// const std::string str = FTL_EXPECT(exp); +// out = str + str; +// return std::errc::operation_in_progress; +// } +// +// std::string str; +// assert(std::errc::operation_in_progress == repeat(StringExp("ha"s), str)); +// assert("haha"s == str); +// assert(std::errc::bad_message == repeat(ftl::Unexpected(std::errc::bad_message), str)); +// assert("haha"s == str); +// +#define FTL_EXPECT(expr) \ + ({ \ + auto exp_ = (expr); \ + if (!exp_.has_value()) { \ + return std::move(exp_.error()); \ + } \ + exp_.value(); \ + }) + +namespace android::ftl { + +// Superset of base::expected with monadic operations. +// +// TODO: Extend std::expected in C++23. +// + template + struct Expected final : base::expected { + using Base = base::expected; + using Base::expected; + + using Base::error; + using Base::has_value; + using Base::value; + + template + constexpr bool has_error(P predicate) const { + return !has_value() && predicate(error()); + } + + constexpr Optional value_opt() const &{ + return has_value() ? Optional(value()) : std::nullopt; + } + + constexpr Optional value_opt() &&{ + return has_value() ? Optional(std::move(value())) : std::nullopt; + } + + // Delete new for this class. Its base doesn't have a virtual destructor, and + // if it got deleted via base class pointer, it would cause undefined + // behavior. There's not a good reason to allocate this object on the heap + // anyway. + static void *operator new(size_t) = delete; + + static void *operator new[](size_t) = delete; + }; + + template + constexpr auto Unexpected(E &&error) { + return base::unexpected(std::forward(error)); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/fake_guard.h b/sysbridge/src/main/cpp/android/ftl/fake_guard.h new file mode 100644 index 0000000000..056564a1a1 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/fake_guard.h @@ -0,0 +1,87 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#define FTL_ATTRIBUTE(a) __attribute__((a)) + +namespace android::ftl { + +// Granular alternative to [[clang::no_thread_safety_analysis]]. Given a std::mutex-like object, +// FakeGuard suppresses enforcement of thread-safe access to guarded variables within its scope. +// While FakeGuard is scoped to a block, there are macro shorthands for a single expression, as +// well as function/lambda scope (though calls must be indirect, e.g. virtual or std::function): +// +// struct { +// std::mutex mutex; +// int x FTL_ATTRIBUTE(guarded_by(mutex)) = -1; +// +// int f() { +// { +// ftl::FakeGuard guard(mutex); +// x = 0; +// } +// +// return FTL_FAKE_GUARD(mutex, x + 1); +// } +// +// std::function g() const { +// return [this]() FTL_FAKE_GUARD(mutex) { return x; }; +// } +// } s; +// +// assert(s.f() == 1); +// assert(s.g()() == 0); +// +// An example of a situation where FakeGuard helps is a mutex that guards writes on Thread 1, and +// reads on Thread 2. Reads on Thread 1, which is the only writer, need not be under lock, so can +// use FakeGuard to appease the thread safety analyzer. Another example is enforcing and documenting +// exclusive access by a single thread. This is done by defining a global constant that represents a +// thread context, and annotating guarded variables as if it were a mutex (though without any effect +// at run time): +// +// constexpr class [[clang::capability("mutex")]] { +// } kMainThreadContext; +// + template + struct [[clang::scoped_lockable]] FakeGuard final { + explicit FakeGuard(const Mutex &mutex) FTL_ATTRIBUTE(acquire_capability(mutex)) {} + + [[clang::release_capability()]] ~FakeGuard() {} + + FakeGuard(const FakeGuard &) = delete; + + FakeGuard &operator=(const FakeGuard &) = delete; + }; + +} // namespace android::ftl + +// TODO: Enable in C++23 once standard attributes can be used on lambdas. +#if 0 +#define FTL_FAKE_GUARD1(mutex) [[using clang: acquire_capability(mutex), release_capability(mutex)]] +#else +#define FTL_FAKE_GUARD1(mutex) \ + FTL_ATTRIBUTE(acquire_capability(mutex)) \ + FTL_ATTRIBUTE(release_capability(mutex)) +#endif + +#define FTL_FAKE_GUARD2(mutex, expr) \ + (android::ftl::FakeGuard(mutex), expr) + +#define FTL_MAKE_FAKE_GUARD(arg1, arg2, guard, ...) guard + +#define FTL_FAKE_GUARD(...) \ + FTL_MAKE_FAKE_GUARD(__VA_ARGS__, FTL_FAKE_GUARD2, FTL_FAKE_GUARD1, )(__VA_ARGS__) diff --git a/sysbridge/src/main/cpp/android/ftl/finalizer.h b/sysbridge/src/main/cpp/android/ftl/finalizer.h new file mode 100644 index 0000000000..b3342e959c --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/finalizer.h @@ -0,0 +1,213 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +namespace android::ftl { + +// An RAII wrapper that invokes a function object as a finalizer when destroyed. +// +// The function object must take no arguments, and must return void. If the function object needs +// any context for the call, it must store it itself, for example with a lambda capture. +// +// The stored function object will be called once (unless canceled via the `cancel()` member +// function) at the first of: +// +// - The Finalizer instance is destroyed. +// - `operator()` is used to invoke the contained function. +// - The Finalizer instance is move-assigned a new value. The function being replaced will be +// invoked, and the replacement will be stored to be called later. +// +// The intent with this class is to keep cleanup code next to the code that requires that +// cleanup be performed. +// +// bool read_file(std::string filename) { +// FILE* f = fopen(filename.c_str(), "rb"); +// if (f == nullptr) return false; +// const auto cleanup = ftl::Finalizer([f]() { fclose(f); }); +// // fread(...), etc +// return true; +// } +// +// The `FinalFunction` template argument to Finalizer allows a polymorphic function +// type for storing the finalization function, such as `std::function` or `ftl::Function`. +// +// For convenience, this header defines a few useful aliases for using those types. +// +// - `FinalizerStd`, an alias for `Finalizer>` +// - `FinalizerFtl`, an alias for `Finalizer>` +// - `FinalizerFtl1`, an alias for `Finalizer>` +// - `FinalizerFtl2`, an alias for `Finalizer>` +// - `FinalizerFtl3`, an alias for `Finalizer>` +// +// Clients of this header are free to define other aliases they need. +// +// A Finalizer that uses a polymorphic function type can be returned from a function call and/or +// stored as member data (to be destroyed along with the containing class). +// +// auto register(Observer* observer) -> ftl::FinalizerStd { +// const auto id = observers.add(observer); +// return ftl::Finalizer([id]() { observers.remove(id); }); +// } +// +// { +// const auto _ = register(observer); +// // do the things that required the registered observer. +// } +// // the observer is removed. +// +// Cautions: +// +// 1. When a Finalizer is stored as member data, you will almost certainly want that cleanup to +// happen first, before the rest of the other member data is destroyed. For safety you should +// assume that the finalization function will access that data directly or indirectly. +// +// This means that Finalizers should be defined last, after all other normal member data in a +// class. +// +// class MyClass { +// public: +// bool initialize() { +// ready_ = true; +// cleanup_ = ftl::Finalizer([this]() { ready_ = false; }); +// return true; +// } +// +// bool ready_ = false; +// +// // Finalizers should be last so other class members can be accessed before being +// // destroyed. +// ftl::FinalizerStd cleanup_; +// }; +// +// 2. Care must be taken to use `ftl::Finalizer()` when constructing locally from a lambda. If you +// forget to do so, you are just creating a lambda that won't be automatically invoked! +// +// const auto bad = [&counter](){ ++counter; }; // Just a lambda instance +// const auto good = ftl::Finalizer([&counter](){ ++counter; }); +// + template + class Finalizer final { + // requires(std::is_invocable_r_v) + static_assert(std::is_invocable_r_v); + + public: + // A default constructed Finalizer does nothing when destroyed. + // requires(std::is_default_constructible_v) + constexpr Finalizer() = default; + + // Constructs a Finalizer from a function object. + // requires(std::is_invocable_v) + template>> + [[nodiscard]] explicit constexpr Finalizer(F &&function) + : Finalizer(std::forward(function), false) {} + + constexpr ~Finalizer() { maybe_invoke(); } + + // Disallow copying. + Finalizer(const Finalizer &that) = delete; + + auto operator=(const Finalizer &that) = delete; + + // Move construction + // requires(std::is_move_constructible_v) + [[nodiscard]] constexpr Finalizer(Finalizer &&that) + : Finalizer(std::move(that.function_), std::exchange(that.canceled_, true)) {} + + // Implicit conversion move construction + // requires(!std::is_same_v>) + template>>> + // NOLINTNEXTLINE(google-explicit-constructor, cppcoreguidelines-rvalue-reference-param-not-moved) + [[nodiscard]] constexpr Finalizer(Finalizer &&that) + : Finalizer(std::move(that.function_), std::exchange(that.canceled_, true)) {} + + // Move assignment + // requires(std::is_move_assignable_v) + constexpr auto operator=(Finalizer &&that) -> Finalizer & { + maybe_invoke(); + + function_ = std::move(that.function_); + canceled_ = std::exchange(that.canceled_, true); + + return *this; + } + + // Implicit conversion move assignment + // requires(!std::is_same_v>) + template>>> + // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) + constexpr auto operator=(Finalizer &&that) -> Finalizer & { + *this = Finalizer(std::move(that.function_), std::exchange(that.canceled_, true)); + return *this; + } + + // Cancels the final function, preventing it from being invoked. + constexpr void cancel() { + canceled_ = true; + maybe_nullify_function(); + } + + // Invokes the final function now, if not already invoked. + constexpr void operator()() { maybe_invoke(); } + + private: + template + friend + class Finalizer; + + template>> + [[nodiscard]] explicit constexpr Finalizer(F &&function, bool canceled) + : function_(std::forward(function)), canceled_(canceled) {} + + constexpr void maybe_invoke() { + if (!std::exchange(canceled_, true)) { + std::invoke(function_); + maybe_nullify_function(); + } + } + + constexpr void maybe_nullify_function() { + // Sets function_ to nullptr if that is supported for the backing type. + if constexpr (std::is_assignable_v) { + function_ = nullptr; + } + } + + FinalFunction function_; + bool canceled_ = true; + }; + + template + Finalizer(F &&) -> Finalizer>; + +// A standard alias for using `std::function` as the polymorphic function type. + using FinalizerStd = Finalizer>; + +// Helpful aliases for using `ftl::Function` as the polymorphic function type. + using FinalizerFtl = Finalizer>; + using FinalizerFtl1 = Finalizer>; + using FinalizerFtl2 = Finalizer>; + using FinalizerFtl3 = Finalizer>; + +} // namespace android::ftl \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/ftl/flags.h b/sysbridge/src/main/cpp/android/ftl/flags.h new file mode 100644 index 0000000000..338acc240e --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/flags.h @@ -0,0 +1,246 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +// TODO(b/185536303): Align with FTL style. + +namespace android::ftl { + +/* A class for handling flags defined by an enum or enum class in a type-safe way. */ + template + class Flags { + // F must be an enum or its underlying type is undefined. Theoretically we could specialize this + // further to avoid this restriction but in general we want to encourage the use of enums + // anyways. + static_assert(std::is_enum_v, "Flags type must be an enum"); + using U = std::underlying_type_t; + + public: + constexpr Flags(F f) : mFlags(static_cast(f)) {} + + constexpr Flags(std::initializer_list fs) : mFlags(combine(fs)) {} + + constexpr Flags() : mFlags(0) {} + + constexpr Flags(const Flags &f) : mFlags(f.mFlags) {} + + // Provide a non-explicit construct for non-enum classes since they easily convert to their + // underlying types (e.g. when used with bitwise operators). For enum classes, however, we + // should force them to be explicitly constructed from their underlying types to make full use + // of the type checker. + template + constexpr Flags(T t, std::enable_if_t, T> * = nullptr) : mFlags(t) {} + + template + explicit constexpr Flags(T t, std::enable_if_t, T> * = nullptr) + : mFlags(t) {} + + class Iterator { + using Bits = std::uint64_t; + static_assert(sizeof(U) <= sizeof(Bits)); + + public: + constexpr Iterator() = default; + + Iterator(Flags flags) : mRemainingFlags(flags.mFlags) { (*this)++; } + + // Pre-fix ++ + Iterator &operator++() { + if (mRemainingFlags.none()) { + mCurrFlag = 0; + } else { + // TODO: Replace with std::countr_zero in C++20. + const Bits bit = static_cast(__builtin_ctzll( + mRemainingFlags.to_ullong())); + mRemainingFlags.reset(static_cast(bit)); + mCurrFlag = static_cast(static_cast(1) << bit); + } + return *this; + } + + // Post-fix ++ + Iterator operator++(int) { + Iterator iter = *this; + ++*this; + return iter; + } + + bool operator==(Iterator other) const { + return mCurrFlag == other.mCurrFlag && mRemainingFlags == other.mRemainingFlags; + } + + bool operator!=(Iterator other) const { return !(*this == other); } + + F operator*() const { return F{mCurrFlag}; } + + // iterator traits + + // In the future we could make this a bidirectional const iterator instead of a forward + // iterator but it doesn't seem worth the added complexity at this point. This could not, + // however, be made a non-const iterator as assigning one flag to another is a non-sensical + // operation. + using iterator_category = std::input_iterator_tag; + using value_type = F; + // Per the C++ spec, because input iterators are not assignable the iterator's reference + // type does not actually need to be a reference. In fact, making it a reference would imply + // that modifying it would change the underlying Flags object, which is obviously wrong for + // the same reason this can't be a non-const iterator. + using reference = F; + using difference_type = void; + using pointer = void; + + private: + std::bitset mRemainingFlags; + U mCurrFlag = 0; + }; + + /* + * Tests whether the given flag is set. + */ + bool test(F flag) const { + U f = static_cast(flag); + return (f & mFlags) == f; + } + + /* Tests whether any of the given flags are set */ + bool any(Flags f = ~Flags()) const { return (mFlags & f.mFlags) != 0; } + + /* Tests whether all of the given flags are set */ + bool all(Flags f) const { return (mFlags & f.mFlags) == f.mFlags; } + + constexpr Flags operator|(Flags rhs) const { + return static_cast(mFlags | rhs.mFlags); + } + + Flags &operator|=(Flags rhs) { + mFlags = mFlags | rhs.mFlags; + return *this; + } + + Flags operator&(Flags rhs) const { return static_cast(mFlags & rhs.mFlags); } + + Flags &operator&=(Flags rhs) { + mFlags = mFlags & rhs.mFlags; + return *this; + } + + Flags operator^(Flags rhs) const { return static_cast(mFlags ^ rhs.mFlags); } + + Flags &operator^=(Flags rhs) { + mFlags = mFlags ^ rhs.mFlags; + return *this; + } + + Flags operator~() { return static_cast(~mFlags); } + + bool operator==(Flags rhs) const { return mFlags == rhs.mFlags; } + + bool operator!=(Flags rhs) const { return !operator==(rhs); } + + Flags &operator=(const Flags &rhs) { + mFlags = rhs.mFlags; + return *this; + } + + inline Flags &clear(Flags f = static_cast(~static_cast(0))) { + return *this &= ~f; + } + + Iterator begin() const { return Iterator(*this); } + + Iterator end() const { return Iterator(); } + + /* + * Returns the stored set of flags. + * + * Note that this returns the underlying type rather than the base enum class. This is because + * the value is no longer necessarily a strict member of the enum since the returned value could + * be multiple enum variants OR'd together. + */ + U get() const { return mFlags; } + + std::string string() const { + std::string result; + bool first = true; + U unstringified = 0; + for (const F f: *this) { + if (const auto flagName = flag_name(f)) { + appendFlag(result, flagName.value(), first); + } else { + unstringified |= static_cast(f); + } + } + + if (unstringified != 0) { + constexpr auto radix = sizeof(U) == 1 ? Radix::kBin : Radix::kHex; + appendFlag(result, to_string(unstringified, radix), first); + } + + if (first) { + result += "0x0"; + } + + return result; + } + + private: + U mFlags; + + static constexpr U combine(std::initializer_list fs) { + U result = 0; + for (const F f: fs) { + result |= static_cast(f); + } + return result; + } + + static void appendFlag(std::string &str, const std::string_view &flag, bool &first) { + if (first) { + first = false; + } else { + str += " | "; + } + str += flag; + } + }; + +// This namespace provides operator overloads for enum classes to make it easier to work with them +// as flags. In order to use these, add them via a `using namespace` declaration. + namespace flag_operators { + + template>> + inline Flags operator~(F f) { + return static_cast(~to_underlying(f)); + } + + template>> + constexpr Flags operator|(F lhs, F rhs) { + return static_cast(to_underlying(lhs) | to_underlying(rhs)); + } + + } // namespace flag_operators +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/function.h b/sysbridge/src/main/cpp/android/ftl/function.h new file mode 100644 index 0000000000..80caf4e743 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/function.h @@ -0,0 +1,313 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace android::ftl { + +// ftl::Function is a container for function object, and can mostly be used in place of +// std::function. +// +// Unlike std::function, a ftl::Function: +// +// * Uses a static amount of memory (controlled by N), and never any dynamic allocation. +// * Satisfies the std::is_trivially_copyable<> trait. +// * Satisfies the std::is_trivially_destructible<> trait. +// +// However those same limits are also required from the contained function object in turn. +// +// The size of a ftl::Function is guaranteed to be: +// +// sizeof(std::intptr_t) * (N + 2) +// +// A ftl::Function can always be implicitly converted to a larger size ftl::Function. +// Trying to convert the other way leads to a compilation error. +// +// A default-constructed ftl::Function is in an empty state. The operator bool() overload returns +// false in this state. It is undefined behavior to attempt to invoke the function in this state. +// +// The ftl::Function can also be constructed or assigned from ftl::no_op. This sets up the +// ftl::Function to be non-empty, with a function that when called does nothing except +// default-constructs a return value. +// +// The ftl::make_function() helpers construct a ftl::Function, including deducing the +// values of F and N from the arguments it is given. +// +// The static ftl::Function::make() helpers construct a ftl::Function without that +// deduction, and also allow for implicit argument conversion if the target being called needs them. +// +// The construction helpers allow any of the following types of functions to be stored: +// +// * Any SMALL function object (as defined by the C++ Standard), such as a lambda with a small +// capture, or other "functor". The requirements are: +// +// 1) The function object must be trivial to destroy (in fact, the destructor will never +// actually be called once copied to the internal storage). +// 2) The function object must be trivial to copy (the raw bytes will be copied as the +// ftl::Function is copied/moved). +// 3) The size of the function object cannot be larger than sizeof(std::intptr_t) * (N + 1), +// and it cannot require stricter alignment than alignof(std::intptr_t). +// +// With the default of N=0, a lambda can only capture a single pointer-sized argument. This is +// enough to capture `this`, which is why N=0 is the default. +// +// * A member function, with the address passed as the template value argument to the construction +// helper function, along with the instance pointer needed to invoke it passed as an ordinary +// argument. +// +// ftl::make_function<&Class::member_function>(this); +// +// Note that the indicated member function will be invoked non-virtually. If you need it to be +// invoked virtually, you should invoke it yourself with a small lambda like so: +// +// ftl::function([this] { virtual_member_function(); }); +// +// * An ordinary function ("free function"), with the address of the function passed as a template +// value argument. +// +// ftl::make_function<&std::atoi>(); +// +// As with the member function helper, as the function is known at compile time, it will be called +// directly. +// +// Example usage: +// +// class MyClass { +// public: +// void on_event() const {} +// int on_string(int*, std::string_view) { return 1; } +// +// auto get_function() { +// return ftl::function([this] { on_event(); }); +// } +// } cls; +// +// // A function container with no arguments, and returning no value. +// ftl::Function f; +// +// // Construct a ftl::Function containing a small lambda. +// f = cls.get_function(); +// +// // Construct a ftl::Function that calls `cls.on_event()`. +// f = ftl::function<&MyClass::on_event>(&cls); +// +// // Create a do-nothing function. +// f = ftl::no_op; +// +// // Invoke the contained function. +// f(); +// +// // Also invokes it. +// std::invoke(f); +// +// // Create a typedef to give a more meaningful name and bound the size. +// using MyFunction = ftl::Function; +// int* ptr = nullptr; +// auto f1 = MyFunction::make( +// [cls = &cls, ptr](std::string_view sv) { +// return cls->on_string(ptr, sv); +// }); +// int r = f1("abc"sv); +// +// // Returns a default-constructed int (0). +// f1 = ftl::no_op; +// r = f1("abc"sv); +// assert(r == 0); + + template + class Function; + +// Used to construct a Function that does nothing. + struct NoOpTag { + }; + + constexpr NoOpTag no_op; + +// Detects that a type is a `ftl::Function` regardless of what `F` and `N` are. + template + struct is_function : public std::false_type { + }; + + template + struct is_function> : public std::true_type { + }; + + template + constexpr bool is_function_v = is_function::value; + + template + class Function final { + // Enforce a valid size, with an arbitrary maximum allowed size for the container of + // sizeof(std::intptr_t) * 16, though that maximum can be relaxed. + static_assert(N <= details::kFunctionMaximumN); + + using OpaqueStorageTraits = details::function_opaque_storage; + + public: + // Defining result_type allows ftl::Function to be substituted for std::function. + using result_type = Ret; + + // Constructs an empty ftl::Function. + Function() = default; + + // Constructing or assigning from nullptr_t also creates an empty ftl::Function. + Function(std::nullptr_t) {} + + Function &operator=(std::nullptr_t) { return *this = Function(nullptr); } + + // Constructing from NoOpTag sets up a a special no-op function which is valid to call, and which + // returns a default constructed return value. + Function(NoOpTag) : function_(details::bind_opaque_no_op()) {} + + Function &operator=(NoOpTag) { return *this = Function(no_op); } + + // Constructing/assigning from a function object stores a copy of that function object, however: + // * It must be trivially copyable, as the implementation makes a copy with memcpy(). + // * It must be trivially destructible, as the implementation doesn't destroy the copy! + // * It must fit in the limited internal storage, which enforces size/alignment restrictions. + + template>> + Function(const F &f) + : opaque_(OpaqueStorageTraits::opaque_copy(f)), + function_(details::bind_opaque_function_object(f)) {} + + template>> + Function &operator=(const F &f) noexcept { + return *this = Function{OpaqueStorageTraits::opaque_copy(f), + details::bind_opaque_function_object(f)}; + } + + // Constructing/assigning from a smaller ftl::Function is allowed, but not anything else. + + template + Function(const Function &other) + : opaque_{OpaqueStorageTraits::opaque_copy(other.opaque_)}, + function_(other.function_) {} + + template + auto &operator=(const Function &other) { + return *this = Function{OpaqueStorageTraits::opaque_copy(other.opaque_), + other.function_}; + } + + // Returns true if a function is set. + explicit operator bool() const { return function_ != nullptr; } + + // Checks if the other function has the same contents as this one. + bool operator==(const Function &other) const { + return other.opaque_ == opaque_ && other.function_ == function_; + } + + bool operator!=(const Function &other) const { return !operator==(other); } + + // Alternative way of testing for a function being set. + bool operator==(std::nullptr_t) const { return function_ == nullptr; } + + bool operator!=(std::nullptr_t) const { return function_ != nullptr; } + + // Invokes the function. + Ret operator()(Args... args) const { + return std::invoke(function_, opaque_.data(), std::forward(args)...); + } + + // Creation helper for function objects, such as lambdas. + template + static auto make(const F &f) -> decltype(Function{f}) { + return Function{f}; + } + + // Creation helper for a class pointer and a compile-time chosen member function to call. + template + static auto make(Class *instance) -> decltype(Function{ + details::bind_member_function(instance, + static_cast(nullptr))}) { + return Function{details::bind_member_function( + instance, static_cast(nullptr))}; + } + + // Creation helper for a compile-time chosen free function to call. + template + static auto make() -> decltype(Function{ + details::bind_free_function( + static_cast(nullptr))}) { + return Function{ + details::bind_free_function( + static_cast(nullptr))}; + } + + private: + // Needed so a Function can be converted to a Function. + template + friend + class Function; + + // The function pointer type of function stored in `function_`. The first argument is always + // `&opaque_`. + using StoredFunction = Ret(void *, Args...); + + // The type of the opaque storage, used to hold an appropriate function object. + // The type stored here is ONLY known to the StoredFunction. + // We always use at least one std::intptr_t worth of storage, and always a multiple of that size. + using OpaqueStorage = typename OpaqueStorageTraits::type; + + // Internal constructor for creating from a raw opaque blob + function pointer. + Function(const OpaqueStorage &opaque, StoredFunction *function) + : opaque_(opaque), function_(function) {} + + // Note: `mutable` so that `operator() const` can use it. + mutable OpaqueStorage opaque_{}; + StoredFunction *function_{nullptr}; + }; + +// Makes a ftl::Function given a function object `F`. + template> + Function(const F &) -> Function; + + template + auto make_function(const F &f) -> decltype(Function{f}) { + return Function{f}; + } + +// Makes a ftl::Function given a `MemberFunction` and a instance pointer to the associated `Class`. + template + auto make_function(Class *instance) + -> decltype(Function{details::bind_member_function( + instance, + static_cast *>(nullptr))}) { + return Function{details::bind_member_function( + instance, + static_cast *>(nullptr))}; + } + +// Makes a ftl::Function given an ordinary free function. + template + auto make_function() -> decltype(Function{ + details::bind_free_function( + static_cast(nullptr))}) { + return Function{ + details::bind_free_function( + static_cast(nullptr))}; + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/future.h b/sysbridge/src/main/cpp/android/ftl/future.h new file mode 100644 index 0000000000..4110674618 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/future.h @@ -0,0 +1,135 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace android::ftl { + +// Thin wrapper around FutureImpl (concretely std::future or std::shared_future) with +// extensions for pure values (created via ftl::yield) and continuations. +// +// See also SharedFuture shorthand below. +// + template class FutureImpl = std::future> + class Future final : public details::BaseFuture, T, FutureImpl> { + using Base = details::BaseFuture; + + friend Base; // For BaseFuture<...>::self. + friend details::BaseFuture, T, std::future>; // For BaseFuture<...>::share. + + public: + // Constructs an invalid future. + Future() : future_(std::in_place_type>) {} + + // Constructs a future from its standard counterpart, implicitly. + Future(FutureImpl &&f) : future_(std::move(f)) {} + + bool valid() const { + return std::holds_alternative(future_) || std::get>(future_).valid(); + } + + // Forwarding functions. Base::share is only defined when FutureImpl is std::future, whereas the + // following are defined for either FutureImpl: + using Base::get; + using Base::wait_for; + + // Attaches a continuation to the future. The continuation is a function that maps T to either R + // or ftl::Future. In the former case, the chain wraps the result in a future as if by + // ftl::yield. + // + // auto future = ftl::yield(123); + // ftl::Future futures[] = {ftl::yield('a'), ftl::yield('b')}; + // + // auto chain = + // ftl::Future(std::move(future)) + // .then([](int x) { return static_cast(x % 2); }) + // .then([&futures](std::size_t i) { return std::move(futures[i]); }); + // + // assert(chain.get() == 'b'); + // + template> + auto then(F &&op) && -> Future> { + return defer( + [](auto &&f, F &&op) { + R r = op(f.get()); + if constexpr (std::is_same_v>) { + return r; + } else { + return r.get(); + } + }, + std::move(*this), std::forward(op)); + } + + private: + template + friend Future yield(V &&); + + template + friend Future yield(Args &&...); + + template + Future(details::ValueTag, Args &&... args) + : future_(std::in_place_type, std::forward(args)...) {} + + std::variant> future_; + }; + + template + using SharedFuture = Future; + +// Deduction guide for implicit conversion. + template class FutureImpl> + Future(FutureImpl &&) -> Future; + +// Creates a future that wraps a value. +// +// auto future = ftl::yield(42); +// assert(future.get() == 42); +// +// auto ptr = std::make_unique('!'); +// auto future = ftl::yield(std::move(ptr)); +// assert(*future.get() == '!'); +// + template + inline Future yield(V &&value) { + return {details::ValueTag{}, std::move(value)}; + } + + template + inline Future yield(Args &&... args) { + return {details::ValueTag{}, std::forward(args)...}; + } + +// Creates a future that defers a function call until its result is queried. +// +// auto future = ftl::defer([](int x) { return x + 1; }, 99); +// assert(future.get() == 100); +// + template + inline auto defer(F &&f, Args &&... args) { + return Future( + std::async(std::launch::deferred, std::forward(f), std::forward(args)...)); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/hash.h b/sysbridge/src/main/cpp/android/ftl/hash.h new file mode 100644 index 0000000000..6d13672e7e --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/hash.h @@ -0,0 +1,44 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace android::ftl { + +// Non-cryptographic hash function (namely CityHash64) for strings with at most 64 characters. +// Unlike std::hash, which returns std::size_t and is only required to produce the same result +// for the same input within a single execution of a program, this hash is stable. + inline std::optional stable_hash(std::string_view view) { + const auto length = view.length(); + if (length <= 16) { + return details::hash_length_0_to_16(view.data(), length); + } + if (length <= 32) { + return details::hash_length_17_to_32(view.data(), length); + } + if (length <= 64) { + return details::hash_length_33_to_64(view.data(), length); + } + return {}; + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/ignore.h b/sysbridge/src/main/cpp/android/ftl/ignore.h new file mode 100644 index 0000000000..bd33511747 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/ignore.h @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace android::ftl { + +// An alternative to `std::ignore` that makes it easy to ignore multiple values. +// +// Examples: +// +// void ftl_ignore_multiple(int arg1, const char* arg2, std::string arg3) { +// // When invoked, all the arguments are ignored. +// ftl::ignore(arg1, arg2, arg3); +// } +// +// void ftl_ignore_single(int arg) { +// // It can be used like std::ignore to ignore a single value +// ftl::ignore = arg; +// } +// + inline constexpr struct { + // NOLINTNEXTLINE(misc-unconventional-assign-operator, readability-named-parameter) + constexpr auto operator=(auto &&) const -> decltype(*this) { return *this; } + + // NOLINTNEXTLINE(readability-named-parameter) + constexpr void operator()(auto &&...) const {} + } ignore; + +} // namespace android::ftl \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/ftl/initializer_list.h b/sysbridge/src/main/cpp/android/ftl/initializer_list.h new file mode 100644 index 0000000000..e6d15c3061 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/initializer_list.h @@ -0,0 +1,112 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace android::ftl { + +// Compile-time counterpart of std::initializer_list that stores per-element constructor +// arguments with heterogeneous types. For a container with elements of type T, given Sizes +// (S0, S1, ..., SN), N elements are initialized: the first element is initialized with the +// first S0 arguments, the second element is initialized with the next S1 arguments, and so +// on. The list of Types (T0, ..., TM) is flattened, so M is equal to the sum of the Sizes. +// +// An InitializerList is created using ftl::init::list, and is consumed by constructors of +// containers. The function call operator is overloaded such that arguments are accumulated +// in a tuple with each successive call. For instance, the following calls initialize three +// strings using different constructors, i.e. string literal, default, and count/character: +// +// ... = ftl::init::list("abc")()(3u, '?'); +// +// The following syntax is a shorthand for key-value pairs, where the first argument is the +// key, and the rest construct the value. The types of the key and value are deduced if the +// first pair contains exactly two arguments: +// +// ... = ftl::init::map(-1, "abc")(-2)(-3, 3u, '?'); +// +// ... = ftl::init::map(0, 'a')(1, 'b')(2, 'c'); +// +// WARNING: The InitializerList returned by an ftl::init::list expression must be consumed +// immediately, since temporary arguments are destroyed after the full expression. Storing +// an InitializerList results in dangling references. +// + template, typename... Types> + struct InitializerList; + + template + struct InitializerList, Types...> { + // Creates a superset InitializerList by appending the number of arguments to Sizes, and + // expanding Types with forwarding references for each argument. + template + [[nodiscard]] constexpr auto operator()(Args &&... args) && -> InitializerList< + T, std::index_sequence, Types..., Args && ...> { + return {std::tuple_cat(std::move(tuple), + std::forward_as_tuple(std::forward(args)...))}; + } + + // The temporary InitializerList returned by operator() is bound to an rvalue reference in + // container constructors, which extends the lifetime of any temporary arguments that this + // tuple refers to until the completion of the full expression containing the construction. + std::tuple tuple; + }; + + template> + struct KeyValue { + }; + +// Shorthand for key-value pairs that assigns the first argument to the key, and the rest to the +// value. The specialization is on KeyValue rather than std::pair, so that ftl::init::list works +// with the latter. + template + struct InitializerList, std::index_sequence, Types...> { + // Accumulate the three arguments to std::pair's piecewise constructor. + template + [[nodiscard]] constexpr auto operator()(K &&k, Args &&... args) && -> InitializerList< + KeyValue, std::index_sequence, Types..., std::piecewise_construct_t, + std::tuple, std::tuple> { + return {std::tuple_cat( + std::move(tuple), + std::forward_as_tuple(std::piecewise_construct, + std::forward_as_tuple(std::forward(k)), + std::forward_as_tuple(std::forward(args)...)))}; + } + + std::tuple tuple; + }; + + namespace init { + + template + [[nodiscard]] constexpr auto list(Args &&... args) { + return InitializerList{}(std::forward(args)...); + } + + template, typename... Args> + [[nodiscard]] constexpr auto map(Args &&... args) { + return list>(std::forward(args)...); + } + + template + [[nodiscard]] constexpr auto map(K &&k, V &&v) { + return list>(std::forward(k), std::forward(v)); + } + + } // namespace init +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/match.h b/sysbridge/src/main/cpp/android/ftl/match.h new file mode 100644 index 0000000000..490a4e72eb --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/match.h @@ -0,0 +1,63 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace android::ftl { + +// Concise alternative to std::visit that compiles to branches rather than a dispatch table. For +// std::variant where N is small, this is slightly faster since the branches can be +// inlined unlike the function pointers. +// +// using namespace std::chrono; +// std::variant duration = 119min; +// +// // Mutable match. +// ftl::match(duration, [](auto& d) { ++d; }); +// +// // Immutable match. Exhaustive due to minutes being convertible to seconds. +// assert("2 hours"s == +// ftl::match(duration, +// [](const seconds& s) { +// const auto h = duration_cast(s); +// return std::to_string(h.count()) + " hours"s; +// }, +// [](const hours& h) { return std::to_string(h.count() / 24) + " days"s; })); +// + template + decltype(auto) match(std::variant &variant, Ms &&... matchers) { + const auto matcher = details::Matcher{std::forward(matchers)...}; + static_assert(details::is_exhaustive_match_v, + "Non-exhaustive match"); + + return details::Match::match(variant, matcher); + } + + template + decltype(auto) match(const std::variant &variant, Ms &&... matchers) { + const auto matcher = details::Matcher{std::forward(matchers)...}; + static_assert(details::is_exhaustive_match_v, + "Non-exhaustive match"); + + return details::Match::match(variant, matcher); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/mixins.h b/sysbridge/src/main/cpp/android/ftl/mixins.h new file mode 100644 index 0000000000..0cfae3ce27 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/mixins.h @@ -0,0 +1,152 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android::ftl { + +// CRTP mixins for defining type-safe wrappers that are distinct from their underlying type. Common +// uses are IDs, opaque handles, and physical quantities. The constructor is provided by (and must +// be inherited from) the `Constructible` mixin, whereas operators (equality, ordering, arithmetic, +// etc.) are enabled through inheritance: +// +// struct Id : ftl::Constructible, ftl::Equatable { +// using Constructible::Constructible; +// }; +// +// static_assert(!std::is_default_constructible_v); +// +// Unlike `Constructible`, `DefaultConstructible` allows default construction. The default value is +// zero-initialized unless specified: +// +// struct Color : ftl::DefaultConstructible, +// ftl::Equatable, +// ftl::Orderable { +// using DefaultConstructible::DefaultConstructible; +// }; +// +// static_assert(Color() == Color(0u)); +// static_assert(ftl::to_underlying(Color(-1)) == 255u); +// static_assert(Color(1u) < Color(2u)); +// +// struct Sequence : ftl::DefaultConstructible, +// ftl::Equatable, +// ftl::Orderable, +// ftl::Incrementable { +// using DefaultConstructible::DefaultConstructible; +// }; +// +// static_assert(Sequence() == Sequence(-1)); +// +// The underlying type need not be a fundamental type: +// +// struct Timeout : ftl::DefaultConstructible, +// ftl::Equatable, +// ftl::Addable { +// using DefaultConstructible::DefaultConstructible; +// }; +// +// using namespace std::chrono_literals; +// static_assert(Timeout() + Timeout(5s) == Timeout(15s)); +// + template + struct Constructible { + explicit constexpr Constructible(T value) : value_(value) {} + + explicit constexpr operator const T &() const { return value_; } + + private: + template class> + friend + class details::Mixin; + + T value_; + }; + + template + struct DefaultConstructible : Constructible { + using Constructible::Constructible; + + constexpr DefaultConstructible() : DefaultConstructible(T{kDefault}) {} + }; + +// Shorthand for casting a type-safe wrapper to its underlying value. + template + constexpr const T &to_underlying(const Constructible &c) { + return static_cast(c); + } + +// Comparison operators for equality. + template + struct Equatable : details::Mixin { + constexpr bool operator==(const Self &other) const { + return to_underlying(this->self()) == to_underlying(other); + } + + constexpr bool operator!=(const Self &other) const { return !(*this == other); } + }; + +// Comparison operators for ordering. + template + struct Orderable : details::Mixin { + constexpr bool operator<(const Self &other) const { + return to_underlying(this->self()) < to_underlying(other); + } + + constexpr bool operator>(const Self &other) const { return other < this->self(); } + + constexpr bool operator>=(const Self &other) const { return !(*this < other); } + + constexpr bool operator<=(const Self &other) const { return !(*this > other); } + }; + +// Pre-increment and post-increment operators. + template + struct Incrementable : details::Mixin { + constexpr Self &operator++() { + ++this->mut(); + return this->self(); + } + + constexpr Self operator++(int) { + const Self tmp = this->self(); + operator++(); + return tmp; + } + }; + +// Additive operators, including incrementing. + template + struct Addable : details::Mixin, Incrementable { + constexpr Self &operator+=(const Self &other) { + this->mut() += to_underlying(other); + return this->self(); + } + + constexpr Self operator+(const Self &other) const { + Self tmp = this->self(); + return tmp += other; + } + + private: + using Base = details::Mixin; + using Base::mut; + using Base::self; + }; + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/non_null.h b/sysbridge/src/main/cpp/android/ftl/non_null.h new file mode 100644 index 0000000000..df3e49dfd9 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/non_null.h @@ -0,0 +1,228 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace android::ftl { + +// Enforces and documents non-null pre/post-condition for (raw or smart) pointers. +// +// void get_length(const ftl::NonNull>& string_ptr, +// ftl::NonNull length_ptr) { +// // No need for `nullptr` checks. +// *length_ptr = string_ptr->length(); +// } +// +// const auto string_ptr = ftl::as_non_null(std::make_shared("android")); +// std::size_t size; +// get_length(string_ptr, ftl::as_non_null(&size)); +// assert(size == 7u); +// +// For compatibility with std::unique_ptr and performance with std::shared_ptr, move +// operations are allowed despite breaking the invariant: +// +// using Pair = std::pair>, std::shared_ptr>; +// +// Pair dupe_if(ftl::NonNull> non_null_ptr, bool condition) { +// // Move the underlying pointer out, so `non_null_ptr` must not be accessed after this point. +// auto unique_ptr = std::move(non_null_ptr).take(); +// +// auto non_null_shared_ptr = ftl::as_non_null(std::shared_ptr(std::move(unique_ptr))); +// auto nullable_shared_ptr = condition ? non_null_shared_ptr.get() : nullptr; +// +// return {std::move(non_null_shared_ptr), std::move(nullable_shared_ptr)}; +// } +// +// auto ptr = ftl::as_non_null(std::make_unique(42)); +// const auto [ptr1, ptr2] = dupe_if(std::move(ptr), true); +// assert(ptr1.get() == ptr2); +// + template + class NonNull final { + struct Passkey { + }; + + public: + // Disallow `nullptr` explicitly for clear compilation errors. + NonNull() = delete; + + NonNull(std::nullptr_t) = delete; + + // Copy operations. + + constexpr NonNull(const NonNull &) = default; + + constexpr NonNull &operator=(const NonNull &) = default; + + template>> + constexpr NonNull(const NonNull &other) : pointer_(other.get()) {} + + template>> + constexpr NonNull &operator=(const NonNull &other) { + pointer_ = other.get(); + return *this; + } + + [[nodiscard]] constexpr const Pointer &get() const { return pointer_; } + + [[nodiscard]] constexpr explicit operator const Pointer &() const { return get(); } + + // Move operations. These break the invariant, so care must be taken to avoid subsequent access. + + constexpr NonNull(NonNull &&) = default; + + constexpr NonNull &operator=(NonNull &&) = default; + + [[nodiscard]] constexpr Pointer take() &&{ return std::move(pointer_); } + + [[nodiscard]] constexpr explicit operator Pointer() && { return take(); } + + // Dereferencing. + [[nodiscard]] constexpr decltype(auto) operator*() const { return *get(); } + + [[nodiscard]] constexpr decltype(auto) operator->() const { return get(); } + + [[nodiscard]] constexpr explicit operator bool() const { return !(pointer_ == nullptr); } + + // Private constructor for ftl::as_non_null. Excluded from candidate constructors for conversions + // through the passkey idiom, for clear compilation errors. + template + constexpr NonNull(Passkey, P &&pointer) : pointer_(std::forward

(pointer)) { + if (pointer_ == nullptr) std::abort(); + } + + private: + template + friend constexpr auto as_non_null(P &&) -> NonNull>; + + Pointer pointer_; + }; + + template + [[nodiscard]] constexpr auto as_non_null(P &&pointer) -> NonNull> { + using Passkey = typename NonNull>::Passkey; + return {Passkey{}, std::forward

(pointer)}; + } + +// NonNull

<=> NonNull + + template + constexpr bool operator==(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() == rhs.get(); + } + + template + constexpr bool operator!=(const NonNull

&lhs, const NonNull &rhs) { + return !operator==(lhs, rhs); + } + + template + constexpr bool operator<(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() < rhs.get(); + } + + template + constexpr bool operator<=(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() <= rhs.get(); + } + + template + constexpr bool operator>=(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() >= rhs.get(); + } + + template + constexpr bool operator>(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() > rhs.get(); + } + +// NonNull

<=> Q + + template + constexpr bool operator==(const NonNull

&lhs, const Q &rhs) { + return lhs.get() == rhs; + } + + template + constexpr bool operator!=(const NonNull

&lhs, const Q &rhs) { + return lhs.get() != rhs; + } + + template + constexpr bool operator<(const NonNull

&lhs, const Q &rhs) { + return lhs.get() < rhs; + } + + template + constexpr bool operator<=(const NonNull

&lhs, const Q &rhs) { + return lhs.get() <= rhs; + } + + template + constexpr bool operator>=(const NonNull

&lhs, const Q &rhs) { + return lhs.get() >= rhs; + } + + template + constexpr bool operator>(const NonNull

&lhs, const Q &rhs) { + return lhs.get() > rhs; + } + +// P <=> NonNull + + template + constexpr bool operator==(const P &lhs, const NonNull &rhs) { + return lhs == rhs.get(); + } + + template + constexpr bool operator!=(const P &lhs, const NonNull &rhs) { + return lhs != rhs.get(); + } + + template + constexpr bool operator<(const P &lhs, const NonNull &rhs) { + return lhs < rhs.get(); + } + + template + constexpr bool operator<=(const P &lhs, const NonNull &rhs) { + return lhs <= rhs.get(); + } + + template + constexpr bool operator>=(const P &lhs, const NonNull &rhs) { + return lhs >= rhs.get(); + } + + template + constexpr bool operator>(const P &lhs, const NonNull &rhs) { + return lhs > rhs.get(); + } + +} // namespace android::ftl + +// Specialize std::hash for ftl::NonNull +template +struct std::hash> { + std::size_t operator()(const android::ftl::NonNull

&ptr) const { + return std::hash

()(ptr.get()); + } +}; diff --git a/sysbridge/src/main/cpp/android/ftl/optional.h b/sysbridge/src/main/cpp/android/ftl/optional.h new file mode 100644 index 0000000000..c7b5d26a67 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/optional.h @@ -0,0 +1,147 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "../libbase/expected.h" + +#include + +namespace android::ftl { + +// Superset of std::optional with monadic operations, as proposed in https://wg21.link/P0798R8. +// +// TODO: Remove standard APIs in C++23. +// + template + struct Optional final : std::optional { + using std::optional::optional; + + // Implicit downcast. + Optional(std::optional other) : std::optional(std::move(other)) {} + + using std::optional::has_value; + using std::optional::value; + + // Returns Optional where F is a function that maps T to U. + template + constexpr auto transform(F &&f) const &{ + using R = details::transform_result_t; + if (has_value()) return R(std::invoke(std::forward(f), value())); + return R(); + } + + template + constexpr auto transform(F &&f) &{ + using R = details::transform_result_t; + if (has_value()) return R(std::invoke(std::forward(f), value())); + return R(); + } + + template + constexpr auto transform(F &&f) const &&{ + using R = details::transform_result_t; + if (has_value()) return R(std::invoke(std::forward(f), std::move(value()))); + return R(); + } + + template + constexpr auto transform(F &&f) &&{ + using R = details::transform_result_t; + if (has_value()) return R(std::invoke(std::forward(f), std::move(value()))); + return R(); + } + + // Returns Optional where F is a function that maps T to Optional. + template + constexpr auto and_then(F &&f) const &{ + using R = details::and_then_result_t; + if (has_value()) return std::invoke(std::forward(f), value()); + return R(); + } + + template + constexpr auto and_then(F &&f) &{ + using R = details::and_then_result_t; + if (has_value()) return std::invoke(std::forward(f), value()); + return R(); + } + + template + constexpr auto and_then(F &&f) const &&{ + using R = details::and_then_result_t; + if (has_value()) return std::invoke(std::forward(f), std::move(value())); + return R(); + } + + template + constexpr auto and_then(F &&f) &&{ + using R = details::and_then_result_t; + if (has_value()) return std::invoke(std::forward(f), std::move(value())); + return R(); + } + + // Returns this Optional if not nullopt, or else the Optional returned by the function F. + template + constexpr auto or_else(F &&f) const & -> details::or_else_result_t { + if (has_value()) return *this; + return std::forward(f)(); + } + + template + constexpr auto or_else(F &&f) && -> details::or_else_result_t { + if (has_value()) return std::move(*this); + return std::forward(f)(); + } + + // Maps this Optional to expected where nullopt becomes E. + template + constexpr auto ok_or(E &&e) && -> base::expected { + if (has_value()) return std::move(value()); + return base::unexpected(std::forward(e)); + } + + // Delete new for this class. Its base doesn't have a virtual destructor, and + // if it got deleted via base class pointer, it would cause undefined + // behavior. There's not a good reason to allocate this object on the heap + // anyway. + static void *operator new(size_t) = delete; + + static void *operator new[](size_t) = delete; + }; + + template + constexpr bool operator==(const Optional &lhs, const Optional &rhs) { + return static_cast>(lhs) == static_cast>(rhs); + } + + template + constexpr bool operator!=(const Optional &lhs, const Optional &rhs) { + return !(lhs == rhs); + } + +// Deduction guides. + template + Optional(T) -> Optional; + + template + Optional(std::optional) -> Optional; + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/shared_mutex.h b/sysbridge/src/main/cpp/android/ftl/shared_mutex.h new file mode 100644 index 0000000000..3008313038 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/shared_mutex.h @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android::ftl { + +// Wrapper around std::shared_mutex to provide capabilities for thread-safety +// annotations. +// TODO(b/257958323): This class is no longer needed once b/135688034 is fixed (currently blocked on +// b/175635923). + class [[clang::capability("shared_mutex")]] SharedMutex final { + public: + [[clang::acquire_capability()]] void lock() { + mutex_.lock(); + } + + [[clang::release_capability()]] void unlock() { + mutex_.unlock(); + } + + [[clang::acquire_shared_capability()]] void lock_shared() { + mutex_.lock_shared(); + } + + [[clang::release_shared_capability()]] void unlock_shared() { + mutex_.unlock_shared(); + } + + private: + std::shared_mutex mutex_; + }; + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/small_map.h b/sysbridge/src/main/cpp/android/ftl/small_map.h new file mode 100644 index 0000000000..f564909cef --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/small_map.h @@ -0,0 +1,309 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace android::ftl { + +// Associative container with unique, unordered keys. Unlike std::unordered_map, key-value pairs are +// stored in contiguous storage for cache efficiency. The map is allocated statically until its size +// exceeds N, at which point mappings are relocated to dynamic memory. The try_emplace operation has +// a non-standard analogue try_replace that destructively emplaces. The API also defines an in-place +// counterpart to insert_or_assign: emplace_or_replace. Lookup is done not via a subscript operator, +// but immutable getters that can optionally transform the value. +// +// SmallMap unconditionally allocates on the heap. +// +// Example usage: +// +// ftl::SmallMap map; +// assert(map.empty()); +// assert(!map.dynamic()); +// +// map = ftl::init::map(123, "abc")(-1)(42, 3u, '?'); +// assert(map.size() == 3u); +// assert(!map.dynamic()); +// +// assert(map.contains(123)); +// assert(map.get(42).transform([](const std::string& s) { return s.size(); }) == 3u); +// +// const auto opt = map.get(-1); +// assert(opt); +// +// std::string& ref = *opt; +// assert(ref.empty()); +// ref = "xyz"; +// +// map.emplace_or_replace(0, "vanilla", 2u, 3u); +// assert(map.dynamic()); +// +// assert(map == SmallMap(ftl::init::map(-1, "xyz"sv)(0, "nil"sv)(42, "???"sv)(123, "abc"sv))); +// + template> + class SmallMap final { + using Map = SmallVector, N>; + + template + friend + class SmallMap; + + public: + using key_type = K; + using mapped_type = V; + + using value_type = typename Map::value_type; + using size_type = typename Map::size_type; + using difference_type = typename Map::difference_type; + + using reference = typename Map::reference; + using iterator = typename Map::iterator; + + using const_reference = typename Map::const_reference; + using const_iterator = typename Map::const_iterator; + + // Creates an empty map. + SmallMap() = default; + + // Constructs at most N key-value pairs in place by forwarding per-pair constructor arguments. + // The template arguments K, V, and N are inferred using the deduction guide defined below. + // The syntax for listing pairs is as follows: + // + // ftl::SmallMap map = ftl::init::map(123, "abc")(-1)(42, 3u, '?'); + // static_assert(std::is_same_v>); + // + // The types of the key and value are deduced if the first pair contains exactly two arguments: + // + // ftl::SmallMap map = ftl::init::map(0, 'a')(1, 'b')(2, 'c'); + // static_assert(std::is_same_v>); + // + template + SmallMap(InitializerList, Types...> &&list) + : map_(std::move(list)) { + deduplicate(); + } + + // Copies or moves key-value pairs from a convertible map. + template + SmallMap(SmallMap other) : map_(std::move(other.map_)) {} + + static constexpr size_type static_capacity() { return N; } + + size_type max_size() const { return map_.max_size(); } + + size_type size() const { return map_.size(); } + + bool empty() const { return map_.empty(); } + + // Returns whether the map is backed by static or dynamic storage. + bool dynamic() const { + if constexpr (static_capacity() > 0) { + return map_.dynamic(); + } else { + return true; + } + } + + iterator begin() { return map_.begin(); } + + const_iterator begin() const { return cbegin(); } + + const_iterator cbegin() const { return map_.cbegin(); } + + iterator end() { return map_.end(); } + + const_iterator end() const { return cend(); } + + const_iterator cend() const { return map_.cend(); } + + // Returns whether a mapping exists for the given key. + bool contains(const key_type &key) const { return get(key).has_value(); } + + // Returns a reference to the value for the given key, or std::nullopt if the key was not found. + // + // ftl::SmallMap map = ftl::init::map('a', 'A')('b', 'B')('c', 'C'); + // + // const auto opt = map.get('c'); + // assert(opt == 'C'); + // + // char d = 'd'; + // const auto ref = map.get('d').value_or(std::ref(d)); + // ref.get() = 'D'; + // assert(d == 'D'); + // + auto get(const key_type &key) const -> Optional> { + for (const auto &[k, v]: *this) { + if (KeyEqual{}(k, key)) { + return std::cref(v); + } + } + return {}; + } + + auto get(const key_type &key) -> Optional> { + for (auto &[k, v]: *this) { + if (KeyEqual{}(k, key)) { + return std::ref(v); + } + } + return {}; + } + + // Returns an iterator to an existing mapping for the given key, or the end() iterator otherwise. + const_iterator find(const key_type &key) const { + return const_cast(*this).find(key); + } + + iterator find(const key_type &key) { return find(key, begin()); } + + // Inserts a mapping unless it exists. Returns an iterator to the inserted or existing mapping, + // and whether the mapping was inserted. + // + // On emplace, if the map reaches its static or dynamic capacity, then all iterators are + // invalidated. Otherwise, only the end() iterator is invalidated. + // + template + std::pair try_emplace(const key_type &key, Args &&... args) { + if (const auto it = find(key); it != end()) { + return {it, false}; + } + + decltype(auto) ref_or_it = + map_.emplace_back(std::piecewise_construct, std::forward_as_tuple(key), + std::forward_as_tuple(std::forward(args)...)); + + if constexpr (static_capacity() > 0) { + return {&ref_or_it, true}; + } else { + return {ref_or_it, true}; + } + } + + // Replaces a mapping if it exists, and returns an iterator to it. Returns the end() iterator + // otherwise. + // + // The value is replaced via move constructor, so type V does not need to define copy/move + // assignment, e.g. its data members may be const. + // + // The arguments may directly or indirectly refer to the mapping being replaced. + // + // Iterators to the replaced mapping point to its replacement, and others remain valid. + // + template + iterator try_replace(const key_type &key, Args &&... args) { + const auto it = find(key); + if (it == end()) return it; + map_.replace(it, std::piecewise_construct, std::forward_as_tuple(key), + std::forward_as_tuple(std::forward(args)...)); + return it; + } + + // In-place counterpart of std::unordered_map's insert_or_assign. Returns true on emplace, or + // false on replace. + // + // The value is emplaced and replaced via move constructor, so type V does not need to define + // copy/move assignment, e.g. its data members may be const. + // + // On emplace, if the map reaches its static or dynamic capacity, then all iterators are + // invalidated. Otherwise, only the end() iterator is invalidated. On replace, iterators + // to the replaced mapping point to its replacement, and others remain valid. + // + template + std::pair emplace_or_replace(const key_type &key, Args &&... args) { + const auto [it, ok] = try_emplace(key, std::forward(args)...); + if (ok) return {it, ok}; + map_.replace(it, std::piecewise_construct, std::forward_as_tuple(key), + std::forward_as_tuple(std::forward(args)...)); + return {it, ok}; + } + + // Removes a mapping if it exists, and returns whether it did. + // + // The last() and end() iterators, as well as those to the erased mapping, are invalidated. + // + bool erase(const key_type &key) { return erase(key, begin()); } + + // Removes a mapping. + // + // The last() and end() iterators, as well as those to the erased mapping, are invalidated. + // + void erase(iterator it) { map_.unstable_erase(it); } + + // Removes all mappings. + // + // All iterators are invalidated. + // + void clear() { map_.clear(); } + + private: + iterator find(const key_type &key, iterator first) { + return std::find_if(first, end(), + [&key](const auto &pair) { return KeyEqual{}(pair.first, key); }); + } + + bool erase(const key_type &key, iterator first) { + const auto it = find(key, first); + if (it == end()) return false; + map_.unstable_erase(it); + return true; + } + + void deduplicate() { + for (auto it = begin(); it != end();) { + if (const auto key = it->first; ++it != end()) { + while (erase(key, it)); + } + } + } + + Map map_; + }; + +// Deduction guide for in-place constructor. + template + SmallMap(InitializerList, std::index_sequence, Types...> &&) + -> SmallMap; + +// Returns whether the key-value pairs of two maps are equal. + template + bool operator==(const SmallMap &lhs, const SmallMap &rhs) { + if (lhs.size() != rhs.size()) return false; + + for (const auto &[k, v]: lhs) { + const auto &lv = v; + if (!rhs.get(k).transform([&lv](const W &rv) { return lv == rv; }).value_or(false)) { + return false; + } + } + + return true; + } + +// TODO: Remove in C++20. + template + inline bool operator!=(const SmallMap &lhs, const SmallMap &rhs) { + return !(lhs == rhs); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/small_vector.h b/sysbridge/src/main/cpp/android/ftl/small_vector.h new file mode 100644 index 0000000000..ebbe63f16e --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/small_vector.h @@ -0,0 +1,469 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace android::ftl { + + template + struct is_small_vector; + +// ftl::StaticVector that promotes to std::vector when full. SmallVector is a drop-in replacement +// for std::vector with statically allocated storage for N elements, whose goal is to improve run +// time by avoiding heap allocation and increasing probability of cache hits. The standard API is +// augmented by an unstable_erase operation that does not preserve order, and a replace operation +// that destructively emplaces. +// +// Unlike std::vector, T does not require copy/move assignment, so may be an object with const data +// members, or be const itself. +// +// SmallVector is a specialization that thinly wraps std::vector. +// +// Example usage: +// +// ftl::SmallVector vector; +// assert(vector.empty()); +// assert(!vector.dynamic()); +// +// vector = {'a', 'b', 'c'}; +// assert(vector.size() == 3u); +// assert(!vector.dynamic()); +// +// vector.push_back('d'); +// assert(vector.dynamic()); +// +// vector.unstable_erase(vector.begin()); +// assert(vector == (ftl::SmallVector{'d', 'b', 'c'})); +// +// vector.pop_back(); +// assert(vector.back() == 'b'); +// assert(vector.dynamic()); +// +// const char array[] = "hi"; +// vector = ftl::SmallVector(array); +// assert(vector == (ftl::SmallVector{'h', 'i', '\0'})); +// assert(!vector.dynamic()); +// +// ftl::SmallVector strings = ftl::init::list("abc")("123456", 3u)(3u, '?'); +// assert(strings.size() == 3u); +// assert(!strings.dynamic()); +// +// assert(strings[0] == "abc"); +// assert(strings[1] == "123"); +// assert(strings[2] == "???"); +// + template + class SmallVector final : details::ArrayTraits, details::ArrayComparators { + using Static = StaticVector; + using Dynamic = SmallVector; + + public: + FTL_ARRAY_TRAIT(T, value_type); + FTL_ARRAY_TRAIT(T, size_type); + FTL_ARRAY_TRAIT(T, difference_type); + + FTL_ARRAY_TRAIT(T, pointer); + FTL_ARRAY_TRAIT(T, reference); + FTL_ARRAY_TRAIT(T, iterator); + FTL_ARRAY_TRAIT(T, reverse_iterator); + + FTL_ARRAY_TRAIT(T, const_pointer); + FTL_ARRAY_TRAIT(T, const_reference); + FTL_ARRAY_TRAIT(T, const_iterator); + FTL_ARRAY_TRAIT(T, const_reverse_iterator); + + // Creates an empty vector. + SmallVector() = default; + + // Constructs at most N elements. See StaticVector for underlying constructors. + template>{}>> + SmallVector(Arg &&arg, Args &&... args) + : vector_(std::in_place_type, std::forward(arg), + std::forward(args)...) {} + + // Copies or moves elements from a smaller convertible vector. + template 0)>> + SmallVector(SmallVector other) : vector_(convert(std::move(other))) {} + + void swap(SmallVector &other) { vector_.swap(other.vector_); } + + // Returns whether the vector is backed by static or dynamic storage. + bool dynamic() const { return std::holds_alternative(vector_); } + + // Avoid std::visit as it generates a dispatch table. +#define DISPATCH(T, F, ...) \ + T F() __VA_ARGS__ { \ + return dynamic() ? std::get(vector_).F() : std::get(vector_).F(); \ + } + + DISPATCH(size_type, max_size, const) + + DISPATCH(size_type, size, const) + + DISPATCH(bool, empty, const) + + DISPATCH(iterator, begin,) + + DISPATCH(const_iterator, begin, const) + + DISPATCH(const_iterator, cbegin, const) + + DISPATCH(iterator, end,) + + DISPATCH(const_iterator, end, const) + + DISPATCH(const_iterator, cend, const) + + DISPATCH(reverse_iterator, rbegin,) + + DISPATCH(const_reverse_iterator, rbegin, const) + + DISPATCH(const_reverse_iterator, crbegin, const) + + DISPATCH(reverse_iterator, rend,) + + DISPATCH(const_reverse_iterator, rend, const) + + DISPATCH(const_reverse_iterator, crend, const) + + DISPATCH(iterator, last,) + + DISPATCH(const_iterator, last, const) + + DISPATCH(reference, front,) + + DISPATCH(const_reference, front, const) + + DISPATCH(reference, back,) + + DISPATCH(const_reference, back, const) + + reference operator[](size_type i) { + return dynamic() ? std::get(vector_)[i] : std::get(vector_)[i]; + } + + const_reference + operator[](size_type i) const { return const_cast(*this)[i]; } + + // Replaces an element, and returns a reference to it. The iterator must be dereferenceable, so + // replacing at end() is erroneous. + // + // The element is emplaced via move constructor, so type T does not need to define copy/move + // assignment, e.g. its data members may be const. + // + // The arguments may directly or indirectly refer to the element being replaced. + // + // Iterators to the replaced element point to its replacement, and others remain valid. + // + template + reference replace(const_iterator it, Args &&... args) { + if (dynamic()) { + return std::get(vector_).replace(it, std::forward(args)...); + } else { + return std::get(vector_).replace(it, std::forward(args)...); + } + } + + // Appends an element, and returns a reference to it. + // + // If the vector reaches its static or dynamic capacity, then all iterators are invalidated. + // Otherwise, only the end() iterator is invalidated. + // + template + reference emplace_back(Args &&... args) { + constexpr auto kInsertStatic = &Static::template emplace_back; + constexpr auto kInsertDynamic = &Dynamic::template emplace_back; + return *insert(std::forward(args)...); + } + + // Appends an element. + // + // If the vector reaches its static or dynamic capacity, then all iterators are invalidated. + // Otherwise, only the end() iterator is invalidated. + // + void push_back(const value_type &v) { + constexpr auto kInsertStatic = + static_cast(&Static::push_back); + constexpr auto kInsertDynamic = + static_cast(&Dynamic::push_back); + insert(v); + } + + void push_back(value_type &&v) { + constexpr auto kInsertStatic = static_cast(&Static::push_back); + constexpr auto kInsertDynamic = + static_cast(&Dynamic::push_back); + insert(std::move(v)); + } + + // Removes the last element. The vector must not be empty, or the call is erroneous. + // + // The last() and end() iterators are invalidated. + // + DISPATCH(void, pop_back,) + + // Removes all elements. + // + // All iterators are invalidated. + // + DISPATCH(void, clear,) + +#undef DISPATCH + + // Erases an element, but does not preserve order. Rather than shifting subsequent elements, + // this moves the last element to the slot of the erased element. + // + // The last() and end() iterators, as well as those to the erased element, are invalidated. + // + void unstable_erase(iterator it) { + if (dynamic()) { + std::get(vector_).unstable_erase(it); + } else { + std::get(vector_).unstable_erase(it); + } + } + + // Extracts the elements as std::vector. + std::vector> promote() &&{ + if (dynamic()) { + return std::get(std::move(vector_)).promote(); + } else { + return {std::make_move_iterator(begin()), std::make_move_iterator(end())}; + } + } + + private: + template + friend + class SmallVector; + + template + static std::variant convert(SmallVector &&other) { + using Other = SmallVector; + + if (other.dynamic()) { + return std::get(std::move(other.vector_)); + } else { + return std::get(std::move(other.vector_)); + } + } + + template + auto insert(Args &&... args) { + if (Dynamic *const vector = std::get_if(&vector_)) { + return (vector->*InsertDynamic)(std::forward(args)...); + } + + auto &vector = std::get(vector_); + if (vector.full()) { + return (promote(vector).*InsertDynamic)(std::forward(args)...); + } else { + return (vector.*InsertStatic)(std::forward(args)...); + } + } + + Dynamic &promote(Static &static_vector) { + assert(static_vector.full()); + + // Allocate double capacity to reduce probability of reallocation. + Dynamic vector; + vector.reserve(Static::max_size() * 2); + std::move(static_vector.begin(), static_vector.end(), std::back_inserter(vector)); + + return vector_.template emplace(std::move(vector)); + } + + std::variant vector_; + }; + +// Partial specialization without static storage. + template + class SmallVector final : details::ArrayTraits, + details::ArrayComparators, + details::ArrayIterators, T>, + std::vector> { + using details::ArrayTraits::replace_at; + + using Iter = details::ArrayIterators; + using Impl = std::vector>; + + friend Iter; + + public: + FTL_ARRAY_TRAIT(T, value_type); + FTL_ARRAY_TRAIT(T, size_type); + FTL_ARRAY_TRAIT(T, difference_type); + + FTL_ARRAY_TRAIT(T, pointer); + FTL_ARRAY_TRAIT(T, reference); + FTL_ARRAY_TRAIT(T, iterator); + FTL_ARRAY_TRAIT(T, reverse_iterator); + + FTL_ARRAY_TRAIT(T, const_pointer); + FTL_ARRAY_TRAIT(T, const_reference); + FTL_ARRAY_TRAIT(T, const_iterator); + FTL_ARRAY_TRAIT(T, const_reverse_iterator); + + // See std::vector for underlying constructors. + using Impl::Impl; + + // Copies and moves a vector, respectively. + SmallVector(const SmallVector &) = default; + + SmallVector(SmallVector &&) = default; + + // Constructs elements in place. See StaticVector for underlying constructor. + template + SmallVector(InitializerList, Types...> &&list) + : SmallVector(SmallVector(std::move(list))) {} + + // Copies or moves elements from a convertible vector. + template + SmallVector(SmallVector other) : Impl(convert(std::move(other))) {} + + SmallVector &operator=(SmallVector other) { + // Define copy/move assignment in terms of copy/move construction. + swap(other); + return *this; + } + + void swap(SmallVector &other) { Impl::swap(other); } + + using Impl::empty; + using Impl::max_size; + using Impl::size; + + using Impl::reserve; + + // std::vector iterators are not necessarily raw pointers. + iterator begin() { return Impl::data(); } + + iterator end() { return Impl::data() + size(); } + + using Iter::begin; + using Iter::end; + + using Iter::cbegin; + using Iter::cend; + + using Iter::rbegin; + using Iter::rend; + + using Iter::crbegin; + using Iter::crend; + + using Iter::last; + + using Iter::back; + using Iter::front; + + using Iter::operator[]; + + template + reference replace(const_iterator it, Args &&... args) { + return replace_at(it, std::forward(args)...); + } + + template + iterator emplace_back(Args &&... args) { + return &Impl::emplace_back(std::forward(args)...); + } + + bool push_back(const value_type &v) { + Impl::push_back(v); + return true; + } + + bool push_back(value_type &&v) { + Impl::push_back(std::move(v)); + return true; + } + + using Impl::clear; + using Impl::pop_back; + + void unstable_erase(iterator it) { + if (it != last()) replace(it, std::move(back())); + pop_back(); + } + + std::vector> promote() &&{ return std::move(*this); } + + private: + template + static Impl convert(SmallVector &&other) { + if constexpr (std::is_constructible_v> &&>) { + return std::move(other).promote(); + } else { + SmallVector vector(other.size()); + + // Consistently with StaticVector, T only requires copy/move construction from U, rather than + // copy/move assignment. + auto it = vector.begin(); + for (auto &element: other) { + vector.replace(it++, std::move(element)); + } + + return vector; + } + } + }; + + template + struct is_small_vector : std::false_type { + }; + + template + struct is_small_vector> : std::true_type { + }; + +// Deduction guide for array constructor. + template + SmallVector(T (&)[N]) -> SmallVector, N>; + +// Deduction guide for variadic constructor. + template, + typename = std::enable_if_t<(std::is_constructible_v && ...)>> + SmallVector(T &&, Us &&...) -> SmallVector; + +// Deduction guide for in-place constructor. + template + SmallVector(InitializerList, Types...> &&) + -> SmallVector; + +// Deduction guide for StaticVector conversion. + template + SmallVector(StaticVector &&) -> SmallVector; + + template + inline void swap(SmallVector &lhs, SmallVector &rhs) { + lhs.swap(rhs); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/static_vector.h b/sysbridge/src/main/cpp/android/ftl/static_vector.h new file mode 100644 index 0000000000..d14c12b390 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/static_vector.h @@ -0,0 +1,431 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace android::ftl { + + constexpr struct IteratorRangeTag { + } kIteratorRange; + +// Fixed-capacity, statically allocated counterpart of std::vector. Like std::array, StaticVector +// allocates contiguous storage for N elements of type T at compile time, but stores at most (rather +// than exactly) N elements. Unlike std::array, its default constructor does not require T to have a +// default constructor, since elements are constructed in place as the vector grows. Operations that +// insert an element (emplace_back, push_back, etc.) fail when the vector is full. The API otherwise +// adheres to standard containers, except the unstable_erase operation that does not preserve order, +// and the replace operation that destructively emplaces. +// +// Unlike std::vector, T does not require copy/move assignment, so may be an object with const data +// members, or be const itself. +// +// StaticVector is analogous to an iterable std::optional. +// StaticVector is an error. +// +// Example usage: +// +// ftl::StaticVector vector; +// assert(vector.empty()); +// +// vector = {'a', 'b'}; +// assert(vector.size() == 2u); +// +// vector.push_back('c'); +// assert(vector.full()); +// +// assert(!vector.push_back('d')); +// assert(vector.size() == 3u); +// +// vector.unstable_erase(vector.begin()); +// assert(vector == (ftl::StaticVector{'c', 'b'})); +// +// vector.pop_back(); +// assert(vector.back() == 'c'); +// +// const char array[] = "hi"; +// vector = ftl::StaticVector(array); +// assert(vector == (ftl::StaticVector{'h', 'i', '\0'})); +// +// ftl::StaticVector strings = ftl::init::list("abc")("123456", 3u)(3u, '?'); +// assert(strings.size() == 3u); +// assert(strings[0] == "abc"); +// assert(strings[1] == "123"); +// assert(strings[2] == "???"); +// + template + class StaticVector final : details::ArrayTraits, + details::ArrayIterators, T>, + details::ArrayComparators { + static_assert(N > 0); + + // For constructor that moves from a smaller convertible vector. + template + friend + class StaticVector; + + using details::ArrayTraits::construct_at; + using details::ArrayTraits::replace_at; + using details::ArrayTraits::in_place_swap_ranges; + using details::ArrayTraits::uninitialized_copy; + + using Iter = details::ArrayIterators; + friend Iter; + + // There is ambiguity when constructing from two iterator-like elements like pointers: + // they could be an iterator range, or arguments for in-place construction. Assume the + // latter unless they are input iterators and cannot be used to construct elements. If + // the former is intended, the caller can pass an IteratorRangeTag to disambiguate. + template> + using is_input_iterator = + std::conjunction, + std::negation>>; + + public: + FTL_ARRAY_TRAIT(T, value_type); + FTL_ARRAY_TRAIT(T, size_type); + FTL_ARRAY_TRAIT(T, difference_type); + + FTL_ARRAY_TRAIT(T, pointer); + FTL_ARRAY_TRAIT(T, reference); + FTL_ARRAY_TRAIT(T, iterator); + FTL_ARRAY_TRAIT(T, reverse_iterator); + + FTL_ARRAY_TRAIT(T, const_pointer); + FTL_ARRAY_TRAIT(T, const_reference); + FTL_ARRAY_TRAIT(T, const_iterator); + FTL_ARRAY_TRAIT(T, const_reverse_iterator); + + // Creates an empty vector. + StaticVector() = default; + + // Copies and moves a vector, respectively. + StaticVector(const StaticVector &other) + : StaticVector(kIteratorRange, other.begin(), other.end()) {} + + StaticVector(StaticVector &&other) { swap(other); } + + // Copies at most N elements from a smaller convertible vector. + template + StaticVector(const StaticVector &other) + : StaticVector(kIteratorRange, other.begin(), other.end()) { + static_assert(N >= M, "Insufficient capacity"); + } + + // Copies at most N elements from a smaller convertible array. + template + explicit StaticVector(U (&array)[M]) + : StaticVector(kIteratorRange, std::begin(array), std::end(array)) { + static_assert(N >= M, "Insufficient capacity"); + } + + // Copies at most N elements from the range [first, last). + // + // IteratorRangeTag disambiguates with initialization from two iterator-like elements. + // + template{}>> + StaticVector(Iterator first, Iterator last) : StaticVector(kIteratorRange, first, last) { + using V = typename std::iterator_traits::value_type; + static_assert(std::is_constructible_v, "Incompatible iterator range"); + } + + template + StaticVector(IteratorRangeTag, Iterator first, Iterator last) + : size_(std::min(max_size(), static_cast(std::distance(first, last)))) { + uninitialized_copy(first, first + size_, begin()); + } + + // Moves at most N elements from a smaller convertible vector. + template + StaticVector(StaticVector &&other) { + static_assert(N >= M, "Insufficient capacity"); + + // Same logic as swap, though M need not be equal to N. + std::uninitialized_move(other.begin(), other.end(), begin()); + std::destroy(other.begin(), other.end()); + std::swap(size_, other.size_); + } + + // Constructs at most N elements. The template arguments T and N are inferred using the + // deduction guide defined below. Note that T is determined from the first element, and + // subsequent elements must have convertible types: + // + // ftl::StaticVector vector = {1, 2, 3}; + // static_assert(std::is_same_v>); + // + // const auto copy = "quince"s; + // auto move = "tart"s; + // ftl::StaticVector vector = {copy, std::move(move)}; + // + // static_assert(std::is_same_v>); + // + template>> + StaticVector(E &&element, Es &&... elements) + : StaticVector(std::index_sequence<0>{}, std::forward(element), + std::forward(elements)...) { + static_assert(sizeof...(elements) < N, "Too many elements"); + } + + // Constructs at most N elements in place by forwarding per-element constructor arguments. The + // template arguments T and N are inferred using the deduction guide defined below. The syntax + // for listing arguments is as follows: + // + // ftl::StaticVector vector = ftl::init::list("abc")()(3u, '?'); + // + // static_assert(std::is_same_v>); + // assert(vector.full()); + // assert(vector[0] == "abc"); + // assert(vector[1].empty()); + // assert(vector[2] == "???"); + // + template + StaticVector(InitializerList, Types...> &&list) + : StaticVector(std::index_sequence<0, 0, Size>{}, std::make_index_sequence{}, + std::index_sequence{}, list.tuple) { + static_assert(sizeof...(Sizes) < N, "Too many elements"); + } + + ~StaticVector() { std::destroy(begin(), end()); } + + StaticVector &operator=(const StaticVector &other) { + StaticVector copy(other); + swap(copy); + return *this; + } + + StaticVector &operator=(StaticVector &&other) { + clear(); + swap(other); + return *this; + } + + // IsEmpty enables a fast path when the vector is known to be empty at compile time. + template + void swap(StaticVector &); + + static constexpr size_type max_size() { return N; } + + size_type size() const { return size_; } + + bool empty() const { return size() == 0; } + + bool full() const { return size() == max_size(); } + + iterator begin() { return std::launder(reinterpret_cast(data_)); } + + iterator end() { return begin() + size(); } + + using Iter::begin; + using Iter::end; + + using Iter::cbegin; + using Iter::cend; + + using Iter::rbegin; + using Iter::rend; + + using Iter::crbegin; + using Iter::crend; + + using Iter::last; + + using Iter::back; + using Iter::front; + + using Iter::operator[]; + + // Replaces an element, and returns a reference to it. The iterator must be dereferenceable, so + // replacing at end() is erroneous. + // + // The element is emplaced via move constructor, so type T does not need to define copy/move + // assignment, e.g. its data members may be const. + // + // The arguments may directly or indirectly refer to the element being replaced. + // + // Iterators to the replaced element point to its replacement, and others remain valid. + // + template + reference replace(const_iterator it, Args &&... args) { + return replace_at(it, std::forward(args)...); + } + + // Appends an element, and returns an iterator to it. If the vector is full, the element is not + // inserted, and the end() iterator is returned. + // + // On success, the end() iterator is invalidated. + // + template + iterator emplace_back(Args &&... args) { + if (full()) return end(); + const iterator it = construct_at(end(), std::forward(args)...); + ++size_; + return it; + } + + // Appends an element unless the vector is full, and returns whether the element was inserted. + // + // On success, the end() iterator is invalidated. + // + bool push_back(const value_type &v) { + // Two statements for sequence point. + const iterator it = emplace_back(v); + return it != end(); + } + + bool push_back(value_type &&v) { + // Two statements for sequence point. + const iterator it = emplace_back(std::move(v)); + return it != end(); + } + + // Removes the last element. The vector must not be empty, or the call is erroneous. + // + // The last() and end() iterators are invalidated. + // + void pop_back() { unstable_erase(last()); } + + // Removes all elements. + // + // All iterators are invalidated. + // + void clear() { + std::destroy(begin(), end()); + size_ = 0; + } + + // Erases an element, but does not preserve order. Rather than shifting subsequent elements, + // this moves the last element to the slot of the erased element. + // + // The last() and end() iterators, as well as those to the erased element, are invalidated. + // + void unstable_erase(const_iterator it) { + std::destroy_at(it); + if (it != last()) { + // Move last element and destroy its source for destructor side effects. This is only + // safe because exceptions are disabled. + construct_at(it, std::move(back())); + std::destroy_at(last()); + } + --size_; + } + + private: + // Recursion for variadic constructor. + template + StaticVector(std::index_sequence, E &&element, Es &&... elements) + : StaticVector(std::index_sequence{}, std::forward(elements)...) { + construct_at(begin() + I, std::forward(element)); + } + + // Base case for variadic constructor. + template + explicit StaticVector(std::index_sequence) : size_(I) {} + + // Recursion for in-place constructor. + // + // Construct element I by extracting its arguments from the InitializerList tuple. ArgIndex + // is the position of its first argument in Args, and ArgCount is the number of arguments. + // The Indices sequence corresponds to [0, ArgCount). + // + // The Sizes sequence lists the argument counts for elements after I, so Size is the ArgCount + // for the next element. The recursion stops when Sizes is empty for the last element. + // + template + StaticVector(std::index_sequence, std::index_sequence, + std::index_sequence, std::tuple &tuple) + : StaticVector(std::index_sequence{}, + std::make_index_sequence{}, std::index_sequence{}, + tuple) { + construct_at(begin() + I, std::move(std::get(tuple))...); + } + + // Base case for in-place constructor. + template + StaticVector(std::index_sequence, std::index_sequence, + std::index_sequence<>, std::tuple &tuple) + : size_(I + 1) { + construct_at(begin() + I, std::move(std::get(tuple))...); + } + + size_type size_ = 0; + std::aligned_storage_t data_[N]; + }; + +// Deduction guide for array constructor. + template + StaticVector(T (&)[N]) -> StaticVector, N>; + +// Deduction guide for variadic constructor. + template, + typename = std::enable_if_t<(std::is_constructible_v && ...)>> + StaticVector(T &&, Us &&...) -> StaticVector; + +// Deduction guide for in-place constructor. + template + StaticVector(InitializerList, Types...> &&) + -> StaticVector; + + template + template + void StaticVector::swap(StaticVector &other) { + auto [to, from] = std::make_pair(this, &other); + if (from == this) return; + + // Assume this vector has fewer elements, so the excess of the other vector will be moved to it. + auto [min, max] = std::make_pair(size(), other.size()); + + // No elements to swap if moving into an empty vector. + if constexpr (IsEmpty) { + assert(min == 0); + } else { + if (min > max) { + std::swap(from, to); + std::swap(min, max); + } + + // Swap elements [0, min). + in_place_swap_ranges(begin(), begin() + min, other.begin()); + + // No elements to move if sizes are equal. + if (min == max) return; + } + + // Move elements [min, max) and destroy their source for destructor side effects. + const auto [first, last] = std::make_pair(from->begin() + min, from->begin() + max); + std::uninitialized_move(first, last, to->begin() + min); + std::destroy(first, last); + + std::swap(size_, other.size_); + } + + template + inline void swap(StaticVector &lhs, StaticVector &rhs) { + lhs.swap(rhs); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/string.h b/sysbridge/src/main/cpp/android/ftl/string.h new file mode 100644 index 0000000000..46e45e8169 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/string.h @@ -0,0 +1,105 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace android::ftl { + + enum class Radix { + kBin = 2, kDec = 10, kHex = 16 + }; + + template + struct to_chars_length { + static_assert(std::is_integral_v); + // Maximum binary digits, plus minus sign and radix prefix. + static constexpr std::size_t value = + std::numeric_limits>::digits + 3; + }; + + template + constexpr std::size_t to_chars_length_v = to_chars_length::value; + + template + using to_chars_buffer_t = char[to_chars_length_v]; + +// Lightweight (not allocating nor sprintf-based) alternative to std::to_string for integers, with +// optional radix. See also ftl::to_string below. +// +// ftl::to_chars_buffer_t<> buffer; +// +// assert(ftl::to_chars(buffer, 123u) == "123"); +// assert(ftl::to_chars(buffer, -42, ftl::Radix::kBin) == "-0b101010"); +// assert(ftl::to_chars(buffer, 0xcafe, ftl::Radix::kHex) == "0xcafe"); +// assert(ftl::to_chars(buffer, '*', ftl::Radix::kHex) == "0x2a"); +// + template + std::string_view to_chars(char (&buffer)[N], T v, Radix radix = Radix::kDec) { + static_assert(N >= to_chars_length_v); + + auto begin = buffer + 2; + const auto [end, err] = std::to_chars(begin, buffer + N, v, static_cast(radix)); + assert(err == std::errc()); + + if (radix == Radix::kDec) { + // TODO: Replace with {begin, end} in C++20. + return {begin, static_cast(end - begin)}; + } + + const auto prefix = radix == Radix::kBin ? 'b' : 'x'; + if constexpr (std::is_unsigned_v) { + buffer[0] = '0'; + buffer[1] = prefix; + } else { + if (*begin == '-') { + *buffer = '-'; + } else { + --begin; + } + + *begin-- = prefix; + *begin = '0'; + } + + // TODO: Replace with {buffer, end} in C++20. + return {buffer, static_cast(end - buffer)}; + } + +// Lightweight (not sprintf-based) alternative to std::to_string for integers, with optional radix. +// +// assert(ftl::to_string(123u) == "123"); +// assert(ftl::to_string(-42, ftl::Radix::kBin) == "-0b101010"); +// assert(ftl::to_string(0xcafe, ftl::Radix::kHex) == "0xcafe"); +// assert(ftl::to_string('*', ftl::Radix::kHex) == "0x2a"); +// + template + inline std::string to_string(T v, Radix radix = Radix::kDec) { + to_chars_buffer_t buffer; + return std::string(to_chars(buffer, v, radix)); + } + + std::string to_string(bool) = delete; + + std::string to_string(bool, Radix) = delete; + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/unit.h b/sysbridge/src/main/cpp/android/ftl/unit.h new file mode 100644 index 0000000000..2dd71e43a1 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/unit.h @@ -0,0 +1,79 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android::ftl { + +// The unit type, and its only value. + constexpr struct Unit { + } unit; + + constexpr bool operator==(Unit, Unit) { + return true; + } + + constexpr bool operator!=(Unit, Unit) { + return false; + } + +// Adapts a function object F to return Unit. The return value of F is ignored. +// +// As a practical use, the function passed to ftl::Optional::transform is not allowed to return +// void (cf. https://wg21.link/P0798R8#mapping-functions-returning-void), but may return Unit if +// only its side effects are meaningful: +// +// ftl::Optional opt = "food"s; +// opt.transform(ftl::unit_fn([](std::string& str) { str.pop_back(); })); +// assert(opt == "foo"s); +// + template + struct UnitFn { + F f; + + template + Unit operator()(Args &&... args) { + return f(std::forward(args)...), unit; + } + }; + + template + constexpr auto unit_fn(F &&f) -> UnitFn> { + return {std::forward(f)}; + } + + namespace details { + +// Identity function for all T except Unit, which maps to void. + template + struct UnitToVoid { + template + static auto from(U &&value) { + return value; + } + }; + + template<> + struct UnitToVoid { + template + static void from(U &&) {} + }; + + } // namespace details +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/input/Input.cpp b/sysbridge/src/main/cpp/android/input/Input.cpp new file mode 100644 index 0000000000..7af317a488 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/Input.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#define LOG_TAG "Input" + +namespace android { + + bool isFromSource(uint32_t source, uint32_t test) { + return (source & test) == test; + } +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/input/Input.h b/sysbridge/src/main/cpp/android/input/Input.h index f6f86a83b9..b07479be2a 100644 --- a/sysbridge/src/main/cpp/android/input/Input.h +++ b/sysbridge/src/main/cpp/android/input/Input.h @@ -65,3 +65,7 @@ enum { // input events received include those that it will not deliver. POLICY_FLAG_PASS_TO_USER = 0x40000000, }; + +namespace android { + bool isFromSource(uint32_t source, uint32_t test); +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/input/InputDevice.cpp b/sysbridge/src/main/cpp/android/input/InputDevice.cpp new file mode 100644 index 0000000000..37f44f74f2 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/InputDevice.cpp @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "InputDevice" + +#include +#include +#include + +#include "../logging.h" +#include "../libbase/stringprintf.h" +#include +#include "InputDevice.h" +#include "InputEventLabels.h" +#include "../ui/LogicalDisplayId.h" + +using android::base::StringPrintf; + +namespace android { + +// Set to true to log detailed debugging messages about IDC file probing. + static constexpr bool DEBUG_PROBE = false; + + static const char *CONFIGURATION_FILE_DIR[] = { + "idc/", + "keylayout/", + "keychars/", + }; + + static const char *CONFIGURATION_FILE_EXTENSION[] = { + ".idc", + ".kl", + ".kcm", + }; + + static bool isValidNameChar(char ch) { + return isascii(ch) && (isdigit(ch) || isalpha(ch) || ch == '-' || ch == '_'); + } + + static void appendInputDeviceConfigurationFileRelativePath(std::string &path, + const std::string &name, + InputDeviceConfigurationFileType type) { + path += CONFIGURATION_FILE_DIR[static_cast(type)]; + path += name; + path += CONFIGURATION_FILE_EXTENSION[static_cast(type)]; + } + + std::string getInputDeviceConfigurationFilePathByDeviceIdentifier( + const InputDeviceIdentifier &deviceIdentifier, InputDeviceConfigurationFileType type, + const char *suffix) { + if (deviceIdentifier.vendor != 0 && deviceIdentifier.product != 0) { + if (deviceIdentifier.version != 0) { + // Try vendor product version. + std::string versionPath = + getInputDeviceConfigurationFilePathByName( + StringPrintf("Vendor_%04x_Product_%" + "04x_Version_%04x%s", + deviceIdentifier.vendor, + deviceIdentifier.product, + deviceIdentifier.version, + suffix), + type); + if (!versionPath.empty()) { + return versionPath; + } + } + + // Try vendor product. + std::string productPath = + getInputDeviceConfigurationFilePathByName( + StringPrintf("Vendor_%04x_Product_%04x%s", + deviceIdentifier.vendor, + deviceIdentifier.product, + suffix), + type); + if (!productPath.empty()) { + return productPath; + } + } + + // Try device name. + return getInputDeviceConfigurationFilePathByName( + deviceIdentifier.getCanonicalName() + suffix, + type); + } + + std::string getInputDeviceConfigurationFilePathByName( + const std::string &name, InputDeviceConfigurationFileType type) { + // Search system repository. + std::string path; + + // Treblized input device config files will be located /product/usr, /system_ext/usr, + // /odm/usr or /vendor/usr. + std::vector pathPrefixes{ + "/product/usr/", + "/system_ext/usr/", + "/odm/usr/", + "/vendor/usr/", + }; + // These files may also be in the APEX pointed by input_device.config_file.apex sysprop. +// if (auto apex = GetProperty("input_device.config_file.apex", ""); !apex.empty()) { +// pathPrefixes.push_back("/apex/" + apex + "/etc/usr/"); +// } + // ANDROID_ROOT may not be set on host + if (auto android_root = getenv("ANDROID_ROOT"); android_root != nullptr) { + pathPrefixes.push_back(std::string(android_root) + "/usr/"); + } + for (const auto &prefix: pathPrefixes) { + path = prefix; + appendInputDeviceConfigurationFileRelativePath(path, name, type); + if (!access(path.c_str(), R_OK)) { + if (DEBUG_PROBE) { + LOGI("Found system-provided input device configuration file at %s", + path.c_str()); + } + return path; + } else if (errno != ENOENT) { + LOGW("Couldn't find a system-provided input device configuration file at %s due to error %d (%s); there may be an IDC file there that cannot be loaded.", + path.c_str(), errno, strerror(errno)); + } else { + if (DEBUG_PROBE) { + LOGE("Didn't find system-provided input device configuration file at %s: %s", + path.c_str(), strerror(errno)); + } + } + } + + // Search user repository. + // TODO Should only look here if not in safe mode. + path = ""; + char *androidData = getenv("ANDROID_DATA"); + if (androidData != nullptr) { + path += androidData; + } + path += "/system/devices/"; + appendInputDeviceConfigurationFileRelativePath(path, name, type); + if (!access(path.c_str(), R_OK)) { + if (DEBUG_PROBE) { + LOGI("Found system user input device configuration file at %s", path.c_str()); + } + return path; + } else if (errno != ENOENT) { + LOGW("Couldn't find a system user input device configuration file at %s due to error %d (%s); there may be an IDC file there that cannot be loaded.", + path.c_str(), errno, strerror(errno)); + } else { + if (DEBUG_PROBE) { + LOGE("Didn't find system user input device configuration file at %s: %s", + path.c_str(), strerror(errno)); + } + } + + // Not found. + if (DEBUG_PROBE) { + LOGI("Probe failed to find input device configuration file with name '%s' and type %s", + name.c_str(), ftl::enum_string(type).c_str()); + } + return ""; + } + +// --- InputDeviceIdentifier + + std::string InputDeviceIdentifier::getCanonicalName() const { + std::string replacedName = name; + for (char &ch: replacedName) { + if (!isValidNameChar(ch)) { + ch = '_'; + } + } + return replacedName; + } + + +// --- InputDeviceInfo --- + + InputDeviceInfo::InputDeviceInfo() { + initialize(-1, 0, -1, InputDeviceIdentifier(), "", false, false, + ui::LogicalDisplayId::INVALID); + } + + InputDeviceInfo::InputDeviceInfo(const InputDeviceInfo &other) + : mId(other.mId), + mGeneration(other.mGeneration), + mControllerNumber(other.mControllerNumber), + mIdentifier(other.mIdentifier), + mAlias(other.mAlias), + mIsExternal(other.mIsExternal), + mHasMic(other.mHasMic), + mKeyboardLayoutInfo(other.mKeyboardLayoutInfo), + mSources(other.mSources), + mKeyboardType(other.mKeyboardType), + mUsiVersion(other.mUsiVersion), + mAssociatedDisplayId(other.mAssociatedDisplayId), + mEnabled(other.mEnabled), + mHasVibrator(other.mHasVibrator), + mHasBattery(other.mHasBattery), + mHasButtonUnderPad(other.mHasButtonUnderPad), + mHasSensor(other.mHasSensor), + mMotionRanges(other.mMotionRanges), + mSensors(other.mSensors), + mLights(other.mLights), + mViewBehavior(other.mViewBehavior) {} + + InputDeviceInfo &InputDeviceInfo::operator=(const InputDeviceInfo &other) { + mId = other.mId; + mGeneration = other.mGeneration; + mControllerNumber = other.mControllerNumber; + mIdentifier = other.mIdentifier; + mAlias = other.mAlias; + mIsExternal = other.mIsExternal; + mHasMic = other.mHasMic; + mKeyboardLayoutInfo = other.mKeyboardLayoutInfo; + mSources = other.mSources; + mKeyboardType = other.mKeyboardType; + mUsiVersion = other.mUsiVersion; + mAssociatedDisplayId = other.mAssociatedDisplayId; + mEnabled = other.mEnabled; + mHasVibrator = other.mHasVibrator; + mHasBattery = other.mHasBattery; + mHasButtonUnderPad = other.mHasButtonUnderPad; + mHasSensor = other.mHasSensor; + mMotionRanges = other.mMotionRanges; + mSensors = other.mSensors; + mLights = other.mLights; + mViewBehavior = other.mViewBehavior; + return *this; + } + + InputDeviceInfo::~InputDeviceInfo() { + } + + void InputDeviceInfo::initialize(int32_t id, int32_t generation, int32_t controllerNumber, + const InputDeviceIdentifier &identifier, + const std::string &alias, + bool isExternal, bool hasMic, + ui::LogicalDisplayId associatedDisplayId, + InputDeviceViewBehavior viewBehavior, bool enabled) { + mId = id; + mGeneration = generation; + mControllerNumber = controllerNumber; + mIdentifier = identifier; + mAlias = alias; + mIsExternal = isExternal; + mHasMic = hasMic; + mSources = 0; + mKeyboardType = AINPUT_KEYBOARD_TYPE_NONE; + mAssociatedDisplayId = associatedDisplayId; + mEnabled = enabled; + mHasVibrator = false; + mHasBattery = false; + mHasButtonUnderPad = false; + mHasSensor = false; + mViewBehavior = viewBehavior; + mUsiVersion.reset(); + mMotionRanges.clear(); + mSensors.clear(); + mLights.clear(); + } + + const InputDeviceInfo::MotionRange *InputDeviceInfo::getMotionRange( + int32_t axis, uint32_t source) const { + for (const MotionRange &range: mMotionRanges) { + if (range.axis == axis && isFromSource(range.source, source)) { + return ⦥ + } + } + return nullptr; + } + + void InputDeviceInfo::addSource(uint32_t source) { + mSources |= source; + } + + void InputDeviceInfo::addMotionRange(int32_t axis, uint32_t source, float min, float max, + float flat, float fuzz, float resolution) { + MotionRange range = {axis, source, min, max, flat, fuzz, resolution}; + mMotionRanges.push_back(range); + } + + void InputDeviceInfo::addMotionRange(const MotionRange &range) { + mMotionRanges.push_back(range); + } + + void InputDeviceInfo::addSensorInfo(const InputDeviceSensorInfo &info) { + if (mSensors.find(info.type) != mSensors.end()) { + LOGW("Sensor type %s already exists, will be replaced by new sensor added.", + ftl::enum_string(info.type).c_str()); + } + mSensors.insert_or_assign(info.type, info); + } + + void InputDeviceInfo::addBatteryInfo(const InputDeviceBatteryInfo &info) { + if (mBatteries.find(info.id) != mBatteries.end()) { + LOGW("Battery id %d already exists, will be replaced by new battery added.", info.id); + } + mBatteries.insert_or_assign(info.id, info); + } + + void InputDeviceInfo::addLightInfo(const InputDeviceLightInfo &info) { + if (mLights.find(info.id) != mLights.end()) { + LOGW("Light id %d already exists, will be replaced by new light added.", info.id); + } + mLights.insert_or_assign(info.id, info); + } + + void InputDeviceInfo::setKeyboardType(int32_t keyboardType) { + mKeyboardType = keyboardType; + } + + void InputDeviceInfo::setKeyboardLayoutInfo(KeyboardLayoutInfo layoutInfo) { + mKeyboardLayoutInfo = std::move(layoutInfo); + } + + std::vector InputDeviceInfo::getSensors() { + std::vector infos; + infos.reserve(mSensors.size()); + for (const auto &[type, info]: mSensors) { + infos.push_back(info); + } + return infos; + } + + std::vector InputDeviceInfo::getLights() { + std::vector infos; + infos.reserve(mLights.size()); + for (const auto &[id, info]: mLights) { + infos.push_back(info); + } + return infos; + } + +} // namespace android \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/input/InputDevice.h b/sysbridge/src/main/cpp/android/input/InputDevice.h new file mode 100644 index 0000000000..cf23049775 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/InputDevice.h @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "../ftl/flags.h" +#include "../ftl/mixins.h" +#include "Input.h" +#include +#include +#include +#include "../ui/LogicalDisplayId.h" + +namespace android { + +/* + * Identifies a device. + */ + struct InputDeviceIdentifier { + inline InputDeviceIdentifier() : + bus(0), vendor(0), product(0), version(0) { + } + + // Information provided by the kernel. + std::string name; + std::string location; + std::string uniqueId; + uint16_t bus; + uint16_t vendor; + uint16_t product; + uint16_t version; + + // A composite input device descriptor string that uniquely identifies the device + // even across reboots or reconnections. The value of this field is used by + // upper layers of the input system to associate settings with individual devices. + // It is hashed from whatever kernel provided information is available. + // Ideally, the way this value is computed should not change between Android releases + // because that would invalidate persistent settings that rely on it. + std::string descriptor; + + // A value added to uniquely identify a device in the absence of a unique id. This + // is intended to be a minimum way to distinguish from other active devices and may + // reuse values that are not associated with an input anymore. + uint16_t nonce; + + // The bluetooth address of the device, if known. + std::optional bluetoothAddress; + + /** + * Return InputDeviceIdentifier.name that has been adjusted as follows: + * - all characters besides alphanumerics, dash, + * and underscore have been replaced with underscores. + * This helps in situations where a file that matches the device name is needed, + * while conforming to the filename limitations. + */ + std::string getCanonicalName() const; + + bool operator==(const InputDeviceIdentifier &) const = default; + + bool operator!=(const InputDeviceIdentifier &) const = default; + }; + +/** + * Holds View related behaviors for an InputDevice. + */ + struct InputDeviceViewBehavior { + /** + * The smooth scroll behavior that applies for all source/axis, if defined by the device. + * Empty optional if the device has not specified the default smooth scroll behavior. + */ + std::optional shouldSmoothScroll; + }; + +/* Types of input device sensors. Keep sync with core/java/android/hardware/Sensor.java */ + enum class InputDeviceSensorType : int32_t { + ACCELEROMETER = ASENSOR_TYPE_ACCELEROMETER, + MAGNETIC_FIELD = ASENSOR_TYPE_MAGNETIC_FIELD, + ORIENTATION = 3, + GYROSCOPE = ASENSOR_TYPE_GYROSCOPE, + LIGHT = ASENSOR_TYPE_LIGHT, + PRESSURE = ASENSOR_TYPE_PRESSURE, + TEMPERATURE = 7, + PROXIMITY = ASENSOR_TYPE_PROXIMITY, + GRAVITY = ASENSOR_TYPE_GRAVITY, + LINEAR_ACCELERATION = ASENSOR_TYPE_LINEAR_ACCELERATION, + ROTATION_VECTOR = ASENSOR_TYPE_ROTATION_VECTOR, + RELATIVE_HUMIDITY = ASENSOR_TYPE_RELATIVE_HUMIDITY, + AMBIENT_TEMPERATURE = ASENSOR_TYPE_AMBIENT_TEMPERATURE, + MAGNETIC_FIELD_UNCALIBRATED = ASENSOR_TYPE_MAGNETIC_FIELD_UNCALIBRATED, + GAME_ROTATION_VECTOR = ASENSOR_TYPE_GAME_ROTATION_VECTOR, + GYROSCOPE_UNCALIBRATED = ASENSOR_TYPE_GYROSCOPE_UNCALIBRATED, + SIGNIFICANT_MOTION = ASENSOR_TYPE_SIGNIFICANT_MOTION, + + ftl_first = ACCELEROMETER, + ftl_last = SIGNIFICANT_MOTION + }; + + enum class InputDeviceSensorAccuracy : int32_t { + NONE = 0, + LOW = 1, + MEDIUM = 2, + HIGH = 3, + + ftl_last = HIGH, + }; + + enum class InputDeviceSensorReportingMode : int32_t { + CONTINUOUS = 0, + ON_CHANGE = 1, + ONE_SHOT = 2, + SPECIAL_TRIGGER = 3, + }; + + enum class InputDeviceLightType : int32_t { + INPUT = 0, + PLAYER_ID = 1, + KEYBOARD_BACKLIGHT = 2, + KEYBOARD_MIC_MUTE = 3, + KEYBOARD_VOLUME_MUTE = 4, + + ftl_last = KEYBOARD_VOLUME_MUTE + }; + + enum class InputDeviceLightCapability : uint32_t { + /** Capability to change brightness of the light */ + BRIGHTNESS = 0x00000001, + /** Capability to change color of the light */ + RGB = 0x00000002, + }; + + struct InputDeviceSensorInfo { + explicit InputDeviceSensorInfo(std::string name, std::string vendor, int32_t version, + InputDeviceSensorType type, + InputDeviceSensorAccuracy accuracy, + float maxRange, float resolution, float power, + int32_t minDelay, + int32_t fifoReservedEventCount, int32_t fifoMaxEventCount, + std::string stringType, int32_t maxDelay, int32_t flags, + int32_t id) + : name(name), + vendor(vendor), + version(version), + type(type), + accuracy(accuracy), + maxRange(maxRange), + resolution(resolution), + power(power), + minDelay(minDelay), + fifoReservedEventCount(fifoReservedEventCount), + fifoMaxEventCount(fifoMaxEventCount), + stringType(stringType), + maxDelay(maxDelay), + flags(flags), + id(id) {} + + // Name string of the sensor. + std::string name; + // Vendor string of this sensor. + std::string vendor; + // Version of the sensor's module. + int32_t version; + // Generic type of this sensor. + InputDeviceSensorType type; + // The current accuracy of sensor event. + InputDeviceSensorAccuracy accuracy; + // Maximum range of the sensor in the sensor's unit. + float maxRange; + // Resolution of the sensor in the sensor's unit. + float resolution; + // The power in mA used by this sensor while in use. + float power; + // The minimum delay allowed between two events in microsecond or zero if this sensor only + // returns a value when the data it's measuring changes. + int32_t minDelay; + // Number of events reserved for this sensor in the batch mode FIFO. + int32_t fifoReservedEventCount; + // Maximum number of events of this sensor that could be batched. + int32_t fifoMaxEventCount; + // The type of this sensor as a string. + std::string stringType; + // The delay between two sensor events corresponding to the lowest frequency that this sensor + // supports. + int32_t maxDelay; + // Sensor flags + int32_t flags; + // Sensor id, same as the input device ID it belongs to. + int32_t id; + }; + + struct BrightnessLevel : ftl::DefaultConstructible, + ftl::Equatable, + ftl::Orderable, + ftl::Addable { + using DefaultConstructible::DefaultConstructible; + }; + + struct InputDeviceLightInfo { + explicit InputDeviceLightInfo(std::string name, int32_t id, InputDeviceLightType type, + ftl::Flags capabilityFlags, + int32_t ordinal, + std::set preferredBrightnessLevels) + : name(name), + id(id), + type(type), + capabilityFlags(capabilityFlags), + ordinal(ordinal), + preferredBrightnessLevels(std::move(preferredBrightnessLevels)) {} + + // Name string of the light. + std::string name; + // Light id + int32_t id; + // Type of the light. + InputDeviceLightType type; + // Light capabilities. + ftl::Flags capabilityFlags; + // Ordinal of the light + int32_t ordinal; + // Custom brightness levels for the light + std::set preferredBrightnessLevels; + }; + + struct InputDeviceBatteryInfo { + explicit InputDeviceBatteryInfo(std::string name, int32_t id) : name(name), id(id) {} + + // Name string of the battery. + std::string name; + // Battery id + int32_t id; + }; + + struct KeyboardLayoutInfo { + explicit KeyboardLayoutInfo(std::string languageTag, std::string layoutType) + : languageTag(languageTag), layoutType(layoutType) {} + + // A BCP 47 conformant language tag such as "en-US". + std::string languageTag; + // The layout type such as QWERTY or AZERTY. + std::string layoutType; + + inline bool operator==(const KeyboardLayoutInfo &other) const { + return languageTag == other.languageTag && layoutType == other.layoutType; + } + + inline bool operator!=(const KeyboardLayoutInfo &other) const { return !(*this == other); } + }; + +// The version of the Universal Stylus Initiative (USI) protocol supported by the input device. + struct InputDeviceUsiVersion { + int32_t majorVersion = -1; + int32_t minorVersion = -1; + }; + +/* + * Describes the characteristics and capabilities of an input device. + */ + class InputDeviceInfo { + public: + InputDeviceInfo(); + + InputDeviceInfo(const InputDeviceInfo &other); + + InputDeviceInfo &operator=(const InputDeviceInfo &other); + + ~InputDeviceInfo(); + + struct MotionRange { + int32_t axis; + uint32_t source; + float min; + float max; + float flat; + float fuzz; + float resolution; + }; + + void initialize(int32_t id, int32_t generation, int32_t controllerNumber, + const InputDeviceIdentifier &identifier, const std::string &alias, + bool isExternal, bool hasMic, ui::LogicalDisplayId associatedDisplayId, + InputDeviceViewBehavior viewBehavior = {{}}, bool enabled = true); + + inline int32_t getId() const { return mId; } + + inline int32_t getControllerNumber() const { return mControllerNumber; } + + inline int32_t getGeneration() const { return mGeneration; } + + inline const InputDeviceIdentifier &getIdentifier() const { return mIdentifier; } + + inline const std::string &getAlias() const { return mAlias; } + + inline const std::string &getDisplayName() const { + return mAlias.empty() ? mIdentifier.name : mAlias; + } + + inline bool isExternal() const { return mIsExternal; } + + inline bool hasMic() const { return mHasMic; } + + inline uint32_t getSources() const { return mSources; } + + const MotionRange *getMotionRange(int32_t axis, uint32_t source) const; + + void addSource(uint32_t source); + + void addMotionRange(int32_t axis, uint32_t source, + float min, float max, float flat, float fuzz, float resolution); + + void addMotionRange(const MotionRange &range); + + void addSensorInfo(const InputDeviceSensorInfo &info); + + void addBatteryInfo(const InputDeviceBatteryInfo &info); + + void addLightInfo(const InputDeviceLightInfo &info); + + void setKeyboardType(int32_t keyboardType); + + inline int32_t getKeyboardType() const { return mKeyboardType; } + + void setKeyboardLayoutInfo(KeyboardLayoutInfo keyboardLayoutInfo); + + inline const std::optional &getKeyboardLayoutInfo() const { + return mKeyboardLayoutInfo; + } + + inline const InputDeviceViewBehavior &getViewBehavior() const { return mViewBehavior; } + + inline void setVibrator(bool hasVibrator) { mHasVibrator = hasVibrator; } + + inline bool hasVibrator() const { return mHasVibrator; } + + inline void setHasBattery(bool hasBattery) { mHasBattery = hasBattery; } + + inline bool hasBattery() const { return mHasBattery; } + + inline void setButtonUnderPad(bool hasButton) { mHasButtonUnderPad = hasButton; } + + inline bool hasButtonUnderPad() const { return mHasButtonUnderPad; } + + inline void setHasSensor(bool hasSensor) { mHasSensor = hasSensor; } + + inline bool hasSensor() const { return mHasSensor; } + + inline const std::vector &getMotionRanges() const { + return mMotionRanges; + } + + std::vector getSensors(); + + std::vector getLights(); + + inline void setUsiVersion(std::optional usiVersion) { + mUsiVersion = std::move(usiVersion); + } + + inline std::optional getUsiVersion() const { return mUsiVersion; } + + inline ui::LogicalDisplayId getAssociatedDisplayId() const { return mAssociatedDisplayId; } + + inline void setEnabled(bool enabled) { mEnabled = enabled; } + + inline bool isEnabled() const { return mEnabled; } + + private: + int32_t mId; + int32_t mGeneration; + int32_t mControllerNumber; + InputDeviceIdentifier mIdentifier; + std::string mAlias; + bool mIsExternal; + bool mHasMic; + std::optional mKeyboardLayoutInfo; + uint32_t mSources; + int32_t mKeyboardType; + std::optional mUsiVersion; + ui::LogicalDisplayId mAssociatedDisplayId{ui::LogicalDisplayId::INVALID}; + bool mEnabled; + + bool mHasVibrator; + bool mHasBattery; + bool mHasButtonUnderPad; + bool mHasSensor; + + std::vector mMotionRanges; + std::unordered_map mSensors; + /* Map from light ID to light info */ + std::unordered_map mLights; + /* Map from battery ID to battery info */ + std::unordered_map mBatteries; + /** The View related behaviors for the device. */ + InputDeviceViewBehavior mViewBehavior; + }; + +/* Types of input device configuration files. */ + enum class InputDeviceConfigurationFileType : int32_t { + CONFIGURATION = 0, /* .idc file */ + KEY_LAYOUT = 1, /* .kl file */ + KEY_CHARACTER_MAP = 2, /* .kcm file */ + ftl_last = KEY_CHARACTER_MAP, + }; + +/* + * Gets the path of an input device configuration file, if one is available. + * Considers both system provided and user installed configuration files. + * The optional suffix is appended to the end of the file name (before the + * extension). + * + * The device identifier is used to construct several default configuration file + * names to try based on the device name, vendor, product, and version. + * + * Returns an empty string if not found. + */ + extern std::string getInputDeviceConfigurationFilePathByDeviceIdentifier( + const InputDeviceIdentifier &deviceIdentifier, InputDeviceConfigurationFileType type, + const char *suffix = ""); + +/* + * Gets the path of an input device configuration file, if one is available. + * Considers both system provided and user installed configuration files. + * + * The name is case-sensitive and is used to construct the filename to resolve. + * All characters except 'a'-'z', 'A'-'Z', '0'-'9', '-', and '_' are replaced by underscores. + * + * Returns an empty string if not found. + */ + extern std::string getInputDeviceConfigurationFilePathByName( + const std::string &name, InputDeviceConfigurationFileType type); + + enum ReservedInputDeviceId : int32_t { + // Device id representing an invalid device + INVALID_INPUT_DEVICE_ID = -2, + // Device id of a special "virtual" keyboard that is always present. + VIRTUAL_KEYBOARD_ID = -1, + // Device id of the "built-in" keyboard if there is one. + BUILT_IN_KEYBOARD_ID = 0, + // First device id available for dynamic devices + END_RESERVED_ID = 1, + }; + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/stringprintf.cpp b/sysbridge/src/main/cpp/android/libbase/stringprintf.cpp new file mode 100644 index 0000000000..d9eb0e501e --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/stringprintf.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "stringprintf.h" + +#include + +#include + +namespace android { + namespace base { + + void StringAppendV(std::string *dst, const char *format, va_list ap) { + // First try with a small fixed size buffer + char space[1024] __attribute__((__uninitialized__)); + + // It's possible for methods that use a va_list to invalidate + // the data in it upon use. The fix is to make a copy + // of the structure before using it and use that copy instead. + va_list backup_ap; + va_copy(backup_ap, ap); + int result = vsnprintf(space, sizeof(space), format, backup_ap); + va_end(backup_ap); + + if (result < static_cast(sizeof(space))) { + if (result >= 0) { + // Normal case -- everything fit. + dst->append(space, result); + return; + } + + if (result < 0) { + // Just an error. + return; + } + } + + // Increase the buffer size to the size requested by vsnprintf, + // plus one for the closing \0. + int length = result + 1; + char *buf = new char[length]; + + // Restore the va_list before we use it again + va_copy(backup_ap, ap); + result = vsnprintf(buf, length, format, backup_ap); + va_end(backup_ap); + + if (result >= 0 && result < length) { + // It fit + dst->append(buf, result); + } + delete[] buf; + } + + std::string StringPrintf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + std::string result; + StringAppendV(&result, fmt, ap); + va_end(ap); + return result; + } + + void StringAppendF(std::string *dst, const char *format, ...) { + va_list ap; + va_start(ap, format); + StringAppendV(dst, format, ap); + va_end(ap); + } + + } // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/stringprintf.h b/sysbridge/src/main/cpp/android/libbase/stringprintf.h new file mode 100644 index 0000000000..02cc100d1a --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/stringprintf.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android { + namespace base { + +// These printf-like functions are implemented in terms of vsnprintf, so they +// use the same attribute for compile-time format string checking. + +// Returns a string corresponding to printf-like formatting of the arguments. + std::string + StringPrintf(const char *fmt, ...) __attribute__((__format__(__printf__, 1, 2))); + +// Appends a printf-like formatting of the arguments to 'dst'. + void StringAppendF(std::string *dst, const char *fmt, ...) + __attribute__((__format__(__printf__, 2, 3))); + +// Appends a printf-like formatting of the arguments to 'dst'. + void StringAppendV(std::string *dst, const char *format, va_list ap) + __attribute__((__format__(__printf__, 2, 0))); + + } // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/ui/LogicalDisplayId.h b/sysbridge/src/main/cpp/android/ui/LogicalDisplayId.h new file mode 100644 index 0000000000..6cb99396ad --- /dev/null +++ b/sysbridge/src/main/cpp/android/ui/LogicalDisplayId.h @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace android::ui { + +// Type-safe wrapper for a logical display id. + struct LogicalDisplayId : ftl::Constructible, + ftl::Equatable, + ftl::Orderable { + using Constructible::Constructible; + + constexpr auto val() const { return ftl::to_underlying(*this); } + + constexpr bool isValid() const { return val() >= 0; } + + std::string toString() const { return std::to_string(val()); } + + static const LogicalDisplayId INVALID; + static const LogicalDisplayId DEFAULT; + }; + + constexpr inline LogicalDisplayId LogicalDisplayId::INVALID{-1}; + constexpr inline LogicalDisplayId LogicalDisplayId::DEFAULT{0}; + + inline std::ostream &operator<<(std::ostream &stream, LogicalDisplayId displayId) { + return stream << displayId.val(); + } + +} // namespace android::ui + +namespace std { + template<> + struct hash { + size_t operator()(const android::ui::LogicalDisplayId &displayId) const { + return hash()(displayId.val()); + } + }; +} // namespace std \ No newline at end of file diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 8921855184..53e9215ad7 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -65,6 +65,8 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI libevdev_event_code_get_name(ev.type, ev.code), ev.value, ev.code); + + result.value(); } while (rc == 1 || rc == 0 || rc == -EAGAIN); return env->NewStringUTF("Hello!"); From 9f3cfc599745217bfc88c0e5ccae32e1f14c7fc3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 24 Jul 2025 15:11:05 -0600 Subject: [PATCH 030/215] #1394 use the Generic.kl as a fallback if the device key layout file can not be found or read --- .../src/main/cpp/android/input/InputDevice.cpp | 12 +++++++++++- sysbridge/src/main/cpp/libevdev_jni.cpp | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/sysbridge/src/main/cpp/android/input/InputDevice.cpp b/sysbridge/src/main/cpp/android/input/InputDevice.cpp index 37f44f74f2..4d7920a3fb 100644 --- a/sysbridge/src/main/cpp/android/input/InputDevice.cpp +++ b/sysbridge/src/main/cpp/android/input/InputDevice.cpp @@ -92,9 +92,16 @@ namespace android { } // Try device name. - return getInputDeviceConfigurationFilePathByName( + std::string namePath = getInputDeviceConfigurationFilePathByName( deviceIdentifier.getCanonicalName() + suffix, type); + + if (!namePath.empty()) { + return namePath; + } + + // As a last resort, just use the Generic file. + return getInputDeviceConfigurationFilePathByName("Generic", type); } std::string getInputDeviceConfigurationFilePathByName( @@ -109,6 +116,7 @@ namespace android { "/system_ext/usr/", "/odm/usr/", "/vendor/usr/", + "/system/usr", }; // These files may also be in the APEX pointed by input_device.config_file.apex sysprop. // if (auto apex = GetProperty("input_device.config_file.apex", ""); !apex.empty()) { @@ -167,6 +175,8 @@ namespace android { LOGI("Probe failed to find input device configuration file with name '%s' and type %s", name.c_str(), ftl::enum_string(type).c_str()); } + + return ""; } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 53e9215ad7..b86c419721 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -9,6 +9,7 @@ #include "logging.h" #include "android/input/KeyLayoutMap.h" #include "android/libbase/result.h" +#include "android/input/InputDevice.h" extern "C" JNIEXPORT jstring JNICALL @@ -55,6 +56,17 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI // } libevdev_grab(dev, LIBEVDEV_GRAB); + android::InputDeviceIdentifier deviceId = android::InputDeviceIdentifier(); + deviceId.bus = libevdev_get_id_bustype(dev); + deviceId.vendor = libevdev_get_id_vendor(dev); + deviceId.product = libevdev_get_id_product(dev); + deviceId.version = libevdev_get_id_version(dev); + + std::string keyLayoutMapPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( + deviceId, android::InputDeviceConfigurationFileType::KEY_LAYOUT); + + LOGE("Key layout path = %s", keyLayoutMapPath.c_str()); + do { struct input_event ev; rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); @@ -67,6 +79,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI ev.code); result.value(); + } while (rc == 1 || rc == 0 || rc == -EAGAIN); return env->NewStringUTF("Hello!"); From db9981af24171d32a9004713cf59e558bb7af53b Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 24 Jul 2025 15:17:11 -0600 Subject: [PATCH 031/215] #1394 read key code and flags from key layout map --- sysbridge/src/main/cpp/libevdev_jni.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index b86c419721..a867926ad9 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -14,14 +14,6 @@ extern "C" JNIEXPORT jstring JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, jobject) { - auto result = android::KeyLayoutMap::load("/system/usr/keylayout/Generic.kl", nullptr); - - if (result.ok()) { - LOGE("RESULT OKAY"); - } else { - LOGE("RESULT FAILED"); - } - char *input_file_path = "/dev/input/event12"; struct libevdev *dev = NULL; int fd; @@ -67,6 +59,14 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI LOGE("Key layout path = %s", keyLayoutMapPath.c_str()); + auto keyLayoutResult = android::KeyLayoutMap::load("/system/usr/keylayout/Generic.kl", nullptr); + + if (keyLayoutResult.ok()) { + LOGE("KEY LAYOUT RESULT OKAY"); + } else { + LOGE("KEY LAYOUT RESULT FAILED"); + } + do { struct input_event ev; rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); @@ -78,7 +78,11 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI ev.value, ev.code); - result.value(); + int32_t outKeycode = -1; + uint32_t outFlags = -1; + keyLayoutResult.value()->mapKey(ev.code, 0, &outKeycode, &outFlags); + + LOGE("Key code = %d Flags = %d", outKeycode, outFlags); } while (rc == 1 || rc == 0 || rc == -EAGAIN); From e2275794a569dccc4b1ac21426d01697bae3f4b1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 24 Jul 2025 15:36:17 -0600 Subject: [PATCH 032/215] #1394 set the device name --- sysbridge/src/main/cpp/libevdev_jni.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index a867926ad9..b8936506eb 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -14,7 +14,7 @@ extern "C" JNIEXPORT jstring JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, jobject) { - char *input_file_path = "/dev/input/event12"; + char *input_file_path = "/dev/input/event6"; struct libevdev *dev = NULL; int fd; int rc = 1; @@ -53,6 +53,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI deviceId.vendor = libevdev_get_id_vendor(dev); deviceId.product = libevdev_get_id_product(dev); deviceId.version = libevdev_get_id_version(dev); + deviceId.name = libevdev_get_name(dev); std::string keyLayoutMapPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( deviceId, android::InputDeviceConfigurationFileType::KEY_LAYOUT); From 0a114133404b456c7445ba741ac47f783382fd7a Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 24 Jul 2025 17:24:43 -0600 Subject: [PATCH 033/215] #1394 log the event time --- sysbridge/src/main/cpp/libevdev_jni.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index b8936506eb..989c56fee0 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -14,7 +14,7 @@ extern "C" JNIEXPORT jstring JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, jobject) { - char *input_file_path = "/dev/input/event6"; + char *input_file_path = "/dev/input/event12"; struct libevdev *dev = NULL; int fd; int rc = 1; @@ -73,11 +73,13 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI 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, Event code: %d\n", + "Event: %s %s %d, Event code: %d, Time: %ld.%ld\n", libevdev_event_type_get_name(ev.type), libevdev_event_code_get_name(ev.type, ev.code), ev.value, - ev.code); + ev.code, + ev.time.tv_sec, + ev.time.tv_usec); int32_t outKeycode = -1; uint32_t outFlags = -1; From 9b0ef277b76a9d3096c4ce25b6a54bfaeedfba2f Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 24 Jul 2025 21:49:26 -0600 Subject: [PATCH 034/215] #1394 demonstrate how to create a virtual device --- sysbridge/src/main/cpp/libevdev_jni.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 989c56fee0..b006b10885 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -3,6 +3,7 @@ #include #include #include "libevdev/libevdev.h" +#include "libevdev/libevdev-uinput.h" #define LOG_TAG "KeyMapperSystemBridge" @@ -15,7 +16,7 @@ extern "C" JNIEXPORT jstring JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, jobject) { char *input_file_path = "/dev/input/event12"; - struct libevdev *dev = NULL; + struct libevdev *dev = nullptr; int fd; int rc = 1; @@ -60,7 +61,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI LOGE("Key layout path = %s", keyLayoutMapPath.c_str()); - auto keyLayoutResult = android::KeyLayoutMap::load("/system/usr/keylayout/Generic.kl", nullptr); + auto keyLayoutResult = android::KeyLayoutMap::load(keyLayoutMapPath, nullptr); if (keyLayoutResult.ok()) { LOGE("KEY LAYOUT RESULT OKAY"); @@ -68,6 +69,13 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI LOGE("KEY LAYOUT RESULT FAILED"); } + // Create a virtual device that is a duplicate of the existing one. +// struct libevdev_uinput *virtual_dev_uninput = nullptr; +// int uinput_fd = open("/dev/uinput", O_RDWR); +// libevdev_uinput_create_from_device(dev, uinput_fd, &virtual_dev_uninput); +// const char *virtual_dev_path = libevdev_uinput_get_devnode(virtual_dev_uninput); +// LOGE("Virtual keyboard device: %s", virtual_dev_path); + do { struct input_event ev; rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); @@ -86,6 +94,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI keyLayoutResult.value()->mapKey(ev.code, 0, &outKeycode, &outFlags); LOGE("Key code = %d Flags = %d", outKeycode, outFlags); +// libevdev_uinput_write_event(virtual_dev_uninput, ev.type, ev.code, ev.value); } while (rc == 1 || rc == 0 || rc == -EAGAIN); From 6c51bff4815209e0a00724ca3f28e4e27a373649 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 24 Jul 2025 22:16:58 -0600 Subject: [PATCH 035/215] #1394 refactor: rename MyKeyEvent to KMKeyEvent and MyMotionEvent to KMMotionEvent --- .../sds100/keymapper/base/BaseMainActivity.kt | 4 +- .../DetectScreenOffKeyEventsController.kt | 4 +- .../detection/DpadMotionEventTracker.kt | 12 +++--- .../keymaps/detection/KeyMapController.kt | 10 ++--- .../RerouteKeyEventsController.kt | 4 +- .../accessibility/BaseAccessibilityService.kt | 10 ++--- .../BaseAccessibilityServiceController.kt | 16 +++---- .../inputmethod/ImeInputEventInjector.kt | 11 ++--- .../base/trigger/RecordTriggerUseCase.kt | 4 +- .../keymaps/DpadMotionEventTrackerTest.kt | 20 ++++----- .../base/keymaps/KeyMapControllerTest.kt | 12 +++--- sysbridge/src/main/cpp/libevdev_jni.cpp | 1 + .../sysbridge/manager/SystemBridgeManager.kt | 1 + .../system/inputevents/InputEventInjector.kt | 42 ++++++++++--------- .../{MyKeyEvent.kt => KMKeyEvent.kt} | 6 ++- .../{MyMotionEvent.kt => KMMotionEvent.kt} | 6 +-- .../shizuku/ShizukuInputEventInjector.kt | 3 +- 17 files changed, 89 insertions(+), 77 deletions(-) rename system/src/main/java/io/github/sds100/keymapper/system/inputevents/{MyKeyEvent.kt => KMKeyEvent.kt} (63%) rename system/src/main/java/io/github/sds100/keymapper/system/inputevents/{MyMotionEvent.kt => KMMotionEvent.kt} (87%) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index fc480bd5c8..ec003122d5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -35,7 +35,7 @@ import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.files.FileUtils -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.root.SuAdapterImpl @@ -216,7 +216,7 @@ abstract class BaseMainActivity : AppCompatActivity() { event ?: return super.onGenericMotionEvent(event) val consume = - recordTriggerController.onActivityMotionEvent(MyMotionEvent.fromMotionEvent(event)) + recordTriggerController.onActivityMotionEvent(KMMotionEvent.fromMotionEvent(event)) return if (consume) { true diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt index 3b79f41c78..6d37ac98ff 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt @@ -1,7 +1,7 @@ package io.github.sds100.keymapper.base.keymaps.detection import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -10,7 +10,7 @@ import kotlinx.coroutines.Job class DetectScreenOffKeyEventsController( private val suAdapter: SuAdapter, private val devicesAdapter: DevicesAdapter, - private val onKeyEvent: suspend (event: MyKeyEvent) -> Unit, + private val onKeyEvent: suspend (event: KMKeyEvent) -> Unit, ) { companion object { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt index 0da1229d11..d627b52b98 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt @@ -4,8 +4,8 @@ import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.system.devices.InputDeviceInfo 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.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KMMotionEvent /** * See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad @@ -32,7 +32,7 @@ class DpadMotionEventTracker { * * @return whether to consume the key event. */ - fun onKeyEvent(event: MyKeyEvent): Boolean { + fun onKeyEvent(event: KMKeyEvent): Boolean { val device = event.device ?: return false if (!InputEventUtils.isDpadKeyCode(event.keyCode)) { @@ -63,7 +63,7 @@ class DpadMotionEventTracker { * * @return An array of key events. Empty if no DPAD buttons changed. */ - fun convertMotionEvent(event: MyMotionEvent): List { + fun convertMotionEvent(event: KMMotionEvent): List { val oldState = dpadState[event.device.getDescriptor()] ?: 0 val newState = eventToDpadState(event) val diff = oldState xor newState @@ -101,7 +101,7 @@ class DpadMotionEventTracker { } return keyCodes.map { - MyKeyEvent( + KMKeyEvent( it, action, metaState = event.metaState, @@ -121,7 +121,7 @@ class DpadMotionEventTracker { return this?.descriptor ?: "" } - private fun eventToDpadState(event: MyMotionEvent): Int { + private fun eventToDpadState(event: KMMotionEvent): Int { var state = 0 if (event.axisHatX == -1.0f) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt index b6325a601a..d79cf2a697 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt @@ -28,8 +28,8 @@ import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.PreferenceDefaults 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.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -569,7 +569,7 @@ class KeyMapController( } } - fun onMotionEvent(event: MyMotionEvent): Boolean { + fun onMotionEvent(event: KMMotionEvent): Boolean { if (!detectKeyMaps) return false // See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad @@ -594,7 +594,7 @@ class KeyMapController( /** * @return whether to consume the [KeyEvent]. */ - fun onKeyEvent(keyEvent: MyKeyEvent): Boolean { + fun onKeyEvent(keyEvent: KMKeyEvent): Boolean { if (!detectKeyMaps) return false if (dpadMotionEventTracker.onKeyEvent(keyEvent)) { @@ -612,7 +612,7 @@ class KeyMapController( return onKeyEventPostFilter(keyEvent) } - private fun onKeyEventPostFilter(keyEvent: MyKeyEvent): Boolean { + private fun onKeyEventPostFilter(keyEvent: KMKeyEvent): Boolean { metaStateFromKeyEvent = keyEvent.metaState // remove the metastate from any modifier keys that remapped and are pressed down diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt index 05331b8032..4bd4ee838f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt @@ -7,7 +7,7 @@ import dagger.assisted.AssistedInject import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -45,7 +45,7 @@ class RerouteKeyEventsController @AssistedInject constructor( */ private var repeatJob: Job? = null - fun onKeyEvent(event: MyKeyEvent): Boolean { + fun onKeyEvent(event: KMKeyEvent): Boolean { if (!useCase.shouldRerouteKeyEvent(event.device?.descriptor)) { return false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index a39025a574..c5ccbfc181 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -33,8 +33,8 @@ import io.github.sds100.keymapper.common.utils.MathUtils import io.github.sds100.keymapper.common.utils.PinchScreenType import io.github.sds100.keymapper.common.utils.Success 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.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl import kotlinx.coroutines.flow.Flow @@ -151,7 +151,7 @@ abstract class BaseAccessibilityService : return getController() ?.onKeyEventFromIme( - MyKeyEvent( + KMKeyEvent( keyCode = event.keyCode, action = event.action, metaState = event.metaState, @@ -167,7 +167,7 @@ abstract class BaseAccessibilityService : event ?: return false return getController() - ?.onMotionEventFromIme(MyMotionEvent.fromMotionEvent(event)) + ?.onMotionEventFromIme(KMMotionEvent.fromMotionEvent(event)) ?: return false } } @@ -313,7 +313,7 @@ abstract class BaseAccessibilityService : } return getController()?.onKeyEvent( - MyKeyEvent( + KMKeyEvent( keyCode = event.keyCode, action = event.action, metaState = event.metaState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index d8ef0a2a22..37ded59f07 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -33,8 +33,8 @@ import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter 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.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -372,7 +372,7 @@ abstract class BaseAccessibilityServiceController( /** * Returns an MyKeyEvent which is either the same or more unique */ - private fun getUniqueEvent(event: MyKeyEvent): MyKeyEvent { + private fun getUniqueEvent(event: KMKeyEvent): KMKeyEvent { // Guard to ignore processing when not applicable if (event.keyCode != KeyEvent.KEYCODE_UNKNOWN) return event @@ -393,7 +393,7 @@ abstract class BaseAccessibilityServiceController( } fun onKeyEvent( - event: MyKeyEvent, + event: KMKeyEvent, detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ): Boolean { val detailedLogInfo = event.toString() @@ -402,7 +402,7 @@ abstract class BaseAccessibilityServiceController( if (event.action == KeyEvent.ACTION_DOWN) { Timber.d("Recorded key ${KeyEvent.keyCodeToString(event.keyCode)}, $detailedLogInfo") - val uniqueEvent: MyKeyEvent = getUniqueEvent(event) + val uniqueEvent: KMKeyEvent = getUniqueEvent(event) service.lifecycleScope.launch { outputEvents.emit( @@ -426,7 +426,7 @@ abstract class BaseAccessibilityServiceController( } else { try { var consume: Boolean - val uniqueEvent: MyKeyEvent = getUniqueEvent(event) + val uniqueEvent: KMKeyEvent = getUniqueEvent(event) consume = keyMapController.onKeyEvent(uniqueEvent) @@ -448,7 +448,7 @@ abstract class BaseAccessibilityServiceController( return false } - fun onKeyEventFromIme(event: MyKeyEvent): Boolean { + fun onKeyEventFromIme(event: KMKeyEvent): Boolean { /* Issue #850 If a volume key is sent while the phone is ringing or in a call @@ -469,7 +469,7 @@ abstract class BaseAccessibilityServiceController( ) } - fun onMotionEventFromIme(event: MyMotionEvent): Boolean { + fun onMotionEventFromIme(event: KMMotionEvent): Boolean { if (isPaused.value) { return false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt index db7abdfcde..4f388fa66a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt @@ -8,6 +8,7 @@ import android.view.KeyCharacterMap import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.system.inputevents.InputEventInjector +import io.github.sds100.keymapper.system.inputevents.createKeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper @@ -79,7 +80,7 @@ class ImeInputEventInjectorImpl( val eventTime = SystemClock.uptimeMillis() - val keyEvent = createInjectedKeyEvent(eventTime, action, model) + val keyEvent = createKeyEvent(eventTime, action, model) putExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT, keyEvent) @@ -92,14 +93,14 @@ class ImeInputEventInjectorImpl( when (model.inputType) { InputEventType.DOWN_UP -> { - val downKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) + val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) keyEventRelayService.sendKeyEvent( downKeyEvent, imePackageName, CALLBACK_ID_INPUT_METHOD, ) - val upKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_UP, model) + val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) keyEventRelayService.sendKeyEvent( upKeyEvent, imePackageName, @@ -108,7 +109,7 @@ class ImeInputEventInjectorImpl( } InputEventType.DOWN -> { - val downKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) + val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) keyEventRelayService.sendKeyEvent( downKeyEvent, imePackageName, @@ -117,7 +118,7 @@ class ImeInputEventInjectorImpl( } InputEventType.UP -> { - val upKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_UP, model) + val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) keyEventRelayService.sendKeyEvent( upKeyEvent, imePackageName, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt index 0564e382c5..ff5a3d3fb7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt @@ -6,7 +6,7 @@ import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -77,7 +77,7 @@ class RecordTriggerController @Inject constructor( * these are sent from the joy sticks. * @return Whether the motion event is consumed. */ - fun onActivityMotionEvent(event: MyMotionEvent): Boolean { + fun onActivityMotionEvent(event: KMMotionEvent): Boolean { if (state.value !is RecordTriggerState.CountingDown) { return false } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt index 23ce84f14b..28f9d1b1d9 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt @@ -4,8 +4,8 @@ import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.base.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 +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import junitparams.JUnitParamsRunner import kotlinx.coroutines.ExperimentalCoroutinesApi import org.hamcrest.MatcherAssert.assertThat @@ -60,7 +60,7 @@ class DpadMotionEventTrackerTest { assertThat( keyEvents, hasItem( - MyKeyEvent( + KMKeyEvent( KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.ACTION_UP, metaState = 0, @@ -74,7 +74,7 @@ class DpadMotionEventTrackerTest { assertThat( keyEvents, hasItem( - MyKeyEvent( + KMKeyEvent( KeyEvent.KEYCODE_DPAD_UP, KeyEvent.ACTION_UP, metaState = 0, @@ -258,8 +258,8 @@ class DpadMotionEventTrackerTest { axisHatY: Float = 0.0f, device: InputDeviceInfo = CONTROLLER_1_DEVICE, isDpad: Boolean = true, - ): MyMotionEvent { - return MyMotionEvent( + ): KMMotionEvent { + return KMMotionEvent( metaState = 0, device = device, axisHatX = axisHatX, @@ -268,8 +268,8 @@ class DpadMotionEventTrackerTest { ) } - private fun createDownKeyEvent(keyCode: Int, device: InputDeviceInfo): MyKeyEvent { - return MyKeyEvent( + private fun createDownKeyEvent(keyCode: Int, device: InputDeviceInfo): KMKeyEvent { + return KMKeyEvent( keyCode = keyCode, action = KeyEvent.ACTION_DOWN, metaState = 0, @@ -280,8 +280,8 @@ class DpadMotionEventTrackerTest { ) } - private fun createUpKeyEvent(keyCode: Int, device: InputDeviceInfo): MyKeyEvent { - return MyKeyEvent( + private fun createUpKeyEvent(keyCode: Int, device: InputDeviceInfo): KMKeyEvent { + return KMKeyEvent( keyCode = keyCode, action = KeyEvent.ACTION_UP, metaState = 0, diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt index 4224d0c656..15cf762755 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt @@ -33,8 +33,8 @@ import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.withFlag 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.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import io.github.sds100.keymapper.system.inputmethod.ImeInfo import junitparams.JUnitParamsRunner import junitparams.Parameters @@ -4180,8 +4180,8 @@ class KeyMapControllerTest { axisHatY: Float = 0.0f, device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, isDpad: Boolean = true, - ): MyMotionEvent { - return MyMotionEvent( + ): KMMotionEvent { + return KMMotionEvent( metaState = 0, device = device, axisHatX = axisHatX, @@ -4195,7 +4195,7 @@ class KeyMapControllerTest { axisHatY: Float = 0.0f, device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, ): Boolean = controller.onMotionEvent( - MyMotionEvent( + KMMotionEvent( metaState = 0, device = device, axisHatX = axisHatX, @@ -4212,7 +4212,7 @@ class KeyMapControllerTest { scanCode: Int = 0, repeatCount: Int = 0, ): Boolean = controller.onKeyEvent( - MyKeyEvent( + KMKeyEvent( keyCode = keyCode, action = action, metaState = metaState ?: 0, diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index b006b10885..8edaf9128a 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -94,6 +94,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI keyLayoutResult.value()->mapKey(ev.code, 0, &outKeycode, &outFlags); LOGE("Key code = %d Flags = %d", outKeycode, outFlags); + // libevdev_uinput_write_event(virtual_dev_uninput, ev.type, ev.code, ev.value); } while (rc == 1 || rc == 0 || rc == -EAGAIN); diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 23c5246148..6c870a78b9 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -26,6 +26,7 @@ class SystemBridgeManagerImpl @Inject constructor( synchronized(lock) { this.systemBridge = ISystemBridge.Stub.asInterface(binder) + // TODO remove this.systemBridge?.sendEvent() } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt index 5a680f9893..b02186d68d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt @@ -5,23 +5,27 @@ 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, - ) - } } + + +/** + * Create a KeyEvent instance that can be injected into the Android system. + */ +fun InputEventInjector.createKeyEvent( + 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, + ) +} \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt similarity index 63% rename from system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt rename to system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 6517eefa26..3225b38968 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -2,7 +2,11 @@ package io.github.sds100.keymapper.system.inputevents import io.github.sds100.keymapper.system.devices.InputDeviceInfo -data class MyKeyEvent( +/** + * This is our own abstraction over KeyEvent so that it is easier to write tests and read + * values without relying on the Android SDK. + */ +data class KMKeyEvent( val keyCode: Int, val action: Int, val metaState: Int, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt similarity index 87% rename from system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt rename to system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt index 5e77e3b9f9..b7363bb589 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt @@ -8,7 +8,7 @@ import io.github.sds100.keymapper.system.devices.InputDeviceUtils * This is our own abstraction over MotionEvent so that it is easier to write tests and read * values without relying on the Android SDK. */ -data class MyMotionEvent( +data class KMMotionEvent( val metaState: Int, val device: InputDeviceInfo?, val axisHatX: Float, @@ -16,8 +16,8 @@ data class MyMotionEvent( val isDpad: Boolean, ) { companion object { - fun fromMotionEvent(event: MotionEvent): MyMotionEvent { - return MyMotionEvent( + fun fromMotionEvent(event: MotionEvent): KMMotionEvent { + return KMMotionEvent( metaState = event.metaState, device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) }, axisHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X), diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt index e63b4e4590..9679b8727c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt @@ -7,6 +7,7 @@ import android.os.SystemClock import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.system.inputevents.InputEventInjector +import io.github.sds100.keymapper.system.inputevents.createKeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -39,7 +40,7 @@ class ShizukuInputEventInjector : InputEventInjector { val eventTime = SystemClock.uptimeMillis() - val keyEvent = createInjectedKeyEvent(eventTime, action, model) + val keyEvent = createKeyEvent(eventTime, action, model) withContext(Dispatchers.IO) { // MUST wait for the application to finish processing the event before sending the next one. From 4914b715e7051e1f7d00197399cf8f999615b7c2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 24 Jul 2025 22:21:32 -0600 Subject: [PATCH 036/215] #1394 create KMInputEvent interface to be inherited by KMKeyEvent and KMMotionEvent --- .../io/github/sds100/keymapper/api/KeyEventRelayService.kt | 5 ++++- .../sds100/keymapper/system/inputevents/KMInputEvent.kt | 3 +++ .../github/sds100/keymapper/system/inputevents/KMKeyEvent.kt | 2 +- .../sds100/keymapper/system/inputevents/KMMotionEvent.kt | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt diff --git a/api/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt b/api/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt index 58c49939e5..db3c7350b5 100644 --- a/api/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt +++ b/api/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt @@ -21,7 +21,10 @@ import java.util.concurrent.ConcurrentHashMap * * This was originally implemented in issue #850 for the action to answer phone calls * because Android doesn't pass volume down key events to the accessibility service - * when the phone is ringing or it is in a phone call. + * when the phone is ringing or it is in a phone call. Later, in Android 14 this relay must be + * used because they also introduced a 1-second delay to context-registered broadcast receivers. + * And who knows what other restrictions will be added in the future :) + * * The accessibility service registers a callback and the input method service * sends the key events. */ diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt new file mode 100644 index 0000000000..db3fa93c67 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt @@ -0,0 +1,3 @@ +package io.github.sds100.keymapper.system.inputevents + +interface KMInputEvent \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 3225b38968..efd390b892 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -14,4 +14,4 @@ data class KMKeyEvent( val device: InputDeviceInfo?, val repeatCount: Int, val source: Int, -) +) : KMInputEvent diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt index b7363bb589..783b61956d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt @@ -14,7 +14,7 @@ data class KMMotionEvent( val axisHatX: Float, val axisHatY: Float, val isDpad: Boolean, -) { +) : KMInputEvent { companion object { fun fromMotionEvent(event: MotionEvent): KMMotionEvent { return KMMotionEvent( From 3331965e9bcadeef6c9c94af827d3f378906c276 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 27 Jul 2025 16:49:02 -0600 Subject: [PATCH 037/215] #1394 send evdev callback Binder to libevdev_jni --- app/src/main/AndroidManifest.xml | 6 +++ .../keymapper/base/actions/ActionUiHelper.kt | 2 +- .../keyevent/ConfigKeyEventActionViewModel.kt | 4 +- .../actions/keyevent/ConfigKeyEventUseCase.kt | 2 +- .../base/keymaps/ConfigKeyMapUseCase.kt | 2 +- .../base/keymaps/KeyMapListItemCreator.kt | 2 +- .../detection/DpadMotionEventTracker.kt | 2 +- .../RerouteKeyEventsController.kt | 2 +- .../base/settings/ConfigSettingsUseCase.kt | 2 +- .../accessibility/BaseAccessibilityService.kt | 2 +- .../trigger/BaseConfigTriggerViewModel.kt | 2 +- .../base/trigger/RecordTriggerEvent.kt | 2 +- .../base/trigger/RecordTriggerUseCase.kt | 2 +- .../base/trigger/TriggerKeyDevice.kt | 2 +- .../base/actions/PerformActionsUseCaseTest.kt | 2 +- ...onfigKeyServiceEventActionViewModelTest.kt | 2 +- .../keymaps/DpadMotionEventTrackerTest.kt | 2 +- .../base/keymaps/KeyMapControllerTest.kt | 2 +- .../base/repositories/KeyMapRepositoryTest.kt | 2 +- .../base/system/devices/FakeDevicesAdapter.kt | 2 +- common/build.gradle.kts | 1 + .../common/utils}/InputDeviceInfo.kt | 4 +- .../common/utils}/InputDeviceUtils.kt | 28 +++++++++++++- sysbridge/build.gradle.kts | 22 ++++++++--- .../keymapper/sysbridge/IEvdevCallback.aidl | 5 +++ .../keymapper/sysbridge/ISystemBridge.aidl | 5 ++- .../utils/InputDeviceIdentifier.aidl | 3 ++ sysbridge/src/main/cpp/CMakeLists.txt | 10 ++++- sysbridge/src/main/cpp/android.cpp | 1 + .../main/cpp/android/input/KeyLayoutMap.cpp | 6 +-- sysbridge/src/main/cpp/libevdev_jni.cpp | 37 ++++++++++++------- sysbridge/src/main/cpp/logging.h | 2 +- .../sysbridge/manager/SystemBridgeManager.kt | 29 ++++++++++++++- .../sysbridge/service/SystemBridge.kt | 18 ++++++--- .../service/SystemBridgeSetupController.kt | 3 ++ .../sysbridge/utils/InputDeviceIdentifier.kt | 14 +++++++ .../system/devices/AndroidDevicesAdapter.kt | 8 ++-- .../system/devices/DevicesAdapter.kt | 3 +- .../system/inputevents/KMKeyEvent.kt | 2 +- .../system/inputevents/KMMotionEvent.kt | 4 +- 40 files changed, 189 insertions(+), 62 deletions(-) rename {system/src/main/java/io/github/sds100/keymapper/system/devices => common/src/main/java/io/github/sds100/keymapper/common/utils}/InputDeviceInfo.kt (81%) rename {system/src/main/java/io/github/sds100/keymapper/system/devices => common/src/main/java/io/github/sds100/keymapper/common/utils}/InputDeviceUtils.kt (61%) create mode 100644 sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl create mode 100644 sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.aidl create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fd3a99ee47..698524e2c1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,10 +5,16 @@ + + + + #include #include +#include namespace android { diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp index 38877796c4..a58b0b9a2c 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -29,8 +29,8 @@ #include "../liblog/log_main.h" #include "Input.h" -#define DEBUG_MAPPING true -#define DEBUG_PARSER true +#define DEBUG_MAPPING false +#define DEBUG_PARSER false // Enables debug output for parser performance. #define DEBUG_PARSER_PERFORMANCE 0 @@ -308,7 +308,7 @@ namespace android { ALOGD_IF(DEBUG_PARSER, "Parsed key %s: code=%d, keyCode=%d, flags=0x%08x.", mapUsage ? "usage" : "scan code", *code, *keyCode, flags); - // The key code may be unknown so only insert a key if it is. + // The key code may be unknown so only insert a key if it is known. if (keyCode) { Key key; key.keyCode = *keyCode; diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 8edaf9128a..96efb804c2 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -11,11 +11,23 @@ #include "android/input/KeyLayoutMap.h" #include "android/libbase/result.h" #include "android/input/InputDevice.h" +#include extern "C" -JNIEXPORT jstring JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNIEnv *env, jobject) { +JNIEXPORT jboolean JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(JNIEnv *env, + jobject thiz, + jobject deviceId, + jobject jcallbackBinder) { + + if (__builtin_available(android 29, *)) { + LOGE("PRE BINDER"); + AIBinder *callback = AIBinder_fromJavaBinder(env, jcallbackBinder); + LOGE("POST BINDER"); + } + char *input_file_path = "/dev/input/event12"; + // TODO call libevdev_free when done with the object. struct libevdev *dev = nullptr; int fd; int rc = 1; @@ -25,13 +37,10 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI 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", @@ -49,15 +58,15 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI // } libevdev_grab(dev, LIBEVDEV_GRAB); - android::InputDeviceIdentifier deviceId = android::InputDeviceIdentifier(); - deviceId.bus = libevdev_get_id_bustype(dev); - deviceId.vendor = libevdev_get_id_vendor(dev); - deviceId.product = libevdev_get_id_product(dev); - deviceId.version = libevdev_get_id_version(dev); - deviceId.name = libevdev_get_name(dev); + android::InputDeviceIdentifier deviceIdentifier = android::InputDeviceIdentifier(); + deviceIdentifier.bus = libevdev_get_id_bustype(dev); + deviceIdentifier.vendor = libevdev_get_id_vendor(dev); + deviceIdentifier.product = libevdev_get_id_product(dev); + deviceIdentifier.version = libevdev_get_id_version(dev); + deviceIdentifier.name = libevdev_get_name(dev); std::string keyLayoutMapPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( - deviceId, android::InputDeviceConfigurationFileType::KEY_LAYOUT); + deviceIdentifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); LOGE("Key layout path = %s", keyLayoutMapPath.c_str()); @@ -99,5 +108,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stringFromJNI(JNI } while (rc == 1 || rc == 0 || rc == -EAGAIN); - return env->NewStringUTF("Hello!"); + return true; + + } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/logging.h b/sysbridge/src/main/cpp/logging.h index 91a750f4a0..9fb28c3c4f 100644 --- a/sysbridge/src/main/cpp/logging.h +++ b/sysbridge/src/main/cpp/logging.h @@ -5,7 +5,7 @@ #include "android/log.h" #ifndef LOG_TAG -#define LOG_TAG "Key Mapper" +#define LOG_TAG "KeyMapperSystemBridge" #endif #ifndef NO_LOG diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 6c870a78b9..509f825fd6 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -1,9 +1,17 @@ package io.github.sds100.keymapper.sysbridge.manager import android.content.Context +import android.hardware.input.InputManager import android.os.IBinder +import android.view.InputDevice +import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.utils.getBluetoothAddress +import io.github.sds100.keymapper.common.utils.getDeviceBus +import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.utils.InputDeviceIdentifier +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -15,6 +23,8 @@ class SystemBridgeManagerImpl @Inject constructor( @ApplicationContext private val ctx: Context ) : SystemBridgeManager { + private val inputManager: InputManager by lazy { ctx.getSystemService()!! } + private val lock: Any = Any() private var systemBridge: ISystemBridge? = null @@ -27,7 +37,24 @@ class SystemBridgeManagerImpl @Inject constructor( this.systemBridge = ISystemBridge.Stub.asInterface(binder) // TODO remove - this.systemBridge?.sendEvent() + val callback = object : IEvdevCallback.Stub() { + override fun onEvdevEvent(type: Int, code: Int, value: Int) { + Timber.e("Received evdev event: type=$type, code=$code, value=$value") + } + } + + val inputDevice: InputDevice = inputManager.getInputDevice(13) ?: return + + val deviceIdentifier = InputDeviceIdentifier( + name = inputDevice.name, + vendor = inputDevice.vendorId, + product = inputDevice.productId, + descriptor = inputDevice.descriptor, + bus = inputDevice.getDeviceBus(), + bluetoothAddress = inputDevice.getBluetoothAddress() + ) + + this.systemBridge?.grabEvdevDevice(deviceIdentifier, callback) } } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 7c0fbf839d..3fb485e439 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -8,12 +8,13 @@ import android.os.Binder import android.os.Bundle import android.os.IBinder import android.os.ServiceManager -import android.system.Os import android.util.Log +import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.provider.BinderContainer import io.github.sds100.keymapper.sysbridge.provider.SystemBridgeBinderProvider import io.github.sds100.keymapper.sysbridge.utils.IContentProviderUtils +import io.github.sds100.keymapper.sysbridge.utils.InputDeviceIdentifier import rikka.hidden.compat.ActivityManagerApis import rikka.hidden.compat.DeviceIdleControllerApis import rikka.hidden.compat.UserManagerApis @@ -26,7 +27,10 @@ class SystemBridge : ISystemBridge.Stub() { // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. - external fun stringFromJNI(): String + external fun grabEvdevDevice( + deviceIdentifier: InputDeviceIdentifier, + binder: IBinder + ): Boolean companion object { private const val TAG: String = "SystemBridge" @@ -160,6 +164,7 @@ class SystemBridge : ISystemBridge.Stub() { // TODO use the process observer to rebind when key mapper starts + for (userId in UserManagerApis.getUserIdsNoThrow()) { // TODO use correct package name sendBinderToApp(this, "io.github.sds100.keymapper.debug", userId) @@ -173,8 +178,11 @@ class SystemBridge : ISystemBridge.Stub() { exitProcess(0) } - override fun sendEvent(): String? { - Log.d(TAG, "UID = ${Os.getuid()}") - return stringFromJNI() + override fun grabEvdevDevice(deviceId: InputDeviceIdentifier, callback: IEvdevCallback?) { + if (callback == null) { + return + } + + grabEvdevDevice(deviceId, callback.asBinder()) } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index ea9a802637..82938065a6 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -42,6 +42,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { startWithAdb() } + + Timber.e("NATIVE LIB = ${ctx.applicationInfo.nativeLibraryDir}") + System.loadLibrary("evdev") } // TODO clean up diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt new file mode 100644 index 0000000000..bcb4622837 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt @@ -0,0 +1,14 @@ +package io.github.sds100.keymapper.sysbridge.utils + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class InputDeviceIdentifier( + val name: String, + val bus: Int, + val vendor: Int, + val product: Int, + val descriptor: String, + val bluetoothAddress: String? +) : Parcelable diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index 4de8e4696a..55807178c1 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -8,14 +8,17 @@ import android.os.Handler import android.os.Looper import android.view.InputDevice import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.ifIsData import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.ifIsData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -25,7 +28,6 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt index b244531a06..41df17686b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt @@ -1,8 +1,9 @@ package io.github.sds100.keymapper.system.devices +import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index efd390b892..23a46dfc34 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.system.inputevents -import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceInfo /** * This is our own abstraction over KeyEvent so that it is easier to write tests and read diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt index 783b61956d..24d2362ddc 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt @@ -1,8 +1,8 @@ package io.github.sds100.keymapper.system.inputevents import android.view.MotionEvent -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.devices.InputDeviceUtils +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils /** * This is our own abstraction over MotionEvent so that it is easier to write tests and read From 0ce8b3dc90677ad14d89a865ff62e40a6ad4e323 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 27 Jul 2025 16:53:13 -0600 Subject: [PATCH 038/215] #1394 set min sdk of 29 for sysbridge files and make the internal to the module --- .../keymapper/sysbridge/manager/SystemBridgeManager.kt | 5 +++++ .../sysbridge/provider/SystemBridgeBinderProvider.kt | 2 +- .../sds100/keymapper/sysbridge/service/SystemBridge.kt | 2 +- .../sysbridge/service/SystemBridgeSetupController.kt | 5 ++++- .../io/github/sds100/keymapper/sysbridge/starter/Starter.kt | 4 +--- .../keymapper/sysbridge/utils/IContentProviderUtils.kt | 2 +- .../keymapper/sysbridge/utils/InputDeviceIdentifier.kt | 2 +- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 509f825fd6..30c0189cc5 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -1,9 +1,12 @@ package io.github.sds100.keymapper.sysbridge.manager +import android.annotation.SuppressLint import android.content.Context import android.hardware.input.InputManager +import android.os.Build import android.os.IBinder import android.view.InputDevice +import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.getBluetoothAddress @@ -59,4 +62,6 @@ class SystemBridgeManagerImpl @Inject constructor( } } +@SuppressLint("ObsoleteSdkInt") +@RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeManager \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt index ebcdc90b99..ff652d6731 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt @@ -19,7 +19,7 @@ import timber.log.Timber * This provider receives the Binder from the system bridge. When app process starts, * the system bridge (it runs under adb/root) will send the binder to client apps with this provider. */ -class SystemBridgeBinderProvider : ContentProvider() { +internal class SystemBridgeBinderProvider : ContentProvider() { companion object { // For receive Binder from Shizuku const val METHOD_SEND_BINDER: String = "sendBinder" diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 3fb485e439..46da08eda2 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -23,7 +23,7 @@ import timber.log.Timber import kotlin.system.exitProcess @SuppressLint("LogNotTimber") -class SystemBridge : ISystemBridge.Stub() { +internal class SystemBridge : ISystemBridge.Stub() { // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 82938065a6..515a90a5f9 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.sysbridge.service +import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.preference.PreferenceManager @@ -26,6 +27,7 @@ import javax.inject.Singleton /** * This starter code is taken from the Shizuku project. */ + @Singleton class SystemBridgeSetupControllerImpl @Inject constructor( @ApplicationContext private val ctx: Context, @@ -127,7 +129,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } - @RequiresApi(Build.VERSION_CODES.M) private fun writeStarterFiles() { coroutineScope.launch(Dispatchers.IO) { try { @@ -202,6 +203,8 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } +@SuppressLint("ObsoleteSdkInt") +@RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeSetupController { @RequiresApi(Build.VERSION_CODES.R) fun pairWirelessAdb(port: Int, code: Int) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt index 2a4aff326e..0596b00df9 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt @@ -5,7 +5,6 @@ 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.sysbridge.R import io.github.sds100.keymapper.sysbridge.ktx.createDeviceProtectedStorageContextCompat import io.github.sds100.keymapper.sysbridge.ktx.logd @@ -22,8 +21,7 @@ import java.io.InputStreamReader import java.io.PrintWriter import java.util.zip.ZipFile -@RequiresApi(Build.VERSION_CODES.M) -object Starter { +internal object Starter { private var commandInternal = arrayOfNulls(2) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt index 49e9b583cb..a32c889554 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt @@ -5,7 +5,7 @@ import android.content.IContentProvider import android.os.Build import android.os.Bundle -object IContentProviderUtils { +internal object IContentProviderUtils { @Throws(android.os.RemoteException::class) fun callCompat( diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt index bcb4622837..97dc872dae 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt @@ -4,7 +4,7 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class InputDeviceIdentifier( +internal data class InputDeviceIdentifier( val name: String, val bus: Int, val vendor: Int, From ef4e0136e57f0c39f52bc5aa4540214df41ea965 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 27 Jul 2025 16:55:25 -0600 Subject: [PATCH 039/215] #1394 remove unnecessary code for checking native library location --- .../keymapper/sysbridge/service/SystemBridgeSetupController.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 515a90a5f9..74c19246bc 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -44,9 +44,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { startWithAdb() } - - Timber.e("NATIVE LIB = ${ctx.applicationInfo.nativeLibraryDir}") - System.loadLibrary("evdev") } // TODO clean up From 23196c4d81f2d5a61f8c667395ed98af5ebe5678 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 27 Jul 2025 17:33:50 -0600 Subject: [PATCH 040/215] #1394 build AIDL file for IEvdevCallback for native library --- sysbridge/build.gradle.kts | 62 +++++ .../keymapper/sysbridge/BnEvdevCallback.h | 57 +++++ .../keymapper/sysbridge/BpEvdevCallback.h | 31 +++ .../keymapper/sysbridge/IEvdevCallback.cpp | 215 ++++++++++++++++++ .../keymapper/sysbridge/IEvdevCallback.h | 72 ++++++ 5 files changed, 437 insertions(+) create mode 100644 sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h create mode 100644 sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h create mode 100644 sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp create mode 100644 sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index 193c252c65..f3d35d88b7 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -1,3 +1,5 @@ +import kotlin.io.path.absolutePathString + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -99,12 +101,15 @@ dependencies { tasks.named("preBuild") { dependsOn(generateLibEvDevEventNames) + dependsOn(compileAidlNdk) } // The list of event names needs to be parsed from the input.h file in the NDK. // input.h can be found in the Android/sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/linux/input.h // folder on macOS. val generateLibEvDevEventNames by tasks.registering(Exec::class) { + dependsOn(compileAidlNdk) + group = "build" description = "Generates event names header from input.h" @@ -134,4 +139,61 @@ val generateLibEvDevEventNames by tasks.registering(Exec::class) { inputs.file(inputHeader) inputs.file(inputEventCodesHeader) outputs.file(outputHeader) +} + +// Task to compile AIDL files for NDK. +// Taken from https://github.com/lakinduboteju/AndroidNdkBinderExamples +val compileAidlNdk by tasks.registering(Exec::class) { + group = "build" + description = "Compiles AIDL files in src/main/aidl to NDK C++ headers and sources." + + val aidlSrcDir = project.file("src/main/aidl") + // Find all .aidl files. Using fileTree ensures it's dynamic. + val aidlFiles = project.fileTree(aidlSrcDir) { + include("**/IEvdevCallback.aidl") + include("**/InputDeviceIdentifier.aidl") + } + + inputs.files(aidlFiles) + .withPathSensitivity(PathSensitivity.RELATIVE) + .withPropertyName("aidlInputFiles") + + val cppOutDir = project.file("src/main/cpp/aidl") + val cppHeaderOutDir = project.file("src/main/cpp") + + outputs.dir(cppOutDir).withPropertyName("cppOutputDir") + outputs.dir(cppHeaderOutDir).withPropertyName("cppHeaderOutputDir") + + // Path to the aidl executable in the Android SDK + val aidlToolPath = + android.sdkDirectory.toPath() + .resolve("build-tools") + .resolve(android.buildToolsVersion) + .resolve("aidl") + .absolutePathString() + val importSearchPath = aidlSrcDir.absolutePath + + // Ensure output directories exist before trying to write to them + cppOutDir.mkdirs() + cppHeaderOutDir.mkdirs() + + if (aidlFiles.isEmpty) { + logger.info("No AIDL files found in $aidlSrcDir. Skipping compileAidlNdk task.") + return@registering // Exit doLast if no files to process + } + + for (aidlFile in aidlFiles) { + logger.lifecycle("Compiling AIDL file (NDK): ${aidlFile.path}") + + commandLine( + aidlToolPath, + "--lang=ndk", + "-o", cppOutDir.absolutePath, + "-h", cppHeaderOutDir.absolutePath, + "-I", importSearchPath, + aidlFile.absolutePath + ) + } + + logger.lifecycle("AIDL NDK compilation finished. Check outputs in $cppOutDir and $cppHeaderOutDir") } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h new file mode 100644 index 0000000000..19d39f15a4 --- /dev/null +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -0,0 +1,57 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Using: /Users/sethd/Library/Android/sdk/build-tools/35.0.0/aidl --lang=ndk -o /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp/aidl -h /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp -I /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl + */ +#pragma once + +#include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" + +#include +#include + +#ifndef __BIONIC__ +#ifndef __assert2 +#define __assert2(a,b,c,d) ((void)0) +#endif +#endif + +namespace aidl { + namespace io { + namespace github { + namespace sds100 { + namespace keymapper { + namespace sysbridge { + class BnEvdevCallback : public ::ndk::BnCInterface { + public: + BnEvdevCallback(); + + virtual ~BnEvdevCallback(); + + protected: + ::ndk::SpAIBinder createBinder() override; + + private: + }; + + class IEvdevCallbackDelegator : public BnEvdevCallback { + public: + explicit IEvdevCallbackDelegator( + const std::shared_ptr &impl) : _impl(impl) { + } + + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, + int32_t in_value) override { + return _impl->onEvdevEvent(in_type, in_code, in_value); + } + + protected: + private: + std::shared_ptr _impl; + }; + + } // namespace sysbridge + } // namespace keymapper + } // namespace sds100 + } // namespace github + } // namespace io +} // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h new file mode 100644 index 0000000000..2a4a3bde5d --- /dev/null +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -0,0 +1,31 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Using: /Users/sethd/Library/Android/sdk/build-tools/35.0.0/aidl --lang=ndk -o /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp/aidl -h /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp -I /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl + */ +#pragma once + +#include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" + +#include + +namespace aidl { + namespace io { + namespace github { + namespace sds100 { + namespace keymapper { + namespace sysbridge { + class BpEvdevCallback : public ::ndk::BpCInterface { + public: + explicit BpEvdevCallback(const ::ndk::SpAIBinder &binder); + + virtual ~BpEvdevCallback(); + + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, + int32_t in_value) override; + }; + } // namespace sysbridge + } // namespace keymapper + } // namespace sds100 + } // namespace github + } // namespace io +} // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp new file mode 100644 index 0000000000..d861afeabc --- /dev/null +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -0,0 +1,215 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Using: /Users/sethd/Library/Android/sdk/build-tools/35.0.0/aidl --lang=ndk -o /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp/aidl -h /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp -I /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl + */ +#include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" + +#include +#include +#include + +namespace aidl { + namespace io { + namespace github { + namespace sds100 { + namespace keymapper { + namespace sysbridge { + static binder_status_t + _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_onTransact( + AIBinder *_aidl_binder, transaction_code_t _aidl_code, + const AParcel *_aidl_in, AParcel *_aidl_out) { + (void) _aidl_in; + (void) _aidl_out; + binder_status_t _aidl_ret_status = STATUS_UNKNOWN_TRANSACTION; + std::shared_ptr _aidl_impl = std::static_pointer_cast( + ::ndk::ICInterface::asInterface(_aidl_binder)); + switch (_aidl_code) { + case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/): { + int32_t in_type; + int32_t in_code; + int32_t in_value; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_type); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_code); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_value); + if (_aidl_ret_status != STATUS_OK) break; + + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent( + in_type, in_code, in_value); + _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, + _aidl_status.get()); + if (_aidl_ret_status != STATUS_OK) break; + + if (!AStatus_isOk(_aidl_status.get())) break; + + break; + } + } + return _aidl_ret_status; + } + + static AIBinder_Class *_g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz = ::ndk::ICInterface::defineClass( + IEvdevCallback::descriptor, + _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_onTransact); + + BpEvdevCallback::BpEvdevCallback(const ::ndk::SpAIBinder &binder) + : BpCInterface(binder) {} + + BpEvdevCallback::~BpEvdevCallback() {} + + ::ndk::ScopedAStatus + BpEvdevCallback::onEvdevEvent(int32_t in_type, int32_t in_code, + int32_t in_value) { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), + _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_type); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_code); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_value); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/), + _aidl_in.getR(), + _aidl_out.getR(), + 0 +#ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL +#endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && + IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent( + in_type, in_code, in_value); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), + _aidl_status.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; + } + +// Source for BnEvdevCallback + BnEvdevCallback::BnEvdevCallback() {} + + BnEvdevCallback::~BnEvdevCallback() {} + + ::ndk::SpAIBinder BnEvdevCallback::createBinder() { + AIBinder *binder = AIBinder_new( + _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz, + static_cast(this)); +#ifdef BINDER_STABILITY_SUPPORT + AIBinder_markCompilationUnitStability(binder); +#endif // BINDER_STABILITY_SUPPORT + return ::ndk::SpAIBinder(binder); + } + +// Source for IEvdevCallback + const char *IEvdevCallback::descriptor = "io.github.sds100.keymapper.sysbridge.IEvdevCallback"; + + IEvdevCallback::IEvdevCallback() {} + + IEvdevCallback::~IEvdevCallback() {} + + + std::shared_ptr + IEvdevCallback::fromBinder(const ::ndk::SpAIBinder &binder) { + if (!AIBinder_associateClass(binder.get(), + _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz)) { +#if __ANDROID_API__ >= 31 + const AIBinder_Class* originalClass = AIBinder_getClass(binder.get()); + if (originalClass == nullptr) return nullptr; + if (0 == strcmp(AIBinder_Class_getDescriptor(originalClass), descriptor)) { + return ::ndk::SharedRefBase::make(binder); + } +#endif + return nullptr; + } + std::shared_ptr<::ndk::ICInterface> interface = ::ndk::ICInterface::asInterface( + binder.get()); + if (interface) { + return std::static_pointer_cast(interface); + } + return ::ndk::SharedRefBase::make(binder); + } + + binder_status_t IEvdevCallback::writeToParcel(AParcel *parcel, + const std::shared_ptr &instance) { + return AParcel_writeStrongBinder(parcel, + instance ? instance->asBinder().get() + : nullptr); + } + + binder_status_t IEvdevCallback::readFromParcel(const AParcel *parcel, + std::shared_ptr *instance) { + ::ndk::SpAIBinder binder; + binder_status_t status = AParcel_readStrongBinder(parcel, + binder.getR()); + if (status != STATUS_OK) return status; + *instance = IEvdevCallback::fromBinder(binder); + return STATUS_OK; + } + + bool IEvdevCallback::setDefaultImpl( + const std::shared_ptr &impl) { + // Only one user of this interface can use this function + // at a time. This is a heuristic to detect if two different + // users in the same process use this function. + assert(!IEvdevCallback::default_impl); + if (impl) { + IEvdevCallback::default_impl = impl; + return true; + } + return false; + } + + const std::shared_ptr &IEvdevCallback::getDefaultImpl() { + return IEvdevCallback::default_impl; + } + + std::shared_ptr IEvdevCallback::default_impl = nullptr; + + ::ndk::ScopedAStatus + IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_type*/, + int32_t /*in_code*/, + int32_t /*in_value*/) { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; + } + + ::ndk::SpAIBinder IEvdevCallbackDefault::asBinder() { + return ::ndk::SpAIBinder(); + } + + bool IEvdevCallbackDefault::isRemote() { + return false; + } + } // namespace sysbridge + } // namespace keymapper + } // namespace sds100 + } // namespace github + } // namespace io +} // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h new file mode 100644 index 0000000000..6d1b0452e7 --- /dev/null +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -0,0 +1,72 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Using: /Users/sethd/Library/Android/sdk/build-tools/35.0.0/aidl --lang=ndk -o /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp/aidl -h /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp -I /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifdef BINDER_STABILITY_SUPPORT +#include +#endif // BINDER_STABILITY_SUPPORT + +namespace aidl { + namespace io { + namespace github { + namespace sds100 { + namespace keymapper { + namespace sysbridge { + class IEvdevCallbackDelegator; + + class IEvdevCallback : public ::ndk::ICInterface { + public: + typedef IEvdevCallbackDelegator DefaultDelegator; + static const char *descriptor; + + IEvdevCallback(); + + virtual ~IEvdevCallback(); + + static constexpr uint32_t TRANSACTION_onEvdevEvent = + FIRST_CALL_TRANSACTION + 0; + + static std::shared_ptr + fromBinder(const ::ndk::SpAIBinder &binder); + + static binder_status_t writeToParcel(AParcel *parcel, + const std::shared_ptr &instance); + + static binder_status_t readFromParcel(const AParcel *parcel, + std::shared_ptr *instance); + + static bool setDefaultImpl(const std::shared_ptr &impl); + + static const std::shared_ptr &getDefaultImpl(); + + virtual ::ndk::ScopedAStatus + onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) = 0; + + private: + static std::shared_ptr default_impl; + }; + + class IEvdevCallbackDefault : public IEvdevCallback { + public: + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, + int32_t in_value) override; + + ::ndk::SpAIBinder asBinder() override; + + bool isRemote() override; + }; + } // namespace sysbridge + } // namespace keymapper + } // namespace sds100 + } // namespace github + } // namespace io +} // namespace aidl From d06e09f7321e903e0db2e56c026508186e26c1fa Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 27 Jul 2025 17:39:14 -0600 Subject: [PATCH 041/215] #1394 commit generated native AIDL interfaces without formatting --- .../keymapper/sysbridge/BnEvdevCallback.h | 69 ++-- .../keymapper/sysbridge/BpEvdevCallback.h | 34 +- .../keymapper/sysbridge/IEvdevCallback.cpp | 359 ++++++++---------- .../keymapper/sysbridge/IEvdevCallback.h | 90 ++--- 4 files changed, 241 insertions(+), 311 deletions(-) diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index 19d39f15a4..3a83b05b24 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -16,42 +16,35 @@ #endif namespace aidl { - namespace io { - namespace github { - namespace sds100 { - namespace keymapper { - namespace sysbridge { - class BnEvdevCallback : public ::ndk::BnCInterface { - public: - BnEvdevCallback(); - - virtual ~BnEvdevCallback(); - - protected: - ::ndk::SpAIBinder createBinder() override; - - private: - }; - - class IEvdevCallbackDelegator : public BnEvdevCallback { - public: - explicit IEvdevCallbackDelegator( - const std::shared_ptr &impl) : _impl(impl) { - } - - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, - int32_t in_value) override { - return _impl->onEvdevEvent(in_type, in_code, in_value); - } - - protected: - private: - std::shared_ptr _impl; - }; - - } // namespace sysbridge - } // namespace keymapper - } // namespace sds100 - } // namespace github - } // namespace io +namespace io { +namespace github { +namespace sds100 { +namespace keymapper { +namespace sysbridge { +class BnEvdevCallback : public ::ndk::BnCInterface { +public: + BnEvdevCallback(); + virtual ~BnEvdevCallback(); +protected: + ::ndk::SpAIBinder createBinder() override; +private: +}; +class IEvdevCallbackDelegator : public BnEvdevCallback { +public: + explicit IEvdevCallbackDelegator(const std::shared_ptr &impl) : _impl(impl) { + } + + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) override { + return _impl->onEvdevEvent(in_type, in_code, in_value); + } +protected: +private: + std::shared_ptr _impl; +}; + +} // namespace sysbridge +} // namespace keymapper +} // namespace sds100 +} // namespace github +} // namespace io } // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index 2a4a3bde5d..3d4248e908 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -9,23 +9,21 @@ #include namespace aidl { - namespace io { - namespace github { - namespace sds100 { - namespace keymapper { - namespace sysbridge { - class BpEvdevCallback : public ::ndk::BpCInterface { - public: - explicit BpEvdevCallback(const ::ndk::SpAIBinder &binder); +namespace io { +namespace github { +namespace sds100 { +namespace keymapper { +namespace sysbridge { +class BpEvdevCallback : public ::ndk::BpCInterface { +public: + explicit BpEvdevCallback(const ::ndk::SpAIBinder& binder); + virtual ~BpEvdevCallback(); - virtual ~BpEvdevCallback(); - - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, - int32_t in_value) override; - }; - } // namespace sysbridge - } // namespace keymapper - } // namespace sds100 - } // namespace github - } // namespace io + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) override; +}; +} // namespace sysbridge +} // namespace keymapper +} // namespace sds100 +} // namespace github +} // namespace io } // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index d861afeabc..b0d996a607 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -9,207 +9,164 @@ #include namespace aidl { - namespace io { - namespace github { - namespace sds100 { - namespace keymapper { - namespace sysbridge { - static binder_status_t - _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_onTransact( - AIBinder *_aidl_binder, transaction_code_t _aidl_code, - const AParcel *_aidl_in, AParcel *_aidl_out) { - (void) _aidl_in; - (void) _aidl_out; - binder_status_t _aidl_ret_status = STATUS_UNKNOWN_TRANSACTION; - std::shared_ptr _aidl_impl = std::static_pointer_cast( - ::ndk::ICInterface::asInterface(_aidl_binder)); - switch (_aidl_code) { - case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/): { - int32_t in_type; - int32_t in_code; - int32_t in_value; - - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_type); - if (_aidl_ret_status != STATUS_OK) break; - - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_code); - if (_aidl_ret_status != STATUS_OK) break; - - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_value); - if (_aidl_ret_status != STATUS_OK) break; - - ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent( - in_type, in_code, in_value); - _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, - _aidl_status.get()); - if (_aidl_ret_status != STATUS_OK) break; - - if (!AStatus_isOk(_aidl_status.get())) break; - - break; - } - } - return _aidl_ret_status; - } - - static AIBinder_Class *_g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz = ::ndk::ICInterface::defineClass( - IEvdevCallback::descriptor, - _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_onTransact); - - BpEvdevCallback::BpEvdevCallback(const ::ndk::SpAIBinder &binder) - : BpCInterface(binder) {} - - BpEvdevCallback::~BpEvdevCallback() {} - - ::ndk::ScopedAStatus - BpEvdevCallback::onEvdevEvent(int32_t in_type, int32_t in_code, - int32_t in_value) { - binder_status_t _aidl_ret_status = STATUS_OK; - ::ndk::ScopedAStatus _aidl_status; - ::ndk::ScopedAParcel _aidl_in; - ::ndk::ScopedAParcel _aidl_out; - - _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), - _aidl_in.getR()); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_type); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_code); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_value); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = AIBinder_transact( - asBinder().get(), - (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/), - _aidl_in.getR(), - _aidl_out.getR(), - 0 -#ifdef BINDER_STABILITY_SUPPORT - | FLAG_PRIVATE_LOCAL -#endif // BINDER_STABILITY_SUPPORT - ); - if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && - IEvdevCallback::getDefaultImpl()) { - _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent( - in_type, in_code, in_value); - goto _aidl_status_return; - } - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), - _aidl_status.getR()); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; - _aidl_error: - _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); - _aidl_status_return: - return _aidl_status; - } - +namespace io { +namespace github { +namespace sds100 { +namespace keymapper { +namespace sysbridge { +static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_onTransact(AIBinder* _aidl_binder, transaction_code_t _aidl_code, const AParcel* _aidl_in, AParcel* _aidl_out) { + (void)_aidl_in; + (void)_aidl_out; + binder_status_t _aidl_ret_status = STATUS_UNKNOWN_TRANSACTION; + std::shared_ptr _aidl_impl = std::static_pointer_cast(::ndk::ICInterface::asInterface(_aidl_binder)); + switch (_aidl_code) { + case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/): { + int32_t in_type; + int32_t in_code; + int32_t in_value; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_type); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_code); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_value); + if (_aidl_ret_status != STATUS_OK) break; + + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_type, in_code, in_value); + _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); + if (_aidl_ret_status != STATUS_OK) break; + + if (!AStatus_isOk(_aidl_status.get())) break; + + break; + } + } + return _aidl_ret_status; +} + +static AIBinder_Class* _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz = ::ndk::ICInterface::defineClass(IEvdevCallback::descriptor, _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_onTransact); + +BpEvdevCallback::BpEvdevCallback(const ::ndk::SpAIBinder& binder) : BpCInterface(binder) {} +BpEvdevCallback::~BpEvdevCallback() {} + +::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_type); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_code); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_value); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/), + _aidl_in.getR(), + _aidl_out.getR(), + 0 + #ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL + #endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_type, in_code, in_value); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; +} // Source for BnEvdevCallback - BnEvdevCallback::BnEvdevCallback() {} - - BnEvdevCallback::~BnEvdevCallback() {} - - ::ndk::SpAIBinder BnEvdevCallback::createBinder() { - AIBinder *binder = AIBinder_new( - _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz, - static_cast(this)); -#ifdef BINDER_STABILITY_SUPPORT - AIBinder_markCompilationUnitStability(binder); -#endif // BINDER_STABILITY_SUPPORT - return ::ndk::SpAIBinder(binder); - } - +BnEvdevCallback::BnEvdevCallback() {} +BnEvdevCallback::~BnEvdevCallback() {} +::ndk::SpAIBinder BnEvdevCallback::createBinder() { + AIBinder* binder = AIBinder_new(_g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz, static_cast(this)); + #ifdef BINDER_STABILITY_SUPPORT + AIBinder_markCompilationUnitStability(binder); + #endif // BINDER_STABILITY_SUPPORT + return ::ndk::SpAIBinder(binder); +} // Source for IEvdevCallback - const char *IEvdevCallback::descriptor = "io.github.sds100.keymapper.sysbridge.IEvdevCallback"; - - IEvdevCallback::IEvdevCallback() {} - - IEvdevCallback::~IEvdevCallback() {} - - - std::shared_ptr - IEvdevCallback::fromBinder(const ::ndk::SpAIBinder &binder) { - if (!AIBinder_associateClass(binder.get(), - _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz)) { -#if __ANDROID_API__ >= 31 - const AIBinder_Class* originalClass = AIBinder_getClass(binder.get()); - if (originalClass == nullptr) return nullptr; - if (0 == strcmp(AIBinder_Class_getDescriptor(originalClass), descriptor)) { - return ::ndk::SharedRefBase::make(binder); - } -#endif - return nullptr; - } - std::shared_ptr<::ndk::ICInterface> interface = ::ndk::ICInterface::asInterface( - binder.get()); - if (interface) { - return std::static_pointer_cast(interface); - } - return ::ndk::SharedRefBase::make(binder); - } - - binder_status_t IEvdevCallback::writeToParcel(AParcel *parcel, - const std::shared_ptr &instance) { - return AParcel_writeStrongBinder(parcel, - instance ? instance->asBinder().get() - : nullptr); - } - - binder_status_t IEvdevCallback::readFromParcel(const AParcel *parcel, - std::shared_ptr *instance) { - ::ndk::SpAIBinder binder; - binder_status_t status = AParcel_readStrongBinder(parcel, - binder.getR()); - if (status != STATUS_OK) return status; - *instance = IEvdevCallback::fromBinder(binder); - return STATUS_OK; - } - - bool IEvdevCallback::setDefaultImpl( - const std::shared_ptr &impl) { - // Only one user of this interface can use this function - // at a time. This is a heuristic to detect if two different - // users in the same process use this function. - assert(!IEvdevCallback::default_impl); - if (impl) { - IEvdevCallback::default_impl = impl; - return true; - } - return false; - } - - const std::shared_ptr &IEvdevCallback::getDefaultImpl() { - return IEvdevCallback::default_impl; - } - - std::shared_ptr IEvdevCallback::default_impl = nullptr; - - ::ndk::ScopedAStatus - IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_type*/, - int32_t /*in_code*/, - int32_t /*in_value*/) { - ::ndk::ScopedAStatus _aidl_status; - _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); - return _aidl_status; - } - - ::ndk::SpAIBinder IEvdevCallbackDefault::asBinder() { - return ::ndk::SpAIBinder(); - } - - bool IEvdevCallbackDefault::isRemote() { - return false; - } - } // namespace sysbridge - } // namespace keymapper - } // namespace sds100 - } // namespace github - } // namespace io +const char* IEvdevCallback::descriptor = "io.github.sds100.keymapper.sysbridge.IEvdevCallback"; +IEvdevCallback::IEvdevCallback() {} +IEvdevCallback::~IEvdevCallback() {} + + +std::shared_ptr IEvdevCallback::fromBinder(const ::ndk::SpAIBinder& binder) { + if (!AIBinder_associateClass(binder.get(), _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz)) { + #if __ANDROID_API__ >= 31 + const AIBinder_Class* originalClass = AIBinder_getClass(binder.get()); + if (originalClass == nullptr) return nullptr; + if (0 == strcmp(AIBinder_Class_getDescriptor(originalClass), descriptor)) { + return ::ndk::SharedRefBase::make(binder); + } + #endif + return nullptr; + } + std::shared_ptr<::ndk::ICInterface> interface = ::ndk::ICInterface::asInterface(binder.get()); + if (interface) { + return std::static_pointer_cast(interface); + } + return ::ndk::SharedRefBase::make(binder); +} + +binder_status_t IEvdevCallback::writeToParcel(AParcel* parcel, const std::shared_ptr& instance) { + return AParcel_writeStrongBinder(parcel, instance ? instance->asBinder().get() : nullptr); +} +binder_status_t IEvdevCallback::readFromParcel(const AParcel* parcel, std::shared_ptr* instance) { + ::ndk::SpAIBinder binder; + binder_status_t status = AParcel_readStrongBinder(parcel, binder.getR()); + if (status != STATUS_OK) return status; + *instance = IEvdevCallback::fromBinder(binder); + return STATUS_OK; +} +bool IEvdevCallback::setDefaultImpl(const std::shared_ptr& impl) { + // Only one user of this interface can use this function + // at a time. This is a heuristic to detect if two different + // users in the same process use this function. + assert(!IEvdevCallback::default_impl); + if (impl) { + IEvdevCallback::default_impl = impl; + return true; + } + return false; +} +const std::shared_ptr& IEvdevCallback::getDefaultImpl() { + return IEvdevCallback::default_impl; +} +std::shared_ptr IEvdevCallback::default_impl = nullptr; +::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/) { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; +} +::ndk::SpAIBinder IEvdevCallbackDefault::asBinder() { + return ::ndk::SpAIBinder(); +} +bool IEvdevCallbackDefault::isRemote() { + return false; +} +} // namespace sysbridge +} // namespace keymapper +} // namespace sds100 +} // namespace github +} // namespace io } // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index 6d1b0452e7..fd8cf4ffd9 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -10,63 +10,45 @@ #include #include #include - #ifdef BINDER_STABILITY_SUPPORT #include #endif // BINDER_STABILITY_SUPPORT namespace aidl { - namespace io { - namespace github { - namespace sds100 { - namespace keymapper { - namespace sysbridge { - class IEvdevCallbackDelegator; - - class IEvdevCallback : public ::ndk::ICInterface { - public: - typedef IEvdevCallbackDelegator DefaultDelegator; - static const char *descriptor; - - IEvdevCallback(); - - virtual ~IEvdevCallback(); - - static constexpr uint32_t TRANSACTION_onEvdevEvent = - FIRST_CALL_TRANSACTION + 0; - - static std::shared_ptr - fromBinder(const ::ndk::SpAIBinder &binder); - - static binder_status_t writeToParcel(AParcel *parcel, - const std::shared_ptr &instance); - - static binder_status_t readFromParcel(const AParcel *parcel, - std::shared_ptr *instance); - - static bool setDefaultImpl(const std::shared_ptr &impl); - - static const std::shared_ptr &getDefaultImpl(); - - virtual ::ndk::ScopedAStatus - onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) = 0; - - private: - static std::shared_ptr default_impl; - }; - - class IEvdevCallbackDefault : public IEvdevCallback { - public: - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, - int32_t in_value) override; - - ::ndk::SpAIBinder asBinder() override; - - bool isRemote() override; - }; - } // namespace sysbridge - } // namespace keymapper - } // namespace sds100 - } // namespace github - } // namespace io +namespace io { +namespace github { +namespace sds100 { +namespace keymapper { +namespace sysbridge { +class IEvdevCallbackDelegator; + +class IEvdevCallback : public ::ndk::ICInterface { +public: + typedef IEvdevCallbackDelegator DefaultDelegator; + static const char* descriptor; + IEvdevCallback(); + virtual ~IEvdevCallback(); + + static constexpr uint32_t TRANSACTION_onEvdevEvent = FIRST_CALL_TRANSACTION + 0; + + static std::shared_ptr fromBinder(const ::ndk::SpAIBinder& binder); + static binder_status_t writeToParcel(AParcel* parcel, const std::shared_ptr& instance); + static binder_status_t readFromParcel(const AParcel* parcel, std::shared_ptr* instance); + static bool setDefaultImpl(const std::shared_ptr& impl); + static const std::shared_ptr& getDefaultImpl(); + virtual ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) = 0; +private: + static std::shared_ptr default_impl; +}; +class IEvdevCallbackDefault : public IEvdevCallback { +public: + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) override; + ::ndk::SpAIBinder asBinder() override; + bool isRemote() override; +}; +} // namespace sysbridge +} // namespace keymapper +} // namespace sds100 +} // namespace github +} // namespace io } // namespace aidl From 42d4597d25ce308a2c7cd667b20cadce97868ab0 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 27 Jul 2025 17:43:31 -0600 Subject: [PATCH 042/215] #1394 sending evdev events over Binder works! --- sysbridge/build.gradle.kts | 3 +++ sysbridge/src/main/cpp/CMakeLists.txt | 3 ++- sysbridge/src/main/cpp/libevdev_jni.cpp | 15 ++++++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index f3d35d88b7..efb40a9de7 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -21,12 +21,15 @@ android { externalNativeBuild { cmake { + val aidlSrcDir = project.file("src/main/cpp/aidl") + // -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. // -DANDROID_WEAK_API_DEFS=ON is required so the libevdev_jni file can run code depending on the SDK. https://developer.android.com/ndk/guides/using-newer-apis arguments( "-DANDROID_STL=c++_static", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", "-DANDROID_WEAK_API_DEFS=ON", + "-Daidl_src_dir=${aidlSrcDir.absolutePath}" ) } } diff --git a/sysbridge/src/main/cpp/CMakeLists.txt b/sysbridge/src/main/cpp/CMakeLists.txt index 6e8b3b8939..f87842b840 100644 --- a/sysbridge/src/main/cpp/CMakeLists.txt +++ b/sysbridge/src/main/cpp/CMakeLists.txt @@ -91,7 +91,8 @@ add_library(evdev SHARED android/utils/Unicode.cpp android/input/InputDevice.cpp android/input/Input.cpp - android/libbase/stringprintf.cpp) + android/libbase/stringprintf.cpp + ${aidl_src_dir}/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp) find_library( binder_ndk-lib diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 96efb804c2..a06ec5e58f 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -11,20 +11,24 @@ #include "android/input/KeyLayoutMap.h" #include "android/libbase/result.h" #include "android/input/InputDevice.h" +#include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" #include +using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; + extern "C" JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(JNIEnv *env, jobject thiz, jobject deviceId, jobject jcallbackBinder) { + LOGE("PRE BINDER"); + AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jcallbackBinder); - if (__builtin_available(android 29, *)) { - LOGE("PRE BINDER"); - AIBinder *callback = AIBinder_fromJavaBinder(env, jcallbackBinder); - LOGE("POST BINDER"); - } + // Create a "strong pointer" to the callback binder. + const ::ndk::SpAIBinder spBinder(callbackAIBinder); + std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); + LOGE("POST BINDER"); char *input_file_path = "/dev/input/event12"; // TODO call libevdev_free when done with the object. @@ -104,6 +108,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J LOGE("Key code = %d Flags = %d", outKeycode, outFlags); + callback->onEvdevEvent(ev.type, ev.code, ev.value); // libevdev_uinput_write_event(virtual_dev_uninput, ev.type, ev.code, ev.value); } while (rc == 1 || rc == 0 || rc == -EAGAIN); From 6d985329d93fd6115b13435a6cabf6acdec2b779 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 27 Jul 2025 23:20:06 -0600 Subject: [PATCH 043/215] #1394 refactor libevdev_jni.cpp and store evdevcallback connections --- .../keymapper/sysbridge/ISystemBridge.aidl | 5 +- .../main/cpp/android/input/KeyLayoutMap.cpp | 1 - sysbridge/src/main/cpp/libevdev_jni.cpp | 229 ++++++++++++------ sysbridge/src/main/cpp/logging.h | 2 +- .../sysbridge/manager/SystemBridgeManager.kt | 97 ++++++-- .../provider/SystemBridgeBinderProvider.kt | 4 +- .../sysbridge/service/SystemBridge.kt | 36 ++- .../android/hardware/input/IInputManager.aidl | 2 + 8 files changed, 276 insertions(+), 100 deletions(-) diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 786f1549b2..f1a91ca546 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -2,12 +2,15 @@ package io.github.sds100.keymapper.sysbridge; import io.github.sds100.keymapper.sysbridge.IEvdevCallback; import io.github.sds100.keymapper.sysbridge.utils.InputDeviceIdentifier; +import android.view.InputEvent; interface ISystemBridge { // Destroy method defined by Shizuku server. This is required // for Shizuku user services. // See demo/service/UserService.java in the Shizuku-API repository. + // TODO is this used? void destroy() = 16777114; - void grabEvdevDevice(in InputDeviceIdentifier deviceId, IEvdevCallback callback) = 1; + boolean grabEvdevDevice(int deviceId, IEvdevCallback callback) = 1; + boolean injectEvent(in InputEvent event, int mode) = 2; } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp index a58b0b9a2c..9fe17d8447 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -112,7 +112,6 @@ namespace android { tokenizer->getFilename().c_str(), tokenizer->getLineNumber(), elapsedTime / 1000000.0); #endif - LOGE("PARSE STATUS = %d", status); if (!status) { return std::move(map); } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index a06ec5e58f..5208fde0f5 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -1,12 +1,11 @@ #include #include #include +#include +#include #include #include "libevdev/libevdev.h" #include "libevdev/libevdev-uinput.h" - -#define LOG_TAG "KeyMapperSystemBridge" - #include "logging.h" #include "android/input/KeyLayoutMap.h" #include "android/libbase/result.h" @@ -16,104 +15,190 @@ using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; +static int findInputDevice( + char *name, + int bus, + int vendor, + int product, + libevdev **outDev +) { + DIR *dir = opendir("/dev/input"); + + if (dir == nullptr) { + LOGE("Failed to open /dev/input directory"); + return -1; + } + + struct dirent *entry; + + while ((entry = readdir(dir)) != nullptr) { + // Skip . and .. entries + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + char fullPath[256]; + snprintf(fullPath, sizeof(fullPath), "/dev/input/%s", entry->d_name); + + // Check if it's a character device (input device) + struct stat st{}; + + LOGD("Found input device: %s", fullPath); + + // Try to open the device to see if it's accessible + int fd = open(fullPath, O_RDONLY); + + if (fd == -1) { + continue; + } + + struct libevdev *dev = nullptr; + int status = libevdev_new_from_fd(fd, &dev); + + if (status != 0) { + LOGE("Failed to open libevdev device from path %s: %s", fullPath, strerror(errno)); + close(fd); + continue; + } + + const char *devName = libevdev_get_name(dev); + int devVendor = libevdev_get_id_vendor(dev); + int devProduct = libevdev_get_id_product(dev); + int devBus = libevdev_get_id_bustype(dev); + + LOGD("Checking device: %s, bus: %d, vendor: %d, product: %d", + devName, devBus, devVendor, devProduct); + + if (strcmp(devName, name) != 0 || + devVendor != vendor || + devProduct != product || + devBus != bus) { + + libevdev_free(dev); + close(fd); + continue; + } + + closedir(dir); + *outDev = dev; + + LOGD("Found input device %s", name); + return 0; + } + + closedir(dir); + + LOGE("Input device not found with name: %s, bus: %d, vendor: %d, product: %d", name, bus, + vendor, product); + + return -1; +} + +android::InputDeviceIdentifier +convertJInputDeviceIdentifier(JNIEnv *env, jobject jInputDeviceIdentifier) { + android::InputDeviceIdentifier deviceIdentifier; + + jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); + + jfieldID busFieldId = env->GetFieldID(inputDeviceIdentifierClass, "bus", "I"); + deviceIdentifier.bus = env->GetIntField(jInputDeviceIdentifier, busFieldId); + + jfieldID vendorFieldId = env->GetFieldID(inputDeviceIdentifierClass, "vendor", "I"); + deviceIdentifier.vendor = env->GetIntField(jInputDeviceIdentifier, vendorFieldId); + + jfieldID productFieldId = env->GetFieldID(inputDeviceIdentifierClass, "product", "I"); + deviceIdentifier.product = env->GetIntField(jInputDeviceIdentifier, productFieldId); + + jfieldID nameFieldId = env->GetFieldID(inputDeviceIdentifierClass, "name", + "Ljava/lang/String;"); + auto nameString = (jstring) env->GetObjectField(jInputDeviceIdentifier, nameFieldId); + + const char *nameChars = env->GetStringUTFChars(nameString, nullptr); + deviceIdentifier.name = std::string(nameChars); + env->ReleaseStringUTFChars(nameString, nameChars); + + return deviceIdentifier; +} + extern "C" JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(JNIEnv *env, jobject thiz, - jobject deviceId, - jobject jcallbackBinder) { - LOGE("PRE BINDER"); - AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jcallbackBinder); + jobject jInputDeviceIdentifier, + jobject jCallbackBinder) { + LOGD("Start gravEvdevDevice"); - // Create a "strong pointer" to the callback binder. - const ::ndk::SpAIBinder spBinder(callbackAIBinder); - std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); - LOGE("POST BINDER"); + android::InputDeviceIdentifier deviceIdentifier = convertJInputDeviceIdentifier(env, + jInputDeviceIdentifier); - char *input_file_path = "/dev/input/event12"; - // TODO call libevdev_free when done with the object. struct libevdev *dev = nullptr; - int fd; - int rc = 1; - fd = open(input_file_path, O_RDONLY); + int rc = findInputDevice(deviceIdentifier.name.data(), + deviceIdentifier.bus, + deviceIdentifier.vendor, + deviceIdentifier.product, + &dev); - if (fd == -1) { - LOGE("Failed to open input file (%s)", - input_file_path); - } - rc = libevdev_new_from_fd(fd, &dev); if (rc < 0) { - LOGE("Failed to init libevdev"); + return false; } - __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); - - android::InputDeviceIdentifier deviceIdentifier = android::InputDeviceIdentifier(); - deviceIdentifier.bus = libevdev_get_id_bustype(dev); - deviceIdentifier.vendor = libevdev_get_id_vendor(dev); - deviceIdentifier.product = libevdev_get_id_product(dev); - deviceIdentifier.version = libevdev_get_id_version(dev); - deviceIdentifier.name = libevdev_get_name(dev); + AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); + + // Create a "strong pointer" to the callback binder. + const ::ndk::SpAIBinder spBinder(callbackAIBinder); + + std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); std::string keyLayoutMapPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( deviceIdentifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); - LOGE("Key layout path = %s", keyLayoutMapPath.c_str()); + LOGD("Key layout path for device %s = %s", deviceIdentifier.name.c_str(), + keyLayoutMapPath.c_str()); auto keyLayoutResult = android::KeyLayoutMap::load(keyLayoutMapPath, nullptr); - if (keyLayoutResult.ok()) { - LOGE("KEY LAYOUT RESULT OKAY"); - } else { - LOGE("KEY LAYOUT RESULT FAILED"); + if (!keyLayoutResult.ok()) { + const auto &error = keyLayoutResult.error(); + + LOGE("Failed to load key layout map for device %s: %d %s", + deviceIdentifier.name.c_str(), error.code().value(), error.message().c_str()); + + return false; + } + + const auto &keyLayoutMap = keyLayoutResult.value(); + + rc = libevdev_grab(dev, LIBEVDEV_GRAB); + + if (rc < 0) { + LOGE("Failed to grab evdev device %s: %s", + libevdev_get_name(dev), strerror(-rc)); + return false; } - // Create a virtual device that is a duplicate of the existing one. -// struct libevdev_uinput *virtual_dev_uninput = nullptr; -// int uinput_fd = open("/dev/uinput", O_RDWR); -// libevdev_uinput_create_from_device(dev, uinput_fd, &virtual_dev_uninput); -// const char *virtual_dev_path = libevdev_uinput_get_devnode(virtual_dev_uninput); -// LOGE("Virtual keyboard device: %s", virtual_dev_path); + LOGD("Grabbed evdev device %s", libevdev_get_name(dev)); 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, Event code: %d, Time: %ld.%ld\n", - libevdev_event_type_get_name(ev.type), - libevdev_event_code_get_name(ev.type, ev.code), - ev.value, - ev.code, - ev.time.tv_sec, - ev.time.tv_usec); + struct input_event ev{}; - int32_t outKeycode = -1; - uint32_t outFlags = -1; - keyLayoutResult.value()->mapKey(ev.code, 0, &outKeycode, &outFlags); + rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); - LOGE("Key code = %d Flags = %d", outKeycode, outFlags); + if (rc == 0) { + int32_t outKeycode = -1; + uint32_t outFlags = -1; + keyLayoutMap->mapKey(ev.code, 0, &outKeycode, &outFlags); - callback->onEvdevEvent(ev.type, ev.code, ev.value); -// libevdev_uinput_write_event(virtual_dev_uninput, ev.type, ev.code, ev.value); + callback->onEvdevEvent(ev.type, ev.code, ev.value); + } } while (rc == 1 || rc == 0 || rc == -EAGAIN); + libevdev_grab(dev, LIBEVDEV_UNGRAB); + libevdev_free(dev); + return true; +} -} \ No newline at end of file +using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; diff --git a/sysbridge/src/main/cpp/logging.h b/sysbridge/src/main/cpp/logging.h index 9fb28c3c4f..0bc5310dc8 100644 --- a/sysbridge/src/main/cpp/logging.h +++ b/sysbridge/src/main/cpp/logging.h @@ -1,7 +1,7 @@ #ifndef _LOGGING_H #define _LOGGING_H -#include +#include #include "android/log.h" #ifndef LOG_TAG diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 30c0189cc5..1e82db48aa 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -5,16 +5,16 @@ import android.content.Context import android.hardware.input.InputManager import android.os.Build import android.os.IBinder +import android.os.IBinder.DeathRecipient import android.view.InputDevice import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.common.utils.getBluetoothAddress -import io.github.sds100.keymapper.common.utils.getDeviceBus import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge -import io.github.sds100.keymapper.sysbridge.utils.InputDeviceIdentifier +import kotlinx.coroutines.CoroutineScope import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -23,43 +23,102 @@ import javax.inject.Singleton */ @Singleton class SystemBridgeManagerImpl @Inject constructor( - @ApplicationContext private val ctx: Context + @ApplicationContext private val ctx: Context, + private val coroutineScope: CoroutineScope ) : SystemBridgeManager { private val inputManager: InputManager by lazy { ctx.getSystemService()!! } - private val lock: Any = Any() + private val systemBridgeLock: Any = Any() private var systemBridge: ISystemBridge? = null + private val callbackLock: Any = Any() + private var evdevConnections: ConcurrentHashMap = + ConcurrentHashMap() + fun pingBinder(): Boolean { - return false + synchronized(systemBridgeLock) { + return systemBridge?.asBinder()?.pingBinder() == true + } } fun onBinderReceived(binder: IBinder) { - synchronized(lock) { + synchronized(systemBridgeLock) { this.systemBridge = ISystemBridge.Stub.asInterface(binder) + } + +// coroutineScope.launch(Dispatchers.Main) { + grabAllDevices() +// } + } + + private fun grabAllDevices() { + for (deviceId in inputManager.inputDeviceIds) { + val device = inputManager.getInputDevice(deviceId) ?: continue + + if (device.name == "qwerty2") { + Timber.d("Grabbing input device: ${device.name} (${device.id})") + +// coroutineScope.launch(Dispatchers.IO) { + grabInputDevice(device) +// } + + // TODO remove + break + } + } + } + + private fun grabInputDevice(inputDevice: InputDevice) { + val deviceId = inputDevice.id - // TODO remove + val callback: IEvdevCallback = synchronized(callbackLock) { val callback = object : IEvdevCallback.Stub() { override fun onEvdevEvent(type: Int, code: Int, value: Int) { - Timber.e("Received evdev event: type=$type, code=$code, value=$value") + Timber.e("Received evdev event from device ${inputDevice.id} ${inputDevice.name}: type=$type, code=$code, value=$value") } } - val inputDevice: InputDevice = inputManager.getInputDevice(13) ?: return + if (evdevConnections.containsKey(deviceId)) { + removeEvdevConnection(deviceId) + } + + val connection = EvdevConnection(deviceId, callback) + evdevConnections[deviceId] = connection + callback.asBinder().linkToDeath(connection, 0) + + return@synchronized callback + } - val deviceIdentifier = InputDeviceIdentifier( - name = inputDevice.name, - vendor = inputDevice.vendorId, - product = inputDevice.productId, - descriptor = inputDevice.descriptor, - bus = inputDevice.getDeviceBus(), - bluetoothAddress = inputDevice.getBluetoothAddress() - ) + try { + this.systemBridge?.grabEvdevDevice(deviceId, callback) - this.systemBridge?.grabEvdevDevice(deviceIdentifier, callback) + } catch (e: Exception) { + Timber.e("Error grabbing input device: ${e.toString()}") } + } + + private fun removeEvdevConnection(deviceId: Int) { + val connection = evdevConnections.remove(deviceId) ?: return + + // Unlink the death recipient from the connection to remove and + // delete it from the list of connections for the package. + connection.callback.asBinder().unlinkToDeath(connection, 0) + } + + private inner class EvdevConnection( + private val deviceId: Int, + val callback: IEvdevCallback, + ) : DeathRecipient { + override fun binderDied() { + Timber.d("EvdevCallback binder died: $deviceId") + synchronized(callbackLock) { + removeEvdevConnection(deviceId) + } + } + } + } @SuppressLint("ObsoleteSdkInt") diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt index ff652d6731..e7304f48fe 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt @@ -50,11 +50,13 @@ internal class SystemBridgeBinderProvider : ContentProvider() { extras.classLoader = BinderContainer::class.java.getClassLoader() val reply = Bundle() + when (method) { METHOD_SEND_BINDER -> { handleSendBinder(extras) } } + return reply } @@ -70,8 +72,6 @@ internal class SystemBridgeBinderProvider : ContentProvider() { ) if (container != null && container.binder != null) { - Timber.d("binder received") - systemBridgeManager.onBinderReceived(container.binder) } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 46da08eda2..504b2d7df6 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -4,11 +4,15 @@ import android.annotation.SuppressLint import android.content.Context import android.content.IContentProvider import android.ddm.DdmHandleAppName +import android.hardware.input.IInputManager import android.os.Binder import android.os.Bundle import android.os.IBinder import android.os.ServiceManager import android.util.Log +import android.view.InputEvent +import io.github.sds100.keymapper.common.utils.getBluetoothAddress +import io.github.sds100.keymapper.common.utils.getDeviceBus import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.provider.BinderContainer @@ -29,7 +33,7 @@ internal class SystemBridge : ISystemBridge.Stub() { external fun grabEvdevDevice( deviceIdentifier: InputDeviceIdentifier, - binder: IBinder + callbackBinder: IBinder ): Boolean companion object { @@ -138,6 +142,8 @@ internal class SystemBridge : ISystemBridge.Stub() { } } + private val inputManager: IInputManager + init { @SuppressLint("UnsafeDynamicallyLoadedCode") System.load("${System.getProperty("keymapper_sysbridge.library.path")}/libevdev.so") @@ -147,6 +153,10 @@ internal class SystemBridge : ISystemBridge.Stub() { waitSystemService(Context.ACTIVITY_SERVICE) waitSystemService(Context.USER_SERVICE) waitSystemService(Context.APP_OPS_SERVICE) + waitSystemService(Context.INPUT_SERVICE) + + inputManager = + IInputManager.Stub.asInterface(ServiceManager.getService(Context.INPUT_SERVICE)) // TODO check that the key mapper app is installed, otherwise end the process. // val ai: ApplicationInfo? = rikka.shizuku.server.ShizukuService.getManagerApplicationInfo() @@ -178,11 +188,29 @@ internal class SystemBridge : ISystemBridge.Stub() { exitProcess(0) } - override fun grabEvdevDevice(deviceId: InputDeviceIdentifier, callback: IEvdevCallback?) { + override fun grabEvdevDevice( + deviceId: Int, + callback: IEvdevCallback? + ): Boolean { if (callback == null) { - return + return false } - grabEvdevDevice(deviceId, callback.asBinder()) + val inputDevice = inputManager.getInputDevice(deviceId) + + val deviceIdentifier = InputDeviceIdentifier( + name = inputDevice.name, + vendor = inputDevice.vendorId, + product = inputDevice.productId, + descriptor = inputDevice.descriptor, + bus = inputDevice.getDeviceBus(), + bluetoothAddress = inputDevice.getBluetoothAddress() + ) + + return grabEvdevDevice(deviceIdentifier, callback.asBinder()) + } + + override fun injectEvent(event: InputEvent?, mode: Int): Boolean { + return false } } \ No newline at end of file diff --git a/systemstubs/src/main/aidl/android/hardware/input/IInputManager.aidl b/systemstubs/src/main/aidl/android/hardware/input/IInputManager.aidl index 0cbe5f383d..15eaf6e908 100644 --- a/systemstubs/src/main/aidl/android/hardware/input/IInputManager.aidl +++ b/systemstubs/src/main/aidl/android/hardware/input/IInputManager.aidl @@ -1,6 +1,8 @@ package android.hardware.input; +import android.view.InputDevice; interface IInputManager { boolean injectInputEvent(in InputEvent event, int mode); + InputDevice getInputDevice(int id); } \ No newline at end of file From 962bdf0e2ae3dccea823078fc52a0dfb5b5950a3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 28 Jul 2025 15:50:10 -0600 Subject: [PATCH 044/215] #1394 use one evdevcallback for multiple devices from system bridge --- .../keymapper/sysbridge/IEvdevCallback.aidl | 2 +- .../keymapper/sysbridge/ISystemBridge.aidl | 3 +- .../keymapper/sysbridge/BnEvdevCallback.h | 7 +- .../keymapper/sysbridge/BpEvdevCallback.h | 4 +- .../keymapper/sysbridge/IEvdevCallback.cpp | 48 ++++++++++++-- .../keymapper/sysbridge/IEvdevCallback.h | 9 ++- sysbridge/src/main/cpp/libevdev_jni.cpp | 17 +++-- .../sysbridge/manager/SystemBridgeManager.kt | 64 +++++++++++-------- .../sysbridge/service/SystemBridge.kt | 19 +++++- .../sysbridge/utils/InputDeviceIdentifier.kt | 1 + 10 files changed, 129 insertions(+), 45 deletions(-) diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl index 2f37d5a98c..23c05af450 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl @@ -1,5 +1,5 @@ package io.github.sds100.keymapper.sysbridge; interface IEvdevCallback { - void onEvdevEvent(int type, int code, int value); + void onEvdevEvent(int deviceId, long timeSec, long timeUsec, int type, int code, int value, int androidCode); } \ No newline at end of file diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index f1a91ca546..e52cc4039f 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -11,6 +11,7 @@ interface ISystemBridge { // TODO is this used? void destroy() = 16777114; - boolean grabEvdevDevice(int deviceId, IEvdevCallback callback) = 1; + boolean grabEvdevDevice(int deviceId) = 1; boolean injectEvent(in InputEvent event, int mode) = 2; + void registerCallback(IEvdevCallback callback) = 3; } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index 3a83b05b24..13a164bbff 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -34,8 +34,11 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { explicit IEvdevCallbackDelegator(const std::shared_ptr &impl) : _impl(impl) { } - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) override { - return _impl->onEvdevEvent(in_type, in_code, in_value); + ::ndk::ScopedAStatus + onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, + int32_t in_code, int32_t in_value, int32_t in_androidCode) override { + return _impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, + in_androidCode); } protected: private: diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index 3d4248e908..b301ed1f33 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -19,7 +19,9 @@ class BpEvdevCallback : public ::ndk::BpCInterface { explicit BpEvdevCallback(const ::ndk::SpAIBinder& binder); virtual ~BpEvdevCallback(); - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) override; + ::ndk::ScopedAStatus + onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, + int32_t in_code, int32_t in_value, int32_t in_androidCode) override; }; } // namespace sysbridge } // namespace keymapper diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index b0d996a607..4313d00b02 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -21,9 +21,22 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback std::shared_ptr _aidl_impl = std::static_pointer_cast(::ndk::ICInterface::asInterface(_aidl_binder)); switch (_aidl_code) { case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/): { + int32_t in_deviceId; + int64_t in_timeSec; + int64_t in_timeUsec; int32_t in_type; int32_t in_code; int32_t in_value; + int32_t in_androidCode; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_deviceId); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeSec); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeUsec); + if (_aidl_ret_status != STATUS_OK) break; _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_type); if (_aidl_ret_status != STATUS_OK) break; @@ -34,7 +47,12 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_value); if (_aidl_ret_status != STATUS_OK) break; - ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_type, in_code, in_value); + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_androidCode); + if (_aidl_ret_status != STATUS_OK) break; + + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_deviceId, in_timeSec, + in_timeUsec, in_type, in_code, + in_value, in_androidCode); _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); if (_aidl_ret_status != STATUS_OK) break; @@ -51,7 +69,10 @@ static AIBinder_Class* _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallba BpEvdevCallback::BpEvdevCallback(const ::ndk::SpAIBinder& binder) : BpCInterface(binder) {} BpEvdevCallback::~BpEvdevCallback() {} -::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) { + ::ndk::ScopedAStatus + BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, + int32_t in_type, int32_t in_code, int32_t in_value, + int32_t in_androidCode) { binder_status_t _aidl_ret_status = STATUS_OK; ::ndk::ScopedAStatus _aidl_status; ::ndk::ScopedAParcel _aidl_in; @@ -60,6 +81,15 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_type, int32_t in_c _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_deviceId); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeSec); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeUsec); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_type); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; @@ -69,6 +99,9 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_type, int32_t in_c _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_value); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_androidCode); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_ret_status = AIBinder_transact( asBinder().get(), (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/), @@ -80,7 +113,9 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_type, int32_t in_c #endif // BINDER_STABILITY_SUPPORT ); if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { - _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_type, in_code, in_value); + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_deviceId, in_timeSec, + in_timeUsec, in_type, in_code, + in_value, in_androidCode); goto _aidl_status_return; } if (_aidl_ret_status != STATUS_OK) goto _aidl_error; @@ -153,7 +188,12 @@ const std::shared_ptr& IEvdevCallback::getDefaultImpl() { return IEvdevCallback::default_impl; } std::shared_ptr IEvdevCallback::default_impl = nullptr; -::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/) { + + ::ndk::ScopedAStatus + IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_deviceId*/, int64_t /*in_timeSec*/, + int64_t /*in_timeUsec*/, int32_t /*in_type*/, + int32_t /*in_code*/, int32_t /*in_value*/, + int32_t /*in_androidCode*/) { ::ndk::ScopedAStatus _aidl_status; _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index fd8cf4ffd9..b3a4da8d85 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -36,13 +36,18 @@ class IEvdevCallback : public ::ndk::ICInterface { static binder_status_t readFromParcel(const AParcel* parcel, std::shared_ptr* instance); static bool setDefaultImpl(const std::shared_ptr& impl); static const std::shared_ptr& getDefaultImpl(); - virtual ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) = 0; + + virtual ::ndk::ScopedAStatus + onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, + int32_t in_code, int32_t in_value, int32_t in_androidCode) = 0; private: static std::shared_ptr default_impl; }; class IEvdevCallbackDefault : public IEvdevCallback { public: - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_type, int32_t in_code, int32_t in_value) override; + ::ndk::ScopedAStatus + onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, + int32_t in_code, int32_t in_value, int32_t in_androidCode) override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; }; diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 5208fde0f5..f362c61969 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -66,8 +66,8 @@ static int findInputDevice( int devProduct = libevdev_get_id_product(dev); int devBus = libevdev_get_id_bustype(dev); - LOGD("Checking device: %s, bus: %d, vendor: %d, product: %d", - devName, devBus, devVendor, devProduct); +// LOGD("Checking device: %s, bus: %d, vendor: %d, product: %d", +// devName, devBus, devVendor, devProduct); if (strcmp(devName, name) != 0 || devVendor != vendor || @@ -82,7 +82,7 @@ static int findInputDevice( closedir(dir); *outDev = dev; - LOGD("Found input device %s", name); +// LOGD("Found input device %s", name); return 0; } @@ -127,6 +127,9 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J jobject jInputDeviceIdentifier, jobject jCallbackBinder) { LOGD("Start gravEvdevDevice"); + jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); + jfieldID idFieldId = env->GetFieldID(inputDeviceIdentifierClass, "id", "I"); + int deviceId = env->GetIntField(jInputDeviceIdentifier, idFieldId); android::InputDeviceIdentifier deviceIdentifier = convertJInputDeviceIdentifier(env, jInputDeviceIdentifier); @@ -179,9 +182,9 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J LOGD("Grabbed evdev device %s", libevdev_get_name(dev)); - do { - struct input_event ev{}; + struct input_event ev{}; + do { rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); if (rc == 0) { @@ -189,7 +192,9 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J uint32_t outFlags = -1; keyLayoutMap->mapKey(ev.code, 0, &outKeycode, &outFlags); - callback->onEvdevEvent(ev.type, ev.code, ev.value); + callback->onEvdevEvent(deviceId, ev.time.tv_sec, ev.time.tv_usec, ev.type, ev.code, + ev.value, + outKeycode); } } while (rc == 1 || rc == 0 || rc == -EAGAIN); diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 1e82db48aa..9efb791ab5 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -35,6 +35,22 @@ class SystemBridgeManagerImpl @Inject constructor( private val callbackLock: Any = Any() private var evdevConnections: ConcurrentHashMap = ConcurrentHashMap() + private val evdevCallback: IEvdevCallback = object : IEvdevCallback.Stub() { + override fun onEvdevEvent( + deviceId: Int, + timeSec: Long, + timeUsec: Long, + type: Int, + code: Int, + value: Int, + androidCode: Int + ) { + Timber.d( + "Evdev event: deviceId=${deviceId}, timeSec=$timeSec, timeUsec=$timeUsec, " + + "type=$type, code=$code, value=$value, androidCode=$androidCode" + ) + } + } fun pingBinder(): Boolean { synchronized(systemBridgeLock) { @@ -45,6 +61,7 @@ class SystemBridgeManagerImpl @Inject constructor( fun onBinderReceived(binder: IBinder) { synchronized(systemBridgeLock) { this.systemBridge = ISystemBridge.Stub.asInterface(binder) + systemBridge?.registerCallback(evdevCallback) } // coroutineScope.launch(Dispatchers.Main) { @@ -53,45 +70,40 @@ class SystemBridgeManagerImpl @Inject constructor( } private fun grabAllDevices() { - for (deviceId in inputManager.inputDeviceIds) { - val device = inputManager.getInputDevice(deviceId) ?: continue + synchronized(callbackLock) { + val deviceId = 1 + if (evdevConnections.containsKey(deviceId)) { + removeEvdevConnection(deviceId) + } - if (device.name == "qwerty2") { - Timber.d("Grabbing input device: ${device.name} (${device.id})") + val connection = EvdevConnection(deviceId, evdevCallback) + evdevConnections[deviceId] = connection + evdevCallback.asBinder().linkToDeath(connection, 0) -// coroutineScope.launch(Dispatchers.IO) { - grabInputDevice(device) -// } + return@synchronized evdevCallback + } - // TODO remove - break + for (deviceId in inputManager.inputDeviceIds) { + if (deviceId == -1) { + continue } + + val device = inputManager.getInputDevice(deviceId) ?: continue + + grabInputDevice(device) } } private fun grabInputDevice(inputDevice: InputDevice) { val deviceId = inputDevice.id - val callback: IEvdevCallback = synchronized(callbackLock) { - val callback = object : IEvdevCallback.Stub() { - override fun onEvdevEvent(type: Int, code: Int, value: Int) { - Timber.e("Received evdev event from device ${inputDevice.id} ${inputDevice.name}: type=$type, code=$code, value=$value") - } - } - - if (evdevConnections.containsKey(deviceId)) { - removeEvdevConnection(deviceId) - } + try { + Timber.d("Grabbing input device: ${inputDevice.name} (${inputDevice.id})") - val connection = EvdevConnection(deviceId, callback) - evdevConnections[deviceId] = connection - callback.asBinder().linkToDeath(connection, 0) + this.systemBridge?.grabEvdevDevice(deviceId) - return@synchronized callback - } + Timber.d("Grabbed input device: ${inputDevice.name} (${inputDevice.id})") - try { - this.systemBridge?.grabEvdevDevice(deviceId, callback) } catch (e: Exception) { Timber.e("Error grabbing input device: ${e.toString()}") diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 504b2d7df6..327f3ec0fa 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -19,6 +19,10 @@ import io.github.sds100.keymapper.sysbridge.provider.BinderContainer import io.github.sds100.keymapper.sysbridge.provider.SystemBridgeBinderProvider import io.github.sds100.keymapper.sysbridge.utils.IContentProviderUtils import io.github.sds100.keymapper.sysbridge.utils.InputDeviceIdentifier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import rikka.hidden.compat.ActivityManagerApis import rikka.hidden.compat.DeviceIdleControllerApis import rikka.hidden.compat.UserManagerApis @@ -143,6 +147,8 @@ internal class SystemBridge : ISystemBridge.Stub() { } private val inputManager: IInputManager + private var callback: IEvdevCallback? = null + private val coroutineScope: CoroutineScope = MainScope() init { @SuppressLint("UnsafeDynamicallyLoadedCode") @@ -188,9 +194,12 @@ internal class SystemBridge : ISystemBridge.Stub() { exitProcess(0) } + override fun registerCallback(callback: IEvdevCallback?) { + this.callback = callback + } + override fun grabEvdevDevice( deviceId: Int, - callback: IEvdevCallback? ): Boolean { if (callback == null) { return false @@ -199,6 +208,7 @@ internal class SystemBridge : ISystemBridge.Stub() { val inputDevice = inputManager.getInputDevice(deviceId) val deviceIdentifier = InputDeviceIdentifier( + id = deviceId, name = inputDevice.name, vendor = inputDevice.vendorId, product = inputDevice.productId, @@ -207,7 +217,12 @@ internal class SystemBridge : ISystemBridge.Stub() { bluetoothAddress = inputDevice.getBluetoothAddress() ) - return grabEvdevDevice(deviceIdentifier, callback.asBinder()) + Log.e(TAG, "THREAD = ${Thread.currentThread().name}") + coroutineScope.launch(Dispatchers.Unconfined) { + grabEvdevDevice(deviceIdentifier, callback!!.asBinder()) + } + + return true } override fun injectEvent(event: InputEvent?, mode: Int): Boolean { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt index 97dc872dae..286f0648f6 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt @@ -5,6 +5,7 @@ import kotlinx.parcelize.Parcelize @Parcelize internal data class InputDeviceIdentifier( + val id: Int, val name: String, val bus: Int, val vendor: Int, From e3f4fdd6a3e38bb8e7b6e371316944d98e25b056 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 28 Jul 2025 16:35:31 -0600 Subject: [PATCH 045/215] #1394 commit generated native AIDL interfaces without formatting --- .../keymapper/sysbridge/BnEvdevCallback.h | 7 +-- .../keymapper/sysbridge/BpEvdevCallback.h | 4 +- .../keymapper/sysbridge/IEvdevCallback.cpp | 60 ++++++++----------- .../keymapper/sysbridge/IEvdevCallback.h | 9 +-- 4 files changed, 29 insertions(+), 51 deletions(-) diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index 13a164bbff..da05ac7c42 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -34,11 +34,8 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { explicit IEvdevCallbackDelegator(const std::shared_ptr &impl) : _impl(impl) { } - ::ndk::ScopedAStatus - onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, - int32_t in_code, int32_t in_value, int32_t in_androidCode) override { - return _impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, - in_androidCode); + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override { + return _impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode); } protected: private: diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index b301ed1f33..eab4c785dc 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -19,9 +19,7 @@ class BpEvdevCallback : public ::ndk::BpCInterface { explicit BpEvdevCallback(const ::ndk::SpAIBinder& binder); virtual ~BpEvdevCallback(); - ::ndk::ScopedAStatus - onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, - int32_t in_code, int32_t in_value, int32_t in_androidCode) override; + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override; }; } // namespace sysbridge } // namespace keymapper diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index 4313d00b02..7c47dd968a 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -21,22 +21,22 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback std::shared_ptr _aidl_impl = std::static_pointer_cast(::ndk::ICInterface::asInterface(_aidl_binder)); switch (_aidl_code) { case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/): { - int32_t in_deviceId; - int64_t in_timeSec; - int64_t in_timeUsec; + int32_t in_deviceId; + int64_t in_timeSec; + int64_t in_timeUsec; int32_t in_type; int32_t in_code; int32_t in_value; - int32_t in_androidCode; + int32_t in_androidCode; - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_deviceId); - if (_aidl_ret_status != STATUS_OK) break; + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_deviceId); + if (_aidl_ret_status != STATUS_OK) break; - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeSec); - if (_aidl_ret_status != STATUS_OK) break; + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeSec); + if (_aidl_ret_status != STATUS_OK) break; - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeUsec); - if (_aidl_ret_status != STATUS_OK) break; + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeUsec); + if (_aidl_ret_status != STATUS_OK) break; _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_type); if (_aidl_ret_status != STATUS_OK) break; @@ -47,12 +47,10 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_value); if (_aidl_ret_status != STATUS_OK) break; - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_androidCode); - if (_aidl_ret_status != STATUS_OK) break; + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_androidCode); + if (_aidl_ret_status != STATUS_OK) break; - ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_deviceId, in_timeSec, - in_timeUsec, in_type, in_code, - in_value, in_androidCode); + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode); _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); if (_aidl_ret_status != STATUS_OK) break; @@ -69,10 +67,7 @@ static AIBinder_Class* _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallba BpEvdevCallback::BpEvdevCallback(const ::ndk::SpAIBinder& binder) : BpCInterface(binder) {} BpEvdevCallback::~BpEvdevCallback() {} - ::ndk::ScopedAStatus - BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, - int32_t in_type, int32_t in_code, int32_t in_value, - int32_t in_androidCode) { +::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) { binder_status_t _aidl_ret_status = STATUS_OK; ::ndk::ScopedAStatus _aidl_status; ::ndk::ScopedAParcel _aidl_in; @@ -81,14 +76,14 @@ BpEvdevCallback::~BpEvdevCallback() {} _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_deviceId); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_deviceId); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeSec); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeSec); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeUsec); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeUsec); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_type); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; @@ -99,8 +94,8 @@ BpEvdevCallback::~BpEvdevCallback() {} _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_value); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_androidCode); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_androidCode); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; _aidl_ret_status = AIBinder_transact( asBinder().get(), @@ -113,9 +108,7 @@ BpEvdevCallback::~BpEvdevCallback() {} #endif // BINDER_STABILITY_SUPPORT ); if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { - _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_deviceId, in_timeSec, - in_timeUsec, in_type, in_code, - in_value, in_androidCode); + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode); goto _aidl_status_return; } if (_aidl_ret_status != STATUS_OK) goto _aidl_error; @@ -188,12 +181,7 @@ const std::shared_ptr& IEvdevCallback::getDefaultImpl() { return IEvdevCallback::default_impl; } std::shared_ptr IEvdevCallback::default_impl = nullptr; - - ::ndk::ScopedAStatus - IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_deviceId*/, int64_t /*in_timeSec*/, - int64_t /*in_timeUsec*/, int32_t /*in_type*/, - int32_t /*in_code*/, int32_t /*in_value*/, - int32_t /*in_androidCode*/) { +::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_deviceId*/, int64_t /*in_timeSec*/, int64_t /*in_timeUsec*/, int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/, int32_t /*in_androidCode*/) { ::ndk::ScopedAStatus _aidl_status; _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index b3a4da8d85..b93b675b85 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -36,18 +36,13 @@ class IEvdevCallback : public ::ndk::ICInterface { static binder_status_t readFromParcel(const AParcel* parcel, std::shared_ptr* instance); static bool setDefaultImpl(const std::shared_ptr& impl); static const std::shared_ptr& getDefaultImpl(); - - virtual ::ndk::ScopedAStatus - onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, - int32_t in_code, int32_t in_value, int32_t in_androidCode) = 0; + virtual ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) = 0; private: static std::shared_ptr default_impl; }; class IEvdevCallbackDefault : public IEvdevCallback { public: - ::ndk::ScopedAStatus - onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, - int32_t in_code, int32_t in_value, int32_t in_androidCode) override; + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; }; From 1d4c90e073ac1bb5d5cdc10725b809df1beedf7b Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 29 Jul 2025 18:17:10 -0600 Subject: [PATCH 046/215] #1394 use epoll to listen to input events --- sysbridge/src/main/cpp/libevdev_jni.cpp | 50 +++++++++++++++++++------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index f362c61969..3bb462dee9 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -12,6 +12,7 @@ #include "android/input/InputDevice.h" #include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" #include +#include using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; @@ -182,22 +183,47 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J LOGD("Grabbed evdev device %s", libevdev_get_name(dev)); - struct input_event ev{}; + struct input_event inputEvent{}; - do { - rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); + int epollFd = epoll_create1(EPOLL_CLOEXEC); + struct epoll_event epollEvent{}; + epollEvent.events = EPOLLIN | EPOLLET; - if (rc == 0) { - int32_t outKeycode = -1; - uint32_t outFlags = -1; - keyLayoutMap->mapKey(ev.code, 0, &outKeycode, &outFlags); + epoll_ctl(epollFd, EPOLL_CTL_ADD, libevdev_get_fd(dev), &epollEvent); - callback->onEvdevEvent(deviceId, ev.time.tv_sec, ev.time.tv_usec, ev.type, ev.code, - ev.value, - outKeycode); - } + int MAX_EVENTS = 1; + + while (true) { + struct epoll_event events[MAX_EVENTS]; + rc = epoll_wait(epollFd, events, MAX_EVENTS, -1); - } while (rc == 1 || rc == 0 || rc == -EAGAIN); + if (rc == -1) { + // Error + LOGE("epoll_wait error %s", strerror(errno)); + continue; + } else if (rc == 0) { + // timeout + continue; + } else { + // the number of ready file descriptors + rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); + + if (rc == 0) { + int32_t outKeycode = -1; + uint32_t outFlags = -1; + keyLayoutMap->mapKey(inputEvent.code, 0, &outKeycode, &outFlags); + + callback->onEvdevEvent(deviceId, inputEvent.time.tv_sec, inputEvent.time.tv_usec, + inputEvent.type, inputEvent.code, + inputEvent.value, + outKeycode); + } + + if (rc != 1 && rc != 0 && rc != -EAGAIN) { + break; + } + } + } libevdev_grab(dev, LIBEVDEV_UNGRAB); libevdev_free(dev); From ba755736cdbbe0fcbeead51a4050be90db2759a7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 30 Jul 2025 15:51:40 -0600 Subject: [PATCH 047/215] #1394 WIP and not working: communicate with epoll between threads --- sysbridge/src/main/cpp/libevdev_jni.cpp | 274 +++++++++++++++--- .../sysbridge/manager/SystemBridgeManager.kt | 2 +- .../sysbridge/service/SystemBridge.kt | 25 +- 3 files changed, 240 insertions(+), 61 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 3bb462dee9..3423daf3a7 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -13,10 +13,25 @@ #include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" #include #include +#include using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; -static int findInputDevice( +struct DeviceContext { + int deviceId; + struct android::InputDeviceIdentifier inputDeviceIdentifier; + struct libevdev *evdev; + struct android::KeyLayoutMap keyLayoutMap; +}; + +static int epollFd = -1; +static std::mutex epollMutex; + +// This maps the file descriptor of an evdev device to its context. +static std::map *evdevDevices = new std::map(); +static std::mutex evdevDevicesMutex; + +static int findEvdevDevice( char *name, int bus, int vendor, @@ -53,8 +68,7 @@ static int findInputDevice( continue; } - struct libevdev *dev = nullptr; - int status = libevdev_new_from_fd(fd, &dev); + int status = libevdev_new_from_fd(fd, outDev); if (status != 0) { LOGE("Failed to open libevdev device from path %s: %s", fullPath, strerror(errno)); @@ -62,10 +76,10 @@ static int findInputDevice( continue; } - const char *devName = libevdev_get_name(dev); - int devVendor = libevdev_get_id_vendor(dev); - int devProduct = libevdev_get_id_product(dev); - int devBus = libevdev_get_id_bustype(dev); + const char *devName = libevdev_get_name(*outDev); + int devVendor = libevdev_get_id_vendor(*outDev); + int devProduct = libevdev_get_id_product(*outDev); + int devBus = libevdev_get_id_bustype(*outDev); // LOGD("Checking device: %s, bus: %d, vendor: %d, product: %d", // devName, devBus, devVendor, devProduct); @@ -75,13 +89,11 @@ static int findInputDevice( devProduct != product || devBus != bus) { - libevdev_free(dev); - close(fd); + libevdev_free(*outDev); // libevdev_free also closes the fd continue; } closedir(dir); - *outDev = dev; // LOGD("Found input device %s", name); return 0; @@ -125,8 +137,8 @@ extern "C" JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(JNIEnv *env, jobject thiz, - jobject jInputDeviceIdentifier, - jobject jCallbackBinder) { + jobject jInputDeviceIdentifier) { + LOGD("Start gravEvdevDevice"); jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); jfieldID idFieldId = env->GetFieldID(inputDeviceIdentifierClass, "id", "I"); @@ -137,7 +149,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J struct libevdev *dev = nullptr; - int rc = findInputDevice(deviceIdentifier.name.data(), + int rc = findEvdevDevice(deviceIdentifier.name.data(), deviceIdentifier.bus, deviceIdentifier.vendor, deviceIdentifier.product, @@ -147,13 +159,6 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J return false; } - AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); - - // Create a "strong pointer" to the callback binder. - const ::ndk::SpAIBinder spBinder(callbackAIBinder); - - std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); - std::string keyLayoutMapPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( deviceIdentifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); @@ -168,34 +173,145 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J LOGE("Failed to load key layout map for device %s: %d %s", deviceIdentifier.name.c_str(), error.code().value(), error.message().c_str()); + int fd = libevdev_get_fd(dev); + libevdev_free(dev); + close(fd); return false; } const auto &keyLayoutMap = keyLayoutResult.value(); - rc = libevdev_grab(dev, LIBEVDEV_GRAB); + int originalFd = libevdev_get_fd(dev); + int newFd = dup(originalFd); + + if (newFd == -1) { + LOGE("Failed to duplicate file descriptor: %s", strerror(errno)); + libevdev_free(dev); // This also closes originalFd + return false; + } + + libevdev_free(dev); // Free the original device context, which also closes originalFd. + + struct libevdev *newDev = nullptr; + rc = libevdev_new_from_fd(newFd, &newDev); + if (rc < 0) { + LOGE("Failed to create new libevdev device from duplicated fd: %s", strerror(-rc)); + close(newFd); + return false; + } + + rc = libevdev_grab(newDev, LIBEVDEV_GRAB); if (rc < 0) { LOGE("Failed to grab evdev device %s: %s", - libevdev_get_name(dev), strerror(-rc)); + libevdev_get_name(newDev), strerror(-rc)); + libevdev_free(newDev); // This also closes newFd return false; } - LOGD("Grabbed evdev device %s", libevdev_get_name(dev)); + int evdevFd = libevdev_get_fd(newDev); + std::lock_guard epollLock(epollMutex); + + struct epoll_event epollEvent{}; + epollEvent.events = EPOLLIN; + + rc = epoll_ctl(epollFd, EPOLL_CTL_ADD, evdevFd, &epollEvent); + + if (rc == -1) { + LOGE("Error adding device to epoll: %s", strerror(errno)); + libevdev_free(newDev); + return false; + } + + std::lock_guard evdevLock(evdevDevicesMutex); + + if (evdevDevices->contains(evdevFd)) { + LOGE("This evdev device is already being listened to. Ungrab it first"); + libevdev_free(newDev); + return false; + } else { + DeviceContext deviceContext = DeviceContext{ + deviceId, + deviceIdentifier, + newDev, + *keyLayoutMap + }; + + evdevDevices->insert_or_assign(evdevFd, deviceContext); + } + + LOGD("Grabbed evdev device %s", libevdev_get_name(newDev)); + + return true; +} + + +int onEpollEvent(DeviceContext *deviceContext, std::shared_ptr callback) { struct input_event inputEvent{}; - int epollFd = epoll_create1(EPOLL_CLOEXEC); + // the number of ready file descriptors + int rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); + + if (rc == 0) { + int32_t outKeycode = -1; + uint32_t outFlags = -1; + deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); + + callback->onEvdevEvent(deviceContext->deviceId, inputEvent.time.tv_sec, + inputEvent.time.tv_usec, + inputEvent.type, inputEvent.code, + inputEvent.value, + outKeycode); + } + + if (rc == 1 || rc == 0 || rc == -EAGAIN) { + return 0; + } else { + return rc; + } +} + +// Set this to some upper limit. It is unlikely that Key Mapper will be polling +// more than a few evdev devices at once. +static int MAX_EPOLL_EVENTS = 100; + +extern "C" +JNIEXPORT void JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, + jobject thiz, + jobject jCallbackBinder) { + AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); + + // Create a "strong pointer" to the callback binder. + const ::ndk::SpAIBinder spBinder(callbackAIBinder); + + std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); + + std::unique_lock epollLock(epollMutex); + + epollFd = epoll_create1(EPOLL_CLOEXEC); + + if (epollFd == -1) { + LOGE("Error creating epoll file descriptor: %s", strerror(errno)); + + epollLock.unlock(); + return; + } + struct epoll_event epollEvent{}; - epollEvent.events = EPOLLIN | EPOLLET; + epollEvent.events = EPOLLIN; + + epollLock.unlock(); - epoll_ctl(epollFd, EPOLL_CTL_ADD, libevdev_get_fd(dev), &epollEvent); + LOGD("Starting evdev event loop"); - int MAX_EVENTS = 1; + struct epoll_event events[MAX_EPOLL_EVENTS]; while (true) { - struct epoll_event events[MAX_EVENTS]; - rc = epoll_wait(epollFd, events, MAX_EVENTS, -1); + LOGD("epoll_wait"); + int rc = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); + LOGD("Post epoll wait %d", rc); if (rc == -1) { // Error @@ -205,31 +321,93 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J // timeout continue; } else { - // the number of ready file descriptors - rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); - - if (rc == 0) { - int32_t outKeycode = -1; - uint32_t outFlags = -1; - keyLayoutMap->mapKey(inputEvent.code, 0, &outKeycode, &outFlags); - - callback->onEvdevEvent(deviceId, inputEvent.time.tv_sec, inputEvent.time.tv_usec, - inputEvent.type, inputEvent.code, - inputEvent.value, - outKeycode); - } + std::lock_guard evdevDevicesLock(evdevDevicesMutex); + + for (int i = 0; i < MAX_EPOLL_EVENTS; i++) { + epoll_event ev = events[i]; + int epollDataFd = ev.data.fd; + + if (!evdevDevices->contains(epollDataFd)) { + // Stop polling this file descriptor since it is not in the list of evdev devices + // to listen to. + epoll_ctl(epollFd, EPOLL_CTL_DEL, epollDataFd, &epollEvent); + continue; + } - if (rc != 1 && rc != 0 && rc != -EAGAIN) { - break; + DeviceContext *device = &(evdevDevices->at(epollDataFd)); + + rc = onEpollEvent(device, callback); + // TODO handle evdevevent errors } + } } +} - libevdev_grab(dev, LIBEVDEV_UNGRAB); - libevdev_free(dev); +extern "C" +JNIEXPORT void JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDevice(JNIEnv *env, + jobject thiz, + jint device_id) { + + std::lock_guard evdevLock(evdevDevicesMutex); + + // Find the device by device_id + for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { + if (it->second.deviceId == device_id) { + int evdevFd = it->first; + DeviceContext *deviceContext = &(it->second); + + // Ungrab the device + libevdev_grab(deviceContext->evdev, LIBEVDEV_UNGRAB); + + // Remove from epoll + std::lock_guard epollLock(epollMutex); + if (epollFd != -1) { + epoll_ctl(epollFd, EPOLL_CTL_DEL, evdevFd, nullptr); + } - return true; + // Free the libevdev device + libevdev_free(deviceContext->evdev); + + // Remove from our map + evdevDevices->erase(it); + + LOGD("Ungrabbed evdev device with id %d", device_id); + return; + } + } + + LOGE("Device with id %d not found for ungrab", device_id); } -using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; +extern "C" +JNIEXPORT void JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv *env, + jobject thiz) { + std::lock_guard evdevLock(evdevDevicesMutex); + + // Clean up all devices + for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { + DeviceContext *deviceContext = &(it->second); + + // Ungrab the device + libevdev_grab(deviceContext->evdev, LIBEVDEV_UNGRAB); + + // Free the libevdev device + libevdev_free(deviceContext->evdev); + } + + // Clear the map + evdevDevices->clear(); + + // Close epoll file descriptor + std::lock_guard epollLock(epollMutex); + if (epollFd != -1) { + close(epollFd); + epollFd = -1; + } + + LOGD("Stopped evdev event loop and cleaned up all devices"); +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 9efb791ab5..a578ddc1cb 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -83,7 +83,7 @@ class SystemBridgeManagerImpl @Inject constructor( return@synchronized evdevCallback } - for (deviceId in inputManager.inputDeviceIds) { + for (deviceId in inputManager.inputDeviceIds.take(2)) { if (deviceId == -1) { continue } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 327f3ec0fa..3b93dfc927 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -36,10 +36,14 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. external fun grabEvdevDevice( - deviceIdentifier: InputDeviceIdentifier, - callbackBinder: IBinder + deviceIdentifier: InputDeviceIdentifier ): Boolean + external fun ungrabEvdevDevice(deviceId: Int) + + external fun startEvdevEventLoop(callback: IEvdevCallback) + external fun stopEvdevEventLoop() + companion object { private const val TAG: String = "SystemBridge" @@ -147,7 +151,6 @@ internal class SystemBridge : ISystemBridge.Stub() { } private val inputManager: IInputManager - private var callback: IEvdevCallback? = null private val coroutineScope: CoroutineScope = MainScope() init { @@ -195,16 +198,16 @@ internal class SystemBridge : ISystemBridge.Stub() { } override fun registerCallback(callback: IEvdevCallback?) { - this.callback = callback + callback ?: return + + coroutineScope.launch(Dispatchers.IO) { + startEvdevEventLoop(callback) + } } override fun grabEvdevDevice( deviceId: Int, ): Boolean { - if (callback == null) { - return false - } - val inputDevice = inputManager.getInputDevice(deviceId) val deviceIdentifier = InputDeviceIdentifier( @@ -217,10 +220,8 @@ internal class SystemBridge : ISystemBridge.Stub() { bluetoothAddress = inputDevice.getBluetoothAddress() ) - Log.e(TAG, "THREAD = ${Thread.currentThread().name}") - coroutineScope.launch(Dispatchers.Unconfined) { - grabEvdevDevice(deviceIdentifier, callback!!.asBinder()) - } + val grabResult = grabEvdevDevice(deviceIdentifier) + Log.e(TAG, "Grabbed $deviceId with result $grabResult") return true } From 4bee3c6c8e997d41cd5d89a167d2924f8eb8cd85 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 30 Jul 2025 16:02:41 -0600 Subject: [PATCH 048/215] #1394 WIP use eventfd --- sysbridge/src/main/cpp/libevdev_jni.cpp | 385 ++++++++++++------------ 1 file changed, 189 insertions(+), 196 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 3423daf3a7..a0a80f1e88 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -14,9 +14,32 @@ #include #include #include +#include +#include +#include using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; +struct GrabData { + int deviceId; + android::InputDeviceIdentifier identifier; +}; + +struct UngrabData { + int deviceId; +}; + +enum CommandType { + GRAB, + UNGRAB, + STOP +}; + +struct Command { + CommandType type; + std::variant data; +}; + struct DeviceContext { int deviceId; struct android::InputDeviceIdentifier inputDeviceIdentifier; @@ -25,14 +48,18 @@ struct DeviceContext { }; static int epollFd = -1; +static int commandEventFd = -1; static std::mutex epollMutex; +static std::queue commandQueue; +static std::mutex commandMutex; + // This maps the file descriptor of an evdev device to its context. static std::map *evdevDevices = new std::map(); static std::mutex evdevDevicesMutex; static int findEvdevDevice( - char *name, + std::string name, int bus, int vendor, int product, @@ -84,7 +111,7 @@ static int findEvdevDevice( // LOGD("Checking device: %s, bus: %d, vendor: %d, product: %d", // devName, devBus, devVendor, devProduct); - if (strcmp(devName, name) != 0 || + if (name.compare(devName) != 0 || devVendor != vendor || devProduct != product || devBus != bus) { @@ -101,7 +128,8 @@ static int findEvdevDevice( closedir(dir); - LOGE("Input device not found with name: %s, bus: %d, vendor: %d, product: %d", name, bus, + LOGE("Input device not found with name: %s, bus: %d, vendor: %d, product: %d", name.c_str(), + bus, vendor, product); return -1; @@ -133,116 +161,37 @@ convertJInputDeviceIdentifier(JNIEnv *env, jobject jInputDeviceIdentifier) { return deviceIdentifier; } +jint JNI_OnLoad(JavaVM *vm, void *reserved) { + evdevDevices = new std::map(); + return JNI_VERSION_1_6; +} + extern "C" JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(JNIEnv *env, jobject thiz, jobject jInputDeviceIdentifier) { - - LOGD("Start gravEvdevDevice"); jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); jfieldID idFieldId = env->GetFieldID(inputDeviceIdentifierClass, "id", "I"); int deviceId = env->GetIntField(jInputDeviceIdentifier, idFieldId); + android::InputDeviceIdentifier identifier = convertJInputDeviceIdentifier(env, + jInputDeviceIdentifier); - android::InputDeviceIdentifier deviceIdentifier = convertJInputDeviceIdentifier(env, - jInputDeviceIdentifier); - - struct libevdev *dev = nullptr; - - int rc = findEvdevDevice(deviceIdentifier.name.data(), - deviceIdentifier.bus, - deviceIdentifier.vendor, - deviceIdentifier.product, - &dev); - - if (rc < 0) { - return false; - } - - std::string keyLayoutMapPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( - deviceIdentifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); - - LOGD("Key layout path for device %s = %s", deviceIdentifier.name.c_str(), - keyLayoutMapPath.c_str()); - - auto keyLayoutResult = android::KeyLayoutMap::load(keyLayoutMapPath, nullptr); - - if (!keyLayoutResult.ok()) { - const auto &error = keyLayoutResult.error(); - - LOGE("Failed to load key layout map for device %s: %d %s", - deviceIdentifier.name.c_str(), error.code().value(), error.message().c_str()); - - int fd = libevdev_get_fd(dev); - libevdev_free(dev); - close(fd); - return false; - } - - const auto &keyLayoutMap = keyLayoutResult.value(); - - int originalFd = libevdev_get_fd(dev); - int newFd = dup(originalFd); + Command cmd; + cmd.type = GRAB; + cmd.data = GrabData{deviceId, identifier}; - if (newFd == -1) { - LOGE("Failed to duplicate file descriptor: %s", strerror(errno)); - libevdev_free(dev); // This also closes originalFd - return false; - } - - libevdev_free(dev); // Free the original device context, which also closes originalFd. - - struct libevdev *newDev = nullptr; - rc = libevdev_new_from_fd(newFd, &newDev); - if (rc < 0) { - LOGE("Failed to create new libevdev device from duplicated fd: %s", strerror(-rc)); - close(newFd); - return false; - } + std::lock_guard lock(commandMutex); + commandQueue.push(cmd); - rc = libevdev_grab(newDev, LIBEVDEV_GRAB); - - if (rc < 0) { - LOGE("Failed to grab evdev device %s: %s", - libevdev_get_name(newDev), strerror(-rc)); - libevdev_free(newDev); // This also closes newFd + // Notify the event loop + uint64_t val = 1; + ssize_t written = write(commandEventFd, &val, sizeof(val)); + if (written < 0) { + LOGE("Failed to write to commandEventFd: %s", strerror(errno)); return false; } - int evdevFd = libevdev_get_fd(newDev); - - std::lock_guard epollLock(epollMutex); - - struct epoll_event epollEvent{}; - epollEvent.events = EPOLLIN; - - rc = epoll_ctl(epollFd, EPOLL_CTL_ADD, evdevFd, &epollEvent); - - if (rc == -1) { - LOGE("Error adding device to epoll: %s", strerror(errno)); - libevdev_free(newDev); - return false; - } - - std::lock_guard evdevLock(evdevDevicesMutex); - - if (evdevDevices->contains(evdevFd)) { - LOGE("This evdev device is already being listened to. Ungrab it first"); - libevdev_free(newDev); - return false; - } else { - DeviceContext deviceContext = DeviceContext{ - deviceId, - deviceIdentifier, - newDev, - *keyLayoutMap - }; - - evdevDevices->insert_or_assign(evdevFd, deviceContext); - } - - LOGD("Grabbed evdev device %s", libevdev_get_name(newDev)); - return true; } @@ -276,72 +225,145 @@ int onEpollEvent(DeviceContext *deviceContext, std::shared_ptr c // more than a few evdev devices at once. static int MAX_EPOLL_EVENTS = 100; +void handleCommand(const Command &cmd) { + if (cmd.type == GRAB) { + const GrabData &data = std::get(cmd.data); + LOGD("Executing GRAB command for deviceId: %d", data.deviceId); + + struct libevdev *dev = nullptr; + int rc = findEvdevDevice(data.identifier.name, + data.identifier.bus, + data.identifier.vendor, + data.identifier.product, + &dev); + if (rc < 0) { + LOGE("Failed to find device for grab command"); + return; + } + + rc = libevdev_grab(dev, LIBEVDEV_GRAB); + if (rc < 0) { + LOGE("Failed to grab evdev device %s: %s", + libevdev_get_name(dev), strerror(-rc)); + libevdev_free(dev); + return; + } + + int evdevFd = libevdev_get_fd(dev); + std::string klPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( + data.identifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); + auto klResult = android::KeyLayoutMap::load(klPath, nullptr); + + // TODO check result is ok + + DeviceContext context{ + data.deviceId, + data.identifier, + dev, + *klResult.value() + }; + + struct epoll_event event; + event.events = EPOLLIN; + event.data.fd = evdevFd; + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, evdevFd, &event) == -1) { + LOGE("Failed to add new device to epoll: %s", strerror(errno)); + libevdev_free(dev); + return; + } + + std::lock_guard lock(evdevDevicesMutex); + evdevDevices->insert_or_assign(evdevFd, context); + + } else if (cmd.type == UNGRAB) { + const UngrabData &data = std::get(cmd.data); + LOGD("Executing UNGRAB command for deviceId: %d", data.deviceId); + + std::lock_guard lock(evdevDevicesMutex); + for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { + if (it->second.deviceId == data.deviceId) { + int fd = it->first; + epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr); + libevdev_grab(it->second.evdev, LIBEVDEV_UNGRAB); + libevdev_free(it->second.evdev); + evdevDevices->erase(it); + break; + } + } + } +} + extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, jobject thiz, jobject jCallbackBinder) { - AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); - - // Create a "strong pointer" to the callback binder. - const ::ndk::SpAIBinder spBinder(callbackAIBinder); - - std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); - - std::unique_lock epollLock(epollMutex); - epollFd = epoll_create1(EPOLL_CLOEXEC); - if (epollFd == -1) { - LOGE("Error creating epoll file descriptor: %s", strerror(errno)); - - epollLock.unlock(); + LOGE("Failed to create epoll fd: %s", strerror(errno)); return; } - struct epoll_event epollEvent{}; - epollEvent.events = EPOLLIN; + commandEventFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (commandEventFd == -1) { + LOGE("Failed to create command eventfd: %s", strerror(errno)); + close(epollFd); + return; + } - epollLock.unlock(); + struct epoll_event event; + event.events = EPOLLIN; + event.data.fd = commandEventFd; + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, commandEventFd, &event) == -1) { + LOGE("Failed to add command eventfd to epoll: %s", strerror(errno)); + close(epollFd); + close(commandEventFd); + return; + } - LOGD("Starting evdev event loop"); + AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); + const ::ndk::SpAIBinder spBinder(callbackAIBinder); + std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); struct epoll_event events[MAX_EPOLL_EVENTS]; - - while (true) { - LOGD("epoll_wait"); - int rc = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); - LOGD("Post epoll wait %d", rc); - - if (rc == -1) { - // Error - LOGE("epoll_wait error %s", strerror(errno)); - continue; - } else if (rc == 0) { - // timeout - continue; - } else { - std::lock_guard evdevDevicesLock(evdevDevicesMutex); - - for (int i = 0; i < MAX_EPOLL_EVENTS; i++) { - epoll_event ev = events[i]; - int epollDataFd = ev.data.fd; - - if (!evdevDevices->contains(epollDataFd)) { - // Stop polling this file descriptor since it is not in the list of evdev devices - // to listen to. - epoll_ctl(epollFd, EPOLL_CTL_DEL, epollDataFd, &epollEvent); - continue; + bool running = true; + while (running) { + int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); + for (int i = 0; i < n; ++i) { + if (events[i].data.fd == commandEventFd) { + uint64_t val; + ssize_t s = read(commandEventFd, &val, sizeof(val)); + if (s < 0) { + LOGE("Error reading from command event fd: %s", strerror(errno)); } - DeviceContext *device = &(evdevDevices->at(epollDataFd)); - - rc = onEpollEvent(device, callback); - // TODO handle evdevevent errors + std::lock_guard lock(commandMutex); + while (!commandQueue.empty()) { + Command cmd = commandQueue.front(); + commandQueue.pop(); + if (cmd.type == STOP) { + running = false; + break; + } + handleCommand(cmd); + } + } else { + std::lock_guard lock(evdevDevicesMutex); + DeviceContext *dc = &evdevDevices->at(events[i].data.fd); + onEpollEvent(dc, callback); } - } } + + // Cleanup + std::lock_guard lock(evdevDevicesMutex); + for (auto const &[fd, dc]: *evdevDevices) { + libevdev_grab(dc.evdev, LIBEVDEV_UNGRAB); + libevdev_free(dc.evdev); + } + evdevDevices->clear(); + close(commandEventFd); + close(epollFd); } extern "C" @@ -349,36 +371,19 @@ JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDevice(JNIEnv *env, jobject thiz, jint device_id) { - - std::lock_guard evdevLock(evdevDevicesMutex); - - // Find the device by device_id - for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { - if (it->second.deviceId == device_id) { - int evdevFd = it->first; - DeviceContext *deviceContext = &(it->second); - - // Ungrab the device - libevdev_grab(deviceContext->evdev, LIBEVDEV_UNGRAB); - - // Remove from epoll - std::lock_guard epollLock(epollMutex); - if (epollFd != -1) { - epoll_ctl(epollFd, EPOLL_CTL_DEL, evdevFd, nullptr); - } - - // Free the libevdev device - libevdev_free(deviceContext->evdev); - - // Remove from our map - evdevDevices->erase(it); - - LOGD("Ungrabbed evdev device with id %d", device_id); - return; - } + Command cmd; + cmd.type = UNGRAB; + cmd.data = UngrabData{device_id}; + + std::lock_guard lock(commandMutex); + commandQueue.push(cmd); + + // Notify the event loop + uint64_t val = 1; + ssize_t written = write(commandEventFd, &val, sizeof(val)); + if (written < 0) { + LOGE("Failed to write to commandEventFd: %s", strerror(errno)); } - - LOGE("Device with id %d not found for ungrab", device_id); } @@ -386,28 +391,16 @@ extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv *env, jobject thiz) { - std::lock_guard evdevLock(evdevDevicesMutex); + Command cmd; + cmd.type = STOP; - // Clean up all devices - for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { - DeviceContext *deviceContext = &(it->second); + std::lock_guard lock(commandMutex); + commandQueue.push(cmd); - // Ungrab the device - libevdev_grab(deviceContext->evdev, LIBEVDEV_UNGRAB); - - // Free the libevdev device - libevdev_free(deviceContext->evdev); + // Notify the event loop + uint64_t val = 1; + ssize_t written = write(commandEventFd, &val, sizeof(val)); + if (written < 0) { + LOGE("Failed to write to commandEventFd: %s", strerror(errno)); } - - // Clear the map - evdevDevices->clear(); - - // Close epoll file descriptor - std::lock_guard epollLock(epollMutex); - if (epollFd != -1) { - close(epollFd); - epollFd = -1; - } - - LOGD("Stopped evdev event loop and cleaned up all devices"); } \ No newline at end of file From d02aa901a441ae8aae23b44e43124ac07ac9bb47 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 2 Aug 2025 06:20:14 +0200 Subject: [PATCH 049/215] #1394 grabbing multiple devices WORKS. Had to initialize my own looper to run the event loop on --- sysbridge/src/main/cpp/libevdev_jni.cpp | 14 ++++++--- .../sysbridge/manager/SystemBridgeManager.kt | 11 ++++--- .../sysbridge/service/SystemBridge.kt | 30 ++++++++++++++----- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index a0a80f1e88..bf7b7c720a 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -196,7 +196,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J } -int onEpollEvent(DeviceContext *deviceContext, std::shared_ptr callback) { +int onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { struct input_event inputEvent{}; // the number of ready file descriptors @@ -205,13 +205,17 @@ int onEpollEvent(DeviceContext *deviceContext, std::shared_ptr c if (rc == 0) { int32_t outKeycode = -1; uint32_t outFlags = -1; + int deviceId = deviceContext->deviceId; deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); - callback->onEvdevEvent(deviceContext->deviceId, inputEvent.time.tv_sec, + callback->onEvdevEvent(deviceId, + inputEvent.time.tv_sec, inputEvent.time.tv_usec, - inputEvent.type, inputEvent.code, + inputEvent.type, + inputEvent.code, inputEvent.value, outKeycode); + LOGE("After"); } if (rc == 1 || rc == 0 || rc == -EAGAIN) { @@ -324,10 +328,12 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); const ::ndk::SpAIBinder spBinder(callbackAIBinder); std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); + LOGE("IS CALLBACK REMOTE %d", callback->isRemote()); struct epoll_event events[MAX_EPOLL_EVENTS]; bool running = true; while (running) { + LOGE("EPOLL WAIT"); int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); for (int i = 0; i < n; ++i) { if (events[i].data.fd == commandEventFd) { @@ -350,7 +356,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo } else { std::lock_guard lock(evdevDevicesMutex); DeviceContext *dc = &evdevDevices->at(events[i].data.fd); - onEpollEvent(dc, callback); + onEpollEvent(dc, callback.get()); } } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index a578ddc1cb..8e8468abe7 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -13,6 +13,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -64,9 +66,10 @@ class SystemBridgeManagerImpl @Inject constructor( systemBridge?.registerCallback(evdevCallback) } -// coroutineScope.launch(Dispatchers.Main) { - grabAllDevices() -// } + coroutineScope.launch { + delay(1000) + grabAllDevices() + } } private fun grabAllDevices() { @@ -83,7 +86,7 @@ class SystemBridgeManagerImpl @Inject constructor( return@synchronized evdevCallback } - for (deviceId in inputManager.inputDeviceIds.take(2)) { + for (deviceId in inputManager.inputDeviceIds) { if (deviceId == -1) { continue } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 3b93dfc927..0ecfbb9848 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -7,7 +7,9 @@ import android.ddm.DdmHandleAppName import android.hardware.input.IInputManager import android.os.Binder import android.os.Bundle +import android.os.Handler import android.os.IBinder +import android.os.Looper import android.os.ServiceManager import android.util.Log import android.view.InputEvent @@ -41,7 +43,7 @@ internal class SystemBridge : ISystemBridge.Stub() { external fun ungrabEvdevDevice(deviceId: Int) - external fun startEvdevEventLoop(callback: IEvdevCallback) + external fun startEvdevEventLoop(callback: IBinder) external fun stopEvdevEventLoop() companion object { @@ -50,7 +52,10 @@ internal class SystemBridge : ISystemBridge.Stub() { @JvmStatic fun main(args: Array) { DdmHandleAppName.setAppName("keymapper_sysbridge", 0) + @Suppress("DEPRECATION") + Looper.prepareMainLooper() SystemBridge() + Looper.loop() } private fun waitSystemService(name: String?) { @@ -152,6 +157,7 @@ internal class SystemBridge : ISystemBridge.Stub() { private val inputManager: IInputManager private val coroutineScope: CoroutineScope = MainScope() + private val mainHandler = Handler(Looper.myLooper()!!) init { @SuppressLint("UnsafeDynamicallyLoadedCode") @@ -184,9 +190,11 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO use the process observer to rebind when key mapper starts - for (userId in UserManagerApis.getUserIdsNoThrow()) { - // TODO use correct package name - sendBinderToApp(this, "io.github.sds100.keymapper.debug", userId) + mainHandler.post { + for (userId in UserManagerApis.getUserIdsNoThrow()) { + // TODO use correct package name + sendBinderToApp(this, "io.github.sds100.keymapper.debug", userId) + } } } @@ -201,7 +209,9 @@ internal class SystemBridge : ISystemBridge.Stub() { callback ?: return coroutineScope.launch(Dispatchers.IO) { - startEvdevEventLoop(callback) + mainHandler.post { + startEvdevEventLoop(callback.asBinder()) + } } } @@ -220,8 +230,14 @@ internal class SystemBridge : ISystemBridge.Stub() { bluetoothAddress = inputDevice.getBluetoothAddress() ) - val grabResult = grabEvdevDevice(deviceIdentifier) - Log.e(TAG, "Grabbed $deviceId with result $grabResult") + // Do heavy work on IO dispatcher + // Post the Binder call to main thread with Looper +// coroutineScope.launch(Dispatchers.IO) { +// mainHandler.post { + + grabEvdevDevice(deviceIdentifier) +// } +// } return true } From 7234bf2fa828f6ef7915407ae56438f294315e3e Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 3 Aug 2025 08:20:18 +0200 Subject: [PATCH 050/215] #1394 clean up evdev code a bit --- sysbridge/src/main/cpp/libevdev_jni.cpp | 41 ++++++++++--------- .../sysbridge/service/SystemBridge.kt | 7 ---- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index bf7b7c720a..e5214e3a75 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -49,7 +49,6 @@ struct DeviceContext { static int epollFd = -1; static int commandEventFd = -1; -static std::mutex epollMutex; static std::queue commandQueue; static std::mutex commandMutex; @@ -108,21 +107,18 @@ static int findEvdevDevice( int devProduct = libevdev_get_id_product(*outDev); int devBus = libevdev_get_id_bustype(*outDev); -// LOGD("Checking device: %s, bus: %d, vendor: %d, product: %d", -// devName, devBus, devVendor, devProduct); - - if (name.compare(devName) != 0 || + if (name != devName || devVendor != vendor || devProduct != product || devBus != bus) { - libevdev_free(*outDev); // libevdev_free also closes the fd + libevdev_free(*outDev); + close(fd); continue; } closedir(dir); -// LOGD("Found input device %s", name); return 0; } @@ -173,20 +169,18 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(J jobject jInputDeviceIdentifier) { jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); jfieldID idFieldId = env->GetFieldID(inputDeviceIdentifierClass, "id", "I"); - int deviceId = env->GetIntField(jInputDeviceIdentifier, idFieldId); android::InputDeviceIdentifier identifier = convertJInputDeviceIdentifier(env, jInputDeviceIdentifier); + int deviceId = env->GetIntField(jInputDeviceIdentifier, idFieldId); - Command cmd; - cmd.type = GRAB; - cmd.data = GrabData{deviceId, identifier}; + Command cmd = {GRAB, GrabData{deviceId, identifier}}; std::lock_guard lock(commandMutex); commandQueue.push(cmd); - // Notify the event loop uint64_t val = 1; ssize_t written = write(commandEventFd, &val, sizeof(val)); + if (written < 0) { LOGE("Failed to write to commandEventFd: %s", strerror(errno)); return false; @@ -215,7 +209,6 @@ int onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { inputEvent.code, inputEvent.value, outKeycode); - LOGE("After"); } if (rc == 1 || rc == 0 || rc == -EAGAIN) { @@ -232,7 +225,6 @@ static int MAX_EPOLL_EVENTS = 100; void handleCommand(const Command &cmd) { if (cmd.type == GRAB) { const GrabData &data = std::get(cmd.data); - LOGD("Executing GRAB command for deviceId: %d", data.deviceId); struct libevdev *dev = nullptr; int rc = findEvdevDevice(data.identifier.name, @@ -258,7 +250,10 @@ void handleCommand(const Command &cmd) { data.identifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); auto klResult = android::KeyLayoutMap::load(klPath, nullptr); - // TODO check result is ok + if (!klResult.ok()) { + LOGE("key layout map not found for device %s", libevdev_get_name(dev)); + return; + } DeviceContext context{ data.deviceId, @@ -267,7 +262,7 @@ void handleCommand(const Command &cmd) { *klResult.value() }; - struct epoll_event event; + struct epoll_event event{}; event.events = EPOLLIN; event.data.fd = evdevFd; if (epoll_ctl(epollFd, EPOLL_CTL_ADD, evdevFd, &event) == -1) { @@ -281,7 +276,6 @@ void handleCommand(const Command &cmd) { } else if (cmd.type == UNGRAB) { const UngrabData &data = std::get(cmd.data); - LOGD("Executing UNGRAB command for deviceId: %d", data.deviceId); std::lock_guard lock(evdevDevicesMutex); for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { @@ -302,6 +296,11 @@ JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, jobject thiz, jobject jCallbackBinder) { + if (epollFd != -1 || commandEventFd != -1) { + LOGE("The evdev event loop has already started."); + return; + } + epollFd = epoll_create1(EPOLL_CLOEXEC); if (epollFd == -1) { LOGE("Failed to create epoll fd: %s", strerror(errno)); @@ -315,7 +314,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo return; } - struct epoll_event event; + struct epoll_event event{}; event.events = EPOLLIN; event.data.fd = commandEventFd; if (epoll_ctl(epollFd, EPOLL_CTL_ADD, commandEventFd, &event) == -1) { @@ -328,13 +327,13 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); const ::ndk::SpAIBinder spBinder(callbackAIBinder); std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); - LOGE("IS CALLBACK REMOTE %d", callback->isRemote()); struct epoll_event events[MAX_EPOLL_EVENTS]; bool running = true; + while (running) { - LOGE("EPOLL WAIT"); int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); + for (int i = 0; i < n; ++i) { if (events[i].data.fd == commandEventFd) { uint64_t val; @@ -363,10 +362,12 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo // Cleanup std::lock_guard lock(evdevDevicesMutex); + for (auto const &[fd, dc]: *evdevDevices) { libevdev_grab(dc.evdev, LIBEVDEV_UNGRAB); libevdev_free(dc.evdev); } + evdevDevices->clear(); close(commandEventFd); close(epollFd); diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 0ecfbb9848..41db87b58f 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -230,14 +230,7 @@ internal class SystemBridge : ISystemBridge.Stub() { bluetoothAddress = inputDevice.getBluetoothAddress() ) - // Do heavy work on IO dispatcher - // Post the Binder call to main thread with Looper -// coroutineScope.launch(Dispatchers.IO) { -// mainHandler.post { - grabEvdevDevice(deviceIdentifier) -// } -// } return true } From 008ac63dc7d203fa59db0fa01b7189cdc7c5fd8a Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 3 Aug 2025 08:34:43 +0200 Subject: [PATCH 051/215] #1394 SystemBridge: call input manager inject input event --- .../github/sds100/keymapper/sysbridge/service/SystemBridge.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 41db87b58f..295238e41a 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -236,6 +236,6 @@ internal class SystemBridge : ISystemBridge.Stub() { } override fun injectEvent(event: InputEvent?, mode: Int): Boolean { - return false + return inputManager.injectInputEvent(event, mode) } } \ No newline at end of file From 11267f9c222be247413ca06c2e983dec5d19c0a2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 4 Aug 2025 18:16:52 +0100 Subject: [PATCH 052/215] refactor: rename RecordTriggerUseCase.kt to RecordTriggerController.kt --- .../sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt | 6 +++--- .../sds100/keymapper/trigger/ConfigTriggerViewModel.kt | 4 ++-- .../io/github/sds100/keymapper/base/BaseMainActivity.kt | 4 ++-- .../github/sds100/keymapper/base/BaseSingletonHiltModule.kt | 4 ++-- .../keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt | 4 ++-- .../keymapper/base/trigger/BaseConfigTriggerViewModel.kt | 2 +- .../{RecordTriggerUseCase.kt => RecordTriggerController.kt} | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) rename base/src/main/java/io/github/sds100/keymapper/base/trigger/{RecordTriggerUseCase.kt => RecordTriggerController.kt} (97%) diff --git a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt index 046f5cee1c..5f3c3e5473 100644 --- a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt @@ -13,7 +13,7 @@ import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.PurchasingManager -import io.github.sds100.keymapper.base.trigger.RecordTriggerUseCase +import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider @@ -28,7 +28,7 @@ class ConfigKeyMapViewModel @Inject constructor( onboarding: OnboardingUseCase, createActionUseCase: CreateActionUseCase, testActionUseCase: TestActionUseCase, - recordTriggerUseCase: RecordTriggerUseCase, + recordTriggerController: RecordTriggerController, createKeyMapShortcutUseCase: CreateKeyMapShortcutUseCase, purchasingManager: PurchasingManager, setupGuiKeyboardUseCase: SetupGuiKeyboardUseCase, @@ -58,7 +58,7 @@ class ConfigKeyMapViewModel @Inject constructor( coroutineScope = viewModelScope, onboarding = onboarding, config = config, - recordTrigger = recordTriggerUseCase, + recordTrigger = recordTriggerController, createKeyMapShortcut = createKeyMapShortcutUseCase, displayKeyMap = display, purchasingManager = purchasingManager, diff --git a/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt index 1654023097..80370bb382 100644 --- a/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt @@ -7,7 +7,7 @@ import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCa import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.PurchasingManager import io.github.sds100.keymapper.base.trigger.BaseConfigTriggerViewModel -import io.github.sds100.keymapper.base.trigger.RecordTriggerUseCase +import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider @@ -19,7 +19,7 @@ class ConfigTriggerViewModel @Inject constructor( private val coroutineScope: CoroutineScope, private val onboarding: OnboardingUseCase, private val config: ConfigKeyMapUseCase, - private val recordTrigger: RecordTriggerUseCase, + private val recordTrigger: RecordTriggerController, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val displayKeyMap: DisplayKeyMapUseCase, private val purchasingManager: PurchasingManager, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index ec003122d5..352c637ff7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -29,7 +29,7 @@ import io.github.sds100.keymapper.base.compose.ComposeColors import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate -import io.github.sds100.keymapper.base.trigger.RecordTriggerController +import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider @@ -73,7 +73,7 @@ abstract class BaseMainActivity : AppCompatActivity() { lateinit var onboardingUseCase: OnboardingUseCase @Inject - lateinit var recordTriggerController: RecordTriggerController + lateinit var recordTriggerController: RecordTriggerControllerImpl @Inject lateinit var notificationReceiverAdapter: NotificationReceiverAdapterImpl diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt index 55a0ffabfe..6e51de948d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt @@ -35,7 +35,7 @@ import io.github.sds100.keymapper.base.system.notifications.AndroidNotificationA import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsUseCase import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsUseCaseImpl import io.github.sds100.keymapper.base.trigger.RecordTriggerController -import io.github.sds100.keymapper.base.trigger.RecordTriggerUseCase +import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.navigation.NavigationProviderImpl import io.github.sds100.keymapper.base.utils.ui.DialogProvider @@ -105,7 +105,7 @@ abstract class BaseSingletonHiltModule { @Binds @Singleton - abstract fun bindRecordTriggerUseCase(impl: RecordTriggerController): RecordTriggerUseCase + abstract fun bindRecordTriggerUseCase(impl: RecordTriggerControllerImpl): RecordTriggerController @Binds @Singleton diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt index 66b9d37eed..c4fab0338a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt @@ -16,7 +16,7 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate -import io.github.sds100.keymapper.base.trigger.RecordTriggerController +import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider @@ -42,7 +42,7 @@ class CreateKeyMapShortcutActivity : AppCompatActivity() { lateinit var onboardingUseCase: OnboardingUseCase @Inject - lateinit var recordTriggerController: RecordTriggerController + lateinit var recordTriggerController: RecordTriggerControllerImpl @Inject lateinit var notificationReceiverAdapter: NotificationReceiverAdapterImpl diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index bc76465d7c..237c5592d7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -65,7 +65,7 @@ abstract class BaseConfigTriggerViewModel( private val coroutineScope: CoroutineScope, private val onboarding: OnboardingUseCase, private val config: ConfigKeyMapUseCase, - private val recordTrigger: RecordTriggerUseCase, + private val recordTrigger: RecordTriggerController, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val displayKeyMap: DisplayKeyMapUseCase, private val purchasingManager: PurchasingManager, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt similarity index 97% rename from base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt rename to base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 63b667cf36..5a8d4ffebc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -20,10 +20,10 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class RecordTriggerController @Inject constructor( +class RecordTriggerControllerImpl @Inject constructor( private val coroutineScope: CoroutineScope, private val serviceAdapter: AccessibilityServiceAdapter, -) : RecordTriggerUseCase { +) : RecordTriggerController { override val state = MutableStateFlow(RecordTriggerState.Idle) private val recordedKeys: MutableList = mutableListOf() @@ -120,7 +120,7 @@ class RecordTriggerController @Inject constructor( } } -interface RecordTriggerUseCase { +interface RecordTriggerController { val state: Flow val onRecordKey: Flow From 113ad177866ed535a9deecc679f1a6d44192dc0e Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 4 Aug 2025 23:27:56 +0100 Subject: [PATCH 053/215] #1394 create interface for InputEventHub --- .../base/input/InputEventDetectionSource.kt | 7 ++++ .../keymapper/base/input/InputEventHub.kt | 25 +++++++++++++++ .../base/input/InputEventHubCallback.kt | 10 ++++++ .../base/keymaps/ConfigKeyMapUseCase.kt | 6 ++-- .../base/keymaps/KeyMapListItemCreator.kt | 6 ++-- .../keymaps/detection/KeyMapController.kt | 4 +-- .../accessibility/BaseAccessibilityService.kt | 4 +-- .../BaseAccessibilityServiceController.kt | 10 +++--- .../trigger/BaseConfigTriggerViewModel.kt | 5 +-- .../base/trigger/KeyCodeTriggerKey.kt | 9 +++--- .../base/trigger/KeyEventDetectionSource.kt | 6 ---- .../base/trigger/RecordTriggerController.kt | 5 +-- .../base/trigger/RecordTriggerEvent.kt | 3 +- .../keymapper/base/trigger/RecordedKey.kt | 4 ++- .../base/trigger/TriggerErrorSnapshot.kt | 3 +- .../keymapper/base/ConfigKeyMapUseCaseTest.kt | 32 +++++++++---------- .../base/keymaps/KeyMapControllerTest.kt | 16 +++++----- .../keymapper/base/utils/KeyMapUtils.kt | 4 +-- .../sysbridge/manager/SystemBridgeManager.kt | 7 ---- 19 files changed, 101 insertions(+), 65 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventDetectionSource.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt new file mode 100644 index 0000000000..6ca2e953a9 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt @@ -0,0 +1,7 @@ +package io.github.sds100.keymapper.base.input + +enum class InputEventDetectionSource { + ACCESSIBILITY_SERVICE, + INPUT_METHOD, + SYSTEM_BRIDGE +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt new file mode 100644 index 0000000000..92a7276852 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -0,0 +1,25 @@ +package io.github.sds100.keymapper.base.input + +import io.github.sds100.keymapper.system.inputevents.KMInputEvent + +interface InputEventHub { + /** + * Register a client that will receive input events through the [callback]. The same [clientId] + * must be used for any requests to other methods in this class. The input events will either + * come from the key event relay service, accessibility service, or system bridge + * depending on the type of event and Key Mapper's permissions. + */ + fun registerClient(clientId: String, callback: InputEventHubCallback) + fun unregisterClient(clientId: String) + + /** + * Set the devices that a client wants to listen to. + */ + fun setCallbackDevices(clientId: String, deviceIds: Array) + + /** + * Inject an input event. This may either use the key event relay service or the system + * bridge depending on the permissions granted to Key Mapper. + */ + fun injectEvent(event: KMInputEvent) +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt new file mode 100644 index 0000000000..c0f86d205e --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt @@ -0,0 +1,10 @@ +package io.github.sds100.keymapper.base.input + +import io.github.sds100.keymapper.system.inputevents.KMInputEvent + +interface InputEventHubCallback { + /** + * @return whether to consume the event. + */ + fun onInputEvent(event: KMInputEvent, detectionSource: InputEventDetectionSource): Boolean +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt index d9be2cd07b..b796af8003 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt @@ -8,13 +8,13 @@ import io.github.sds100.keymapper.base.constraints.Constraint import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.floating.FloatingButtonEntityMapper +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice @@ -355,7 +355,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( override fun addKeyCodeTriggerKey( keyCode: Int, device: TriggerKeyDevice, - detectionSource: KeyEventDetectionSource, + detectionSource: InputEventDetectionSource, ) = editTrigger { trigger -> val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -996,7 +996,7 @@ interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { fun addKeyCodeTriggerKey( keyCode: Int, device: TriggerKeyDevice, - detectionSource: KeyEventDetectionSource, + detectionSource: InputEventDetectionSource, ) suspend fun addFloatingButtonTriggerKey(buttonUid: String) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt index e94709db7d..4359ee8ea2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt @@ -9,12 +9,12 @@ import io.github.sds100.keymapper.base.actions.ActionUiHelper import io.github.sds100.keymapper.base.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot @@ -272,8 +272,8 @@ class KeyMapListItemCreator( val parts = mutableListOf() - if (deviceName != null || key.detectionSource == KeyEventDetectionSource.INPUT_METHOD || !key.consumeEvent) { - if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { + if (deviceName != null || key.detectionSource == InputEventDetectionSource.INPUT_METHOD || !key.consumeEvent) { + if (key.detectionSource == InputEventDetectionSource.INPUT_METHOD) { parts.add(getString(R.string.flag_detect_from_input_method)) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt index d79cf2a697..ed7fa80f34 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt @@ -11,6 +11,7 @@ import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.constraints.isSatisfied +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey @@ -18,7 +19,6 @@ import io.github.sds100.keymapper.base.trigger.AssistantTriggerType import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice @@ -113,7 +113,7 @@ class KeyMapController( val keyMap = model.keyMap // TRIGGER STUFF keyMap.trigger.keys.forEachIndexed { keyIndex, key -> - if (key is KeyCodeTriggerKey && key.detectionSource == KeyEventDetectionSource.INPUT_METHOD && key.consumeEvent) { + if (key is KeyCodeTriggerKey && key.detectionSource == InputEventDetectionSource.INPUT_METHOD && key.consumeEvent) { triggerKeysThatSendRepeatedKeyEvents.add(key) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index 91cb05ff1b..b3a0830e0f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -24,8 +24,8 @@ import androidx.savedstate.SavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjectorImpl -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.KMError @@ -322,7 +322,7 @@ abstract class BaseAccessibilityService : repeatCount = event.repeatCount, source = event.source, ), - KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) ?: false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 37ded59f07..ce62825ef0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -11,6 +11,7 @@ import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.actions.PerformActionsUseCaseImpl import io.github.sds100.keymapper.base.actions.TestActionEvent import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCaseImpl +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent @@ -20,7 +21,6 @@ import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.base.trigger.RecordTriggerEvent import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.hasFlag @@ -394,7 +394,7 @@ abstract class BaseAccessibilityServiceController( fun onKeyEvent( event: KMKeyEvent, - detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource: InputEventDetectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ): Boolean { val detailedLogInfo = event.toString() @@ -459,13 +459,13 @@ abstract class BaseAccessibilityServiceController( if (event.action == KeyEvent.ACTION_UP && (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP || event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) { onKeyEvent( event.copy(action = KeyEvent.ACTION_DOWN), - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, ) } return onKeyEvent( event, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, ) } @@ -488,7 +488,7 @@ abstract class BaseAccessibilityServiceController( RecordTriggerEvent.RecordedTriggerKey( keyEvent.keyCode, keyEvent.device, - KeyEventDetectionSource.INPUT_METHOD, + InputEventDetectionSource.INPUT_METHOD, ), ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 237c5592d7..21a1381234 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapOptionsViewModel import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase @@ -509,7 +510,7 @@ abstract class BaseConfigTriggerViewModel( // Issue #491. Some key codes can only be detected through an input method. This will // be shown to the user by showing a keyboard icon next to the trigger key name so // explain this to the user. - if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD && displayKeyMap.showTriggerKeyboardIconExplanation.first()) { + if (key.detectionSource == InputEventDetectionSource.INPUT_METHOD && displayKeyMap.showTriggerKeyboardIconExplanation.first()) { val dialog = DialogModel.Alert( title = getString(R.string.dialog_title_keyboard_icon_means_ime_detection), message = getString(R.string.dialog_message_keyboard_icon_means_ime_detection), @@ -747,7 +748,7 @@ abstract class BaseConfigTriggerViewModel( return buildString { append(InputEventStrings.keyCodeToString(key.keyCode)) - if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { + if (key.detectionSource == InputEventDetectionSource.INPUT_METHOD) { val midDot = getString(R.string.middot) append(" $midDot ${getString(R.string.flag_detect_from_input_method)}") } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt index cbb4aa976c..afa7f7c3ea 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.trigger +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.withFlag @@ -15,7 +16,7 @@ data class KeyCodeTriggerKey( val device: TriggerKeyDevice, override val clickType: ClickType, override val consumeEvent: Boolean = true, - val detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + val detectionSource: InputEventDetectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) : TriggerKey() { override val allowedLongPress: Boolean = true @@ -67,9 +68,9 @@ data class KeyCodeTriggerKey( val detectionSource = if (entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD)) { - KeyEventDetectionSource.INPUT_METHOD + InputEventDetectionSource.INPUT_METHOD } else { - KeyEventDetectionSource.ACCESSIBILITY_SERVICE + InputEventDetectionSource.ACCESSIBILITY_SERVICE } return KeyCodeTriggerKey( @@ -108,7 +109,7 @@ data class KeyCodeTriggerKey( flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) } - if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { + if (key.detectionSource == InputEventDetectionSource.INPUT_METHOD) { flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventDetectionSource.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventDetectionSource.kt deleted file mode 100644 index c231beff8a..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventDetectionSource.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -enum class KeyEventDetectionSource { - ACCESSIBILITY_SERVICE, - INPUT_METHOD, -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 5a8d4ffebc..fe86be3004 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.base.trigger import android.view.KeyEvent +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult @@ -93,7 +94,7 @@ class RecordTriggerControllerImpl @Inject constructor( val recordedKey = createRecordedKeyEvent( keyEvent.keyCode, keyEvent.device, - KeyEventDetectionSource.INPUT_METHOD, + InputEventDetectionSource.INPUT_METHOD, ) recordedKeys.add(recordedKey) @@ -108,7 +109,7 @@ class RecordTriggerControllerImpl @Inject constructor( private fun createRecordedKeyEvent( keyCode: Int, device: InputDeviceInfo?, - detectionSource: KeyEventDetectionSource, + detectionSource: InputEventDetectionSource, ): RecordedKey { val triggerKeyDevice = if (device != null && device.isExternal) { TriggerKeyDevice.External(device.descriptor, device.name) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt index bc235207d6..d6bb5a3df8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.base.trigger import android.os.Parcelable +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import kotlinx.parcelize.Parcelize @@ -12,7 +13,7 @@ sealed class RecordTriggerEvent : AccessibilityServiceEvent() { data class RecordedTriggerKey( val keyCode: Int, val device: InputDeviceInfo?, - val detectionSource: KeyEventDetectionSource, + val detectionSource: InputEventDetectionSource, ) : RecordTriggerEvent(), Parcelable diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt index f4db2f265b..e6d59fd552 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt @@ -1,7 +1,9 @@ package io.github.sds100.keymapper.base.trigger +import io.github.sds100.keymapper.base.input.InputEventDetectionSource + data class RecordedKey( val keyCode: Int, val device: TriggerKeyDevice, - val detectionSource: KeyEventDetectionSource, + val detectionSource: InputEventDetectionSource, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt index 70184806ee..ef979f425d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.base.trigger import android.os.Build import android.view.KeyEvent +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.keymaps.requiresImeKeyEventForwardingInPhoneCall import io.github.sds100.keymapper.base.purchasing.ProductId @@ -74,7 +75,7 @@ data class TriggerErrorSnapshot( InputEventUtils.isDpadKeyCode( key.keyCode, ) && - key.detectionSource == KeyEventDetectionSource.INPUT_METHOD + key.detectionSource == InputEventDetectionSource.INPUT_METHOD if (showDpadImeSetupError && !isKeyMapperImeChosen && containsDpadKey) { return TriggerError.DPAD_IME_NOT_SELECTED diff --git a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt index 3d57377e3d..2d035f401c 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt @@ -4,13 +4,13 @@ import android.view.KeyEvent import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.constraints.Constraint +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCaseController import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode @@ -60,7 +60,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -79,7 +79,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -124,7 +124,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.setTriggerDoublePress() @@ -144,7 +144,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.setTriggerDoublePress() @@ -164,7 +164,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.setTriggerLongPress() @@ -184,7 +184,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.setTriggerLongPress() @@ -203,7 +203,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_DPAD_LEFT, TriggerKeyDevice.Any, - KeyEventDetectionSource.INPUT_METHOD, + InputEventDetectionSource.INPUT_METHOD, ) useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) @@ -223,7 +223,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) @@ -245,7 +245,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) @@ -267,12 +267,12 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_UP, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.setTriggerLongPress() @@ -289,7 +289,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.setTriggerDoublePress() useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -305,7 +305,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) useCase.setTriggerLongPress() useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -365,7 +365,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( modifierKeyCode, TriggerKeyDevice.Internal, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) // THEN @@ -387,7 +387,7 @@ class ConfigKeyMapUseCaseTest { useCase.addKeyCodeTriggerKey( KeyEvent.KEYCODE_A, TriggerKeyDevice.Internal, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) // THEN diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt index d112da2fdd..27d6a1074b 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt @@ -12,13 +12,13 @@ import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice @@ -846,7 +846,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -877,7 +877,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -912,7 +912,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -941,7 +941,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.LONG_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -968,7 +968,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -998,7 +998,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, ), ) @@ -1047,7 +1047,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.LONG_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, ), ) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt index cacadd68b9..a18eb8f9f3 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt @@ -1,8 +1,8 @@ package io.github.sds100.keymapper.base.utils +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice @@ -28,7 +28,7 @@ fun triggerKey( device: TriggerKeyDevice = TriggerKeyDevice.Internal, clickType: ClickType = ClickType.SHORT_PRESS, consume: Boolean = true, - detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + detectionSource: InputEventDetectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ): KeyCodeTriggerKey = KeyCodeTriggerKey( keyCode = keyCode, device = device, diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 8e8468abe7..088e28219d 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -13,8 +13,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -65,11 +63,6 @@ class SystemBridgeManagerImpl @Inject constructor( this.systemBridge = ISystemBridge.Stub.asInterface(binder) systemBridge?.registerCallback(evdevCallback) } - - coroutineScope.launch { - delay(1000) - grabAllDevices() - } } private fun grabAllDevices() { From 528f0fe7d05883de6f18595e245451b21b910806 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 Aug 2025 02:17:16 +0100 Subject: [PATCH 054/215] #1394 WIP: create InputEventHub --- .../sds100/keymapper/base/BaseMainActivity.kt | 4 +- .../keymapper/base/input/InputEventHub.kt | 93 ++++++++++++ .../detection/DpadMotionEventTracker.kt | 7 +- .../keymaps/detection/KeyMapController.kt | 4 +- .../accessibility/BaseAccessibilityService.kt | 21 +-- .../BaseAccessibilityServiceController.kt | 4 +- .../base/trigger/RecordTriggerController.kt | 4 +- .../keymaps/DpadMotionEventTrackerTest.kt | 14 +- .../base/keymaps/KeyMapControllerTest.kt | 14 +- .../keymapper/sysbridge/ISystemBridge.aidl | 5 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 3 +- .../manager/SystemBridgeConnection.kt | 9 ++ .../sysbridge/manager/SystemBridgeManager.kt | 135 +++++++----------- .../sysbridge/service/SystemBridge.kt | 32 ++++- .../system/inputevents/InputEventUtils.kt | 5 - .../system/inputevents/KMEvdevEvent.kt | 12 ++ .../system/inputevents/KMGamePadEvent.kt | 26 ++++ .../system/inputevents/KMInputEvent.kt | 2 +- .../system/inputevents/KMKeyEvent.kt | 35 ++++- .../system/inputevents/KMMotionEvent.kt | 29 ---- 20 files changed, 291 insertions(+), 167 deletions(-) create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnection.kt create mode 100644 system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt create mode 100644 system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt delete mode 100644 system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 352c637ff7..3c6d498964 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -35,7 +35,7 @@ import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.files.FileUtils -import io.github.sds100.keymapper.system.inputevents.KMMotionEvent +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.root.SuAdapterImpl @@ -216,7 +216,7 @@ abstract class BaseMainActivity : AppCompatActivity() { event ?: return super.onGenericMotionEvent(event) val consume = - recordTriggerController.onActivityMotionEvent(KMMotionEvent.fromMotionEvent(event)) + recordTriggerController.onActivityMotionEvent(KMGamePadEvent(event)) return if (consume) { true diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 92a7276852..8ae807f087 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -1,6 +1,99 @@ package io.github.sds100.keymapper.base.input +import android.os.Build +import io.github.sds100.keymapper.sysbridge.IEvdevCallback +import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager import io.github.sds100.keymapper.system.inputevents.KMInputEvent +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class InputEventHubImpl @Inject constructor( + private val systemBridgeManager: SystemBridgeManager +) : InputEventHub, IEvdevCallback.Stub() { + + private val callbacks: ConcurrentHashMap = ConcurrentHashMap() + + private var systemBridge: ISystemBridge? = null + + private val systemBridgeConnection: SystemBridgeConnection = object : SystemBridgeConnection { + override fun onServiceConnected(service: ISystemBridge) { + Timber.i("InputEventHub connected to SystemBridge") + + systemBridge = service + } + + override fun onServiceDisconnected(service: ISystemBridge) { + Timber.i("InputEventHub disconnected from SystemBridge") + + systemBridge = null + } + + override fun onBindingDied() { + Timber.i("SystemBridge connection died") + } + } + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeManager.registerConnection(systemBridgeConnection) + } + } + + override fun onEvdevEvent( + deviceId: Int, + timeSec: Long, + timeUsec: Long, + type: Int, + code: Int, + value: Int, + androidCode: Int + ) { + Timber.d( + "Evdev event: deviceId=${deviceId}, timeSec=$timeSec, timeUsec=$timeUsec, " + + "type=$type, code=$code, value=$value, androidCode=$androidCode" + ) + } + + override fun registerClient( + clientId: String, + callback: InputEventHubCallback + ) { + if (callbacks.contains(clientId)) { + throw IllegalArgumentException("This client already has a callback registered!") + } + + callbacks[clientId] = CallbackContext(callback, mutableSetOf()) + } + + override fun unregisterClient(clientId: String) { + // TODO ungrab the evdev devices + callbacks.remove(clientId) + } + + override fun setCallbackDevices( + clientId: String, + deviceIds: Array + ) { + TODO() + } + + override fun injectEvent(event: KMInputEvent) { + TODO() + } + + private class CallbackContext( + val callback: InputEventHubCallback, + /** + * The input events from devices that this callback subscribes to. + */ + val deviceIds: MutableSet + ) +} interface InputEventHub { /** diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt index 1064ec3630..0b153e03ce 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt @@ -4,8 +4,8 @@ import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputevents.KMMotionEvent /** * See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad @@ -63,7 +63,7 @@ class DpadMotionEventTracker { * * @return An array of key events. Empty if no DPAD buttons changed. */ - fun convertMotionEvent(event: KMMotionEvent): List { + fun convertMotionEvent(event: KMGamePadEvent): List { val oldState = dpadState[event.device.getDescriptor()] ?: 0 val newState = eventToDpadState(event) val diff = oldState xor newState @@ -109,6 +109,7 @@ class DpadMotionEventTracker { device = event.device, repeatCount = 0, source = InputDevice.SOURCE_DPAD, + eventTime = event.eventTime ) } } @@ -121,7 +122,7 @@ class DpadMotionEventTracker { return this?.descriptor ?: "" } - private fun eventToDpadState(event: KMMotionEvent): Int { + private fun eventToDpadState(event: KMGamePadEvent): Int { var state = 0 if (event.axisHatX == -1.0f) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt index ed7fa80f34..c6078bb9ca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt @@ -28,8 +28,8 @@ import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -569,7 +569,7 @@ class KeyMapController( } } - fun onMotionEvent(event: KMMotionEvent): Boolean { + fun onMotionEvent(event: KMGamePadEvent): Boolean { if (!detectKeyMaps) return false // See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index b3a0830e0f..6914cd4bfe 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -33,8 +33,8 @@ import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.MathUtils import io.github.sds100.keymapper.common.utils.PinchScreenType import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl import kotlinx.coroutines.flow.Flow @@ -159,6 +159,7 @@ abstract class BaseAccessibilityService : device = device, repeatCount = event.repeatCount, source = event.source, + eventTime = event.eventTime ), ) ?: false } @@ -167,7 +168,7 @@ abstract class BaseAccessibilityService : event ?: return false return getController() - ?.onMotionEventFromIme(KMMotionEvent.fromMotionEvent(event)) + ?.onMotionEventFromIme(KMGamePadEvent(event)) ?: return false } } @@ -306,22 +307,8 @@ abstract class BaseAccessibilityService : override fun onKeyEvent(event: KeyEvent?): Boolean { event ?: return super.onKeyEvent(event) - val device = if (event.device == null) { - null - } else { - InputDeviceUtils.createInputDeviceInfo(event.device) - } - return getController()?.onKeyEvent( - KMKeyEvent( - keyCode = event.keyCode, - action = event.action, - metaState = event.metaState, - scanCode = event.scanCode, - device = device, - repeatCount = event.repeatCount, - source = event.source, - ), + KMKeyEvent(event), InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) ?: false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index ce62825ef0..61c3b8575b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -33,8 +33,8 @@ import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -469,7 +469,7 @@ abstract class BaseAccessibilityServiceController( ) } - fun onMotionEventFromIme(event: KMMotionEvent): Boolean { + fun onMotionEventFromIme(event: KMGamePadEvent): Boolean { if (isPaused.value) { return false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index fe86be3004..5e269944c0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -7,7 +7,7 @@ import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputevents.KMMotionEvent +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -78,7 +78,7 @@ class RecordTriggerControllerImpl @Inject constructor( * these are sent from the joy sticks. * @return Whether the motion event is consumed. */ - fun onActivityMotionEvent(event: KMMotionEvent): Boolean { + fun onActivityMotionEvent(event: KMGamePadEvent): Boolean { if (state.value !is RecordTriggerState.CountingDown) { return false } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt index a4be4b9d49..a346bc987b 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt @@ -4,8 +4,8 @@ import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import junitparams.JUnitParamsRunner import kotlinx.coroutines.ExperimentalCoroutinesApi import org.hamcrest.MatcherAssert.assertThat @@ -68,6 +68,7 @@ class DpadMotionEventTrackerTest { device = CONTROLLER_1_DEVICE, repeatCount = 0, source = InputDevice.SOURCE_DPAD, + eventTime = motionEvent.eventTime ), ), ) @@ -82,6 +83,7 @@ class DpadMotionEventTrackerTest { device = CONTROLLER_1_DEVICE, repeatCount = 0, source = InputDevice.SOURCE_DPAD, + eventTime = motionEvent.eventTime ), ), ) @@ -257,14 +259,13 @@ class DpadMotionEventTrackerTest { axisHatX: Float = 0.0f, axisHatY: Float = 0.0f, device: InputDeviceInfo = CONTROLLER_1_DEVICE, - isDpad: Boolean = true, - ): KMMotionEvent { - return KMMotionEvent( + ): KMGamePadEvent { + return KMGamePadEvent( metaState = 0, device = device, axisHatX = axisHatX, axisHatY = axisHatY, - isDpad = isDpad, + eventTime = System.currentTimeMillis() ) } @@ -277,6 +278,7 @@ class DpadMotionEventTrackerTest { device = device, repeatCount = 0, source = 0, + eventTime = System.currentTimeMillis() ) } @@ -289,6 +291,8 @@ class DpadMotionEventTrackerTest { device = device, repeatCount = 0, source = 0, + eventTime = System.currentTimeMillis() + ) } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt index 27d6a1074b..bb88b1adda 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt @@ -33,8 +33,8 @@ import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.system.camera.CameraLens +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputevents.KMMotionEvent import io.github.sds100.keymapper.system.inputmethod.ImeInfo import junitparams.JUnitParamsRunner import junitparams.Parameters @@ -4179,14 +4179,13 @@ class KeyMapControllerTest { axisHatX: Float = 0.0f, axisHatY: Float = 0.0f, device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, - isDpad: Boolean = true, - ): KMMotionEvent { - return KMMotionEvent( + ): KMGamePadEvent { + return KMGamePadEvent( metaState = 0, device = device, axisHatX = axisHatX, axisHatY = axisHatY, - isDpad = isDpad, + eventTime = System.currentTimeMillis() ) } @@ -4195,12 +4194,12 @@ class KeyMapControllerTest { axisHatY: Float = 0.0f, device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, ): Boolean = controller.onMotionEvent( - KMMotionEvent( + KMGamePadEvent( metaState = 0, device = device, axisHatX = axisHatX, axisHatY = axisHatY, - isDpad = true, + eventTime = System.currentTimeMillis() ), ) @@ -4220,6 +4219,7 @@ class KeyMapControllerTest { device = device, repeatCount = repeatCount, source = 0, + eventTime = System.currentTimeMillis() ), ) diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index e52cc4039f..6b290d346d 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -12,6 +12,7 @@ interface ISystemBridge { void destroy() = 16777114; boolean grabEvdevDevice(int deviceId) = 1; - boolean injectEvent(in InputEvent event, int mode) = 2; - void registerCallback(IEvdevCallback callback) = 3; + void registerEvdevCallback(IEvdevCallback callback) = 2; + void unregisterEvdevCallback() = 3; + boolean injectEvent(in InputEvent event, int mode) = 4; } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index e5214e3a75..ea9c7073aa 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -398,8 +398,7 @@ extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv *env, jobject thiz) { - Command cmd; - cmd.type = STOP; + Command cmd = {STOP}; std::lock_guard lock(commandMutex); commandQueue.push(cmd); diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnection.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnection.kt new file mode 100644 index 0000000000..2955a1fb4a --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnection.kt @@ -0,0 +1,9 @@ +package io.github.sds100.keymapper.sysbridge.manager + +import io.github.sds100.keymapper.sysbridge.ISystemBridge + +interface SystemBridgeConnection { + fun onServiceConnected(service: ISystemBridge) + fun onServiceDisconnected(service: ISystemBridge) + fun onBindingDied() +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 088e28219d..06df4223e3 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -1,54 +1,35 @@ package io.github.sds100.keymapper.sysbridge.manager import android.annotation.SuppressLint -import android.content.Context -import android.hardware.input.InputManager import android.os.Build import android.os.IBinder import android.os.IBinder.DeathRecipient -import android.view.InputDevice import androidx.annotation.RequiresApi -import androidx.core.content.getSystemService -import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge -import kotlinx.coroutines.CoroutineScope -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton /** - * This class handles starting, stopping, (dis)connecting to the system bridge + * This class handles starting, stopping and (dis)connecting to the system bridge. */ @Singleton -class SystemBridgeManagerImpl @Inject constructor( - @ApplicationContext private val ctx: Context, - private val coroutineScope: CoroutineScope -) : SystemBridgeManager { - - private val inputManager: InputManager by lazy { ctx.getSystemService()!! } +class SystemBridgeManagerImpl @Inject constructor() : SystemBridgeManager { private val systemBridgeLock: Any = Any() private var systemBridge: ISystemBridge? = null - private val callbackLock: Any = Any() - private var evdevConnections: ConcurrentHashMap = - ConcurrentHashMap() - private val evdevCallback: IEvdevCallback = object : IEvdevCallback.Stub() { - override fun onEvdevEvent( - deviceId: Int, - timeSec: Long, - timeUsec: Long, - type: Int, - code: Int, - value: Int, - androidCode: Int - ) { - Timber.d( - "Evdev event: deviceId=${deviceId}, timeSec=$timeSec, timeUsec=$timeUsec, " + - "type=$type, code=$code, value=$value, androidCode=$androidCode" - ) + private val connectionsLock: Any = Any() + private val connections: MutableSet = mutableSetOf() + + private val deathRecipient: DeathRecipient = DeathRecipient { + synchronized(systemBridgeLock) { + systemBridge = null + } + + synchronized(connectionsLock) { + for (connection in connections) { + connection.onBindingDied() + } } } @@ -58,77 +39,61 @@ class SystemBridgeManagerImpl @Inject constructor( } } + /** + * This is called by the SystemBridgeBinderProvider content provider. + */ fun onBinderReceived(binder: IBinder) { + val systemBridge = ISystemBridge.Stub.asInterface(binder) + synchronized(systemBridgeLock) { - this.systemBridge = ISystemBridge.Stub.asInterface(binder) - systemBridge?.registerCallback(evdevCallback) + systemBridge.asBinder().linkToDeath(deathRecipient, 0) + this.systemBridge = systemBridge } - } - private fun grabAllDevices() { - synchronized(callbackLock) { - val deviceId = 1 - if (evdevConnections.containsKey(deviceId)) { - removeEvdevConnection(deviceId) + synchronized(connectionsLock) { + for (connection in connections) { + connection.onServiceConnected(systemBridge) } - - val connection = EvdevConnection(deviceId, evdevCallback) - evdevConnections[deviceId] = connection - evdevCallback.asBinder().linkToDeath(connection, 0) - - return@synchronized evdevCallback } + } - for (deviceId in inputManager.inputDeviceIds) { - if (deviceId == -1) { - continue - } - - val device = inputManager.getInputDevice(deviceId) ?: continue - - grabInputDevice(device) + override fun registerConnection(connection: SystemBridgeConnection) { + synchronized(connectionsLock) { + connections.add(connection) } } - private fun grabInputDevice(inputDevice: InputDevice) { - val deviceId = inputDevice.id - - try { - Timber.d("Grabbing input device: ${inputDevice.name} (${inputDevice.id})") - - this.systemBridge?.grabEvdevDevice(deviceId) - - Timber.d("Grabbed input device: ${inputDevice.name} (${inputDevice.id})") - - - } catch (e: Exception) { - Timber.e("Error grabbing input device: ${e.toString()}") + override fun unregisterConnection(connection: SystemBridgeConnection) { + synchronized(connectionsLock) { + connections.remove(connection) } - } - private fun removeEvdevConnection(deviceId: Int) { - val connection = evdevConnections.remove(deviceId) ?: return + override fun startWithShizuku() { + TODO("Not yet implemented") + } - // Unlink the death recipient from the connection to remove and - // delete it from the list of connections for the package. - connection.callback.asBinder().unlinkToDeath(connection, 0) + override fun startWithAdb() { + TODO("Not yet implemented") } - private inner class EvdevConnection( - private val deviceId: Int, - val callback: IEvdevCallback, - ) : DeathRecipient { - override fun binderDied() { - Timber.d("EvdevCallback binder died: $deviceId") - synchronized(callbackLock) { - removeEvdevConnection(deviceId) - } - } + override fun startWithRoot() { + TODO("Not yet implemented") } + override fun stopSystemBridge() { + TODO("Not yet implemented") + } } @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) -interface SystemBridgeManager \ No newline at end of file +interface SystemBridgeManager { + fun registerConnection(connection: SystemBridgeConnection) + fun unregisterConnection(connection: SystemBridgeConnection) + + fun startWithShizuku() + fun startWithAdb() + fun startWithRoot() + fun stopSystemBridge() +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 295238e41a..d4708e32ed 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -37,6 +37,7 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. + // TODO return error code and map this to a SystemBridgeError in key mapper external fun grabEvdevDevice( deviceIdentifier: InputDeviceIdentifier ): Boolean @@ -159,6 +160,13 @@ internal class SystemBridge : ISystemBridge.Stub() { private val coroutineScope: CoroutineScope = MainScope() private val mainHandler = Handler(Looper.myLooper()!!) + private val evdevCallbackLock: Any = Any() + private var evdevCallback: IEvdevCallback? = null + private val evdevCallbackDeathRecipient: IBinder.DeathRecipient = IBinder.DeathRecipient { + Log.d(TAG, "EvdevCallback binder died") + stopEvdevEventLoop() + } + init { @SuppressLint("UnsafeDynamicallyLoadedCode") System.load("${System.getProperty("keymapper_sysbridge.library.path")}/libevdev.so") @@ -202,19 +210,39 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO ungrab all evdev devices if no key mapper app is bound to the service override fun destroy() { Log.d(TAG, "SystemBridge destroyed") + + // Must be last line in this method because it halts the JVM. exitProcess(0) } - override fun registerCallback(callback: IEvdevCallback?) { + override fun registerEvdevCallback(callback: IEvdevCallback?) { callback ?: return + val binder = callback.asBinder() + + if (this.evdevCallback != null) { + unregisterEvdevCallback() + } + + synchronized(evdevCallbackLock) { + this.evdevCallback = callback + binder.linkToDeath(evdevCallbackDeathRecipient, 0) + } + coroutineScope.launch(Dispatchers.IO) { mainHandler.post { - startEvdevEventLoop(callback.asBinder()) + startEvdevEventLoop(binder) } } } + override fun unregisterEvdevCallback() { + synchronized(evdevCallbackLock) { + evdevCallback?.asBinder()?.unlinkToDeath(evdevCallbackDeathRecipient, 0) + evdevCallback = null + } + } + override fun grabEvdevDevice( deviceId: Int, ): Boolean { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt index ace7de6e71..27b0a8dbea 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt @@ -1,8 +1,6 @@ package io.github.sds100.keymapper.system.inputevents import android.os.Build -import android.view.InputDevice -import android.view.InputEvent import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.withFlag @@ -459,9 +457,6 @@ object InputEventUtils { code == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT } - 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, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt new file mode 100644 index 0000000000..2da6fdd705 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt @@ -0,0 +1,12 @@ +package io.github.sds100.keymapper.system.inputevents + +data class KMEvdevEvent( + val type: Int, + val code: Int, + val value: Int, + + // This is only non null when receiving an event. If sending an event + // then the time does not need to be set. + val timeSec: Long? = null, + val timeUsec: Long? = null +) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt new file mode 100644 index 0000000000..cc25146b73 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt @@ -0,0 +1,26 @@ +package io.github.sds100.keymapper.system.inputevents + +import android.view.MotionEvent +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils + +/** + * This is our own abstraction over MotionEvent so that it is easier to write tests and read + * values without relying on the Android SDK. + */ +data class KMGamePadEvent( + val eventTime: Long, + val metaState: Int, + val device: InputDeviceInfo?, + val axisHatX: Float, + val axisHatY: Float, +) : KMInputEvent { + + constructor(event: MotionEvent) : this( + eventTime = event.eventTime, + metaState = event.metaState, + device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) }, + axisHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X), + axisHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y), + ) +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt index db3fa93c67..1e12eaed08 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt @@ -1,3 +1,3 @@ package io.github.sds100.keymapper.system.inputevents -interface KMInputEvent \ No newline at end of file +sealed interface KMInputEvent \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 23a46dfc34..41fc86a66c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -1,6 +1,8 @@ package io.github.sds100.keymapper.system.inputevents +import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils /** * This is our own abstraction over KeyEvent so that it is easier to write tests and read @@ -14,4 +16,35 @@ data class KMKeyEvent( val device: InputDeviceInfo?, val repeatCount: Int, val source: Int, -) : KMInputEvent + val eventTime: Long +) : KMInputEvent { + + constructor(keyEvent: KeyEvent) : this( + keyCode = keyEvent.keyCode, + action = keyEvent.action, + metaState = keyEvent.metaState, + scanCode = keyEvent.scanCode, + device = if (keyEvent.device == null) { + null + } else { + InputDeviceUtils.createInputDeviceInfo(keyEvent.device) + }, + repeatCount = keyEvent.repeatCount, + source = keyEvent.source, + eventTime = keyEvent.eventTime + ) + + fun toKeyEvent(): KeyEvent { + return KeyEvent( + eventTime, + eventTime, + action, + keyCode, + repeatCount, + metaState, + device?.id ?: -1, + scanCode, + source + ) + } +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt deleted file mode 100644 index 24d2362ddc..0000000000 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMMotionEvent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.sds100.keymapper.system.inputevents - -import android.view.MotionEvent -import io.github.sds100.keymapper.common.utils.InputDeviceInfo -import io.github.sds100.keymapper.common.utils.InputDeviceUtils - -/** - * This is our own abstraction over MotionEvent so that it is easier to write tests and read - * values without relying on the Android SDK. - */ -data class KMMotionEvent( - val metaState: Int, - val device: InputDeviceInfo?, - val axisHatX: Float, - val axisHatY: Float, - val isDpad: Boolean, -) : KMInputEvent { - companion object { - fun fromMotionEvent(event: MotionEvent): KMMotionEvent { - return KMMotionEvent( - metaState = event.metaState, - device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) }, - axisHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X), - axisHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y), - isDpad = InputEventUtils.isDpadDevice(event), - ) - } - } -} From 385b55de67417e83f34dc053d210bf7dfd77a009 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 Aug 2025 12:48:32 +0100 Subject: [PATCH 055/215] #1394 InputEventHub: write code to inject input events --- .../keymapper/base/input/InputEventHub.kt | 75 ++++++++++++++++++- .../inputmethod/ImeInputEventInjector.kt | 1 + .../keymapper/sysbridge/ISystemBridge.aidl | 3 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 11 +++ .../sysbridge/service/SystemBridge.kt | 5 ++ .../sysbridge/utils/SystemBridgeResult.kt | 8 ++ .../system/inputevents/InputEventInjector.kt | 3 +- .../system/inputevents/KMEvdevEvent.kt | 3 +- .../system/inputmethod/InputKeyModel.kt | 1 + 9 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 8ae807f087..a688770d58 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -1,11 +1,22 @@ package io.github.sds100.keymapper.base.input import android.os.Build +import android.view.KeyEvent +import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector +import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.success import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -13,9 +24,14 @@ import javax.inject.Singleton @Singleton class InputEventHubImpl @Inject constructor( - private val systemBridgeManager: SystemBridgeManager + private val systemBridgeManager: SystemBridgeManager, + private val imeInputEventInjector: ImeInputEventInjector ) : InputEventHub, IEvdevCallback.Stub() { + companion object { + private const val INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2 + } + private val callbacks: ConcurrentHashMap = ConcurrentHashMap() private var systemBridge: ISystemBridge? = null @@ -82,8 +98,59 @@ class InputEventHubImpl @Inject constructor( TODO() } - override fun injectEvent(event: KMInputEvent) { - TODO() + override suspend fun injectEvent(event: KMInputEvent): KMResult { + when (event) { + is KMGamePadEvent -> { + throw IllegalArgumentException("KMGamePadEvents can not be injected. Must use an evdev event instead.") + } + + is KMKeyEvent -> { + val systemBridge = this.systemBridge + + if (systemBridge == null) { + // TODO InputKeyModel will be removed + val action = if (event.action == KeyEvent.ACTION_DOWN) { + InputEventType.DOWN + } else { + InputEventType.UP + } + + val model = InputKeyModel( + keyCode = event.keyCode, + inputType = action, + metaState = event.metaState, + deviceId = event.device?.id ?: -1, + scanCode = event.scanCode, + repeat = event.repeatCount, + source = event.source + ) + + imeInputEventInjector.inputKeyEvent(model) + + return Success(true) + } else { + return systemBridge.injectEvent( + event.toKeyEvent(), + INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH + ).success() + } + } + + is KMEvdevEvent -> { + val systemBridge = this.systemBridge + + if (systemBridge == null) { + return SystemBridgeError.Disconnected + } + + return systemBridge.writeEvdevEvent( + event.deviceId, + event.type, + event.code, + event.value + ).success() + } + } } private class CallbackContext( @@ -114,5 +181,5 @@ interface InputEventHub { * Inject an input event. This may either use the key event relay service or the system * bridge depending on the permissions granted to Key Mapper. */ - fun injectEvent(event: KMInputEvent) + suspend fun injectEvent(event: KMInputEvent): KMResult } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt index 4f388fa66a..acbae7543f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt @@ -43,6 +43,7 @@ class ImeInputEventInjectorImpl( private const val CALLBACK_ID_INPUT_METHOD = "input_method" } + // TODO delete this and InputKeyModel override suspend fun inputKeyEvent(model: InputKeyModel) { Timber.d("Inject key event with input method ${KeyEvent.keyCodeToString(model.keyCode)}, $model") diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 6b290d346d..969703f2ad 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -14,5 +14,6 @@ interface ISystemBridge { boolean grabEvdevDevice(int deviceId) = 1; void registerEvdevCallback(IEvdevCallback callback) = 2; void unregisterEvdevCallback() = 3; - boolean injectEvent(in InputEvent event, int mode) = 4; + boolean writeEvdevEvent(int deviceId, int type, int code, int value) = 4; + boolean injectEvent(in InputEvent event, int mode) = 5; } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index ea9c7073aa..f71d80d845 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -409,4 +409,15 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoo if (written < 0) { LOGE("Failed to write to commandEventFd: %s", strerror(errno)); } +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNative(JNIEnv *env, + jobject thiz, + jint device_id, + jint type, + jint code, + jint value) { + // TODO: implement writeEvdevEvent() } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index d4708e32ed..9f28231ca8 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -43,6 +43,7 @@ internal class SystemBridge : ISystemBridge.Stub() { ): Boolean external fun ungrabEvdevDevice(deviceId: Int) + external fun writeEvdevEventNative(deviceId: Int, type: Int, code: Int, value: Int): Boolean external fun startEvdevEventLoop(callback: IBinder) external fun stopEvdevEventLoop() @@ -266,4 +267,8 @@ internal class SystemBridge : ISystemBridge.Stub() { override fun injectEvent(event: InputEvent?, mode: Int): Boolean { return inputManager.injectInputEvent(event, mode) } + + override fun writeEvdevEvent(deviceId: Int, type: Int, code: Int, value: Int): Boolean { + return writeEvdevEventNative(deviceId, type, code, value) + } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt new file mode 100644 index 0000000000..10023629f6 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt @@ -0,0 +1,8 @@ +package io.github.sds100.keymapper.sysbridge.utils + +import io.github.sds100.keymapper.common.utils.KMError + +sealed class SystemBridgeError : KMError() { + data object NotStarted : SystemBridgeError() + data object Disconnected : SystemBridgeError() +} \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt index b02186d68d..c6005860c7 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt @@ -7,11 +7,10 @@ interface InputEventInjector { suspend fun inputKeyEvent(model: InputKeyModel) } - /** * Create a KeyEvent instance that can be injected into the Android system. */ -fun InputEventInjector.createKeyEvent( +fun createKeyEvent( eventTime: Long, action: Int, model: InputKeyModel, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt index 2da6fdd705..03e6587109 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.system.inputevents data class KMEvdevEvent( + val deviceId: Int, val type: Int, val code: Int, val value: Int, @@ -9,4 +10,4 @@ data class KMEvdevEvent( // then the time does not need to be set. val timeSec: Long? = null, val timeUsec: Long? = null -) +) : KMInputEvent diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt index 1142fef197..49a8fb937f 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.system.inputmethod import android.view.InputDevice import io.github.sds100.keymapper.common.utils.InputEventType +// TODO delete data class InputKeyModel( val keyCode: Int, val inputType: InputEventType = InputEventType.DOWN_UP, From dd33c7495be991d7d6eb6a69d4e08ddf2a0d3f4d Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 Aug 2025 12:53:25 +0100 Subject: [PATCH 056/215] #1394 InputEventHub: handle remote exceptions --- .../keymapper/base/input/InputEventHub.kt | 94 +++++++++++-------- .../sds100/keymapper/common/utils/KMResult.kt | 2 +- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index a688770d58..8eb34b248b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -1,9 +1,11 @@ package io.github.sds100.keymapper.base.input import android.os.Build +import android.os.RemoteException import android.view.KeyEvent import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.success @@ -105,50 +107,66 @@ class InputEventHubImpl @Inject constructor( } is KMKeyEvent -> { - val systemBridge = this.systemBridge - - if (systemBridge == null) { - // TODO InputKeyModel will be removed - val action = if (event.action == KeyEvent.ACTION_DOWN) { - InputEventType.DOWN - } else { - InputEventType.UP - } - - val model = InputKeyModel( - keyCode = event.keyCode, - inputType = action, - metaState = event.metaState, - deviceId = event.device?.id ?: -1, - scanCode = event.scanCode, - repeat = event.repeatCount, - source = event.source - ) - - imeInputEventInjector.inputKeyEvent(model) - - return Success(true) - } else { - return systemBridge.injectEvent( - event.toKeyEvent(), - INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH - ).success() - } + return injectKeyEvent(event) } is KMEvdevEvent -> { - val systemBridge = this.systemBridge + return injectEvdevEvent(event) + } + } + } + + private fun injectEvdevEvent(event: KMEvdevEvent): KMResult { + val systemBridge = this.systemBridge + + if (systemBridge == null) { + return SystemBridgeError.Disconnected + } - if (systemBridge == null) { - return SystemBridgeError.Disconnected - } + try { + return systemBridge.writeEvdevEvent( + event.deviceId, + event.type, + event.code, + event.value + ).success() + } catch (e: RemoteException) { + return KMError.Exception(e) + } + } + + private suspend fun injectKeyEvent(event: KMKeyEvent): KMResult { + val systemBridge = this.systemBridge + + if (systemBridge == null) { + // TODO InputKeyModel will be removed + val action = if (event.action == KeyEvent.ACTION_DOWN) { + InputEventType.DOWN + } else { + InputEventType.UP + } - return systemBridge.writeEvdevEvent( - event.deviceId, - event.type, - event.code, - event.value + val model = InputKeyModel( + keyCode = event.keyCode, + inputType = action, + metaState = event.metaState, + deviceId = event.device?.id ?: -1, + scanCode = event.scanCode, + repeat = event.repeatCount, + source = event.source + ) + + imeInputEventInjector.inputKeyEvent(model) + + return Success(true) + } else { + try { + return systemBridge.injectEvent( + event.toKeyEvent(), + INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH ).success() + } catch (e: RemoteException) { + return KMError.Exception(e) } } } diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt index ac7493b303..c8fbdcf24f 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt @@ -164,4 +164,4 @@ fun KMResult.handle(onSuccess: (value: T) -> U, onError: (error: KMErr is KMError -> onError(this) } -fun T.success() = Success(this) +fun T.success() = Success(this) \ No newline at end of file From 50f39b1d6a071c29806b759880f19c0f47281108 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 Aug 2025 20:30:26 +0100 Subject: [PATCH 057/215] #1394 refactor KeyEventRelayServiceWrapper to be a singleton and clients register callbacks through it --- .../AccessibilityServiceController.kt | 10 +- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 6 + .../sds100/keymapper/base/BaseMainActivity.kt | 1 + .../keymapper/base/BaseSingletonHiltModule.kt | 18 ++ .../base/actions/PerformActionsUseCase.kt | 2 - .../base/input/InputEventDetectionSource.kt | 3 +- .../keymapper/base/input/InputEventHub.kt | 117 +++++++++++- .../keymaps/detection/DetectKeyMapsUseCase.kt | 2 - .../RerouteKeyEventsController.kt | 2 - .../accessibility/BaseAccessibilityService.kt | 63 ------- .../BaseAccessibilityServiceController.kt | 172 ++++++++++-------- .../inputmethod/ImeInputEventInjector.kt | 10 +- ...iltModule.kt => SystemBridgeHiltModule.kt} | 2 +- .../KeyEventRelayServiceWrapper.kt | 54 +++--- .../system/inputmethod/KeyMapperImeService.kt | 17 +- 15 files changed, 278 insertions(+), 201 deletions(-) rename sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/{SysBridgeHiltModule.kt => SystemBridgeHiltModule.kt} (95%) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index d5a9ebd1f3..8832c80ada 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -5,6 +5,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.github.sds100.keymapper.base.actions.PerformActionsUseCaseImpl import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCaseImpl +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl @@ -14,6 +15,7 @@ import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilitySer import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.devices.DevicesAdapter +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import io.github.sds100.keymapper.system.root.SuAdapter class AccessibilityServiceController @AssistedInject constructor( @@ -29,7 +31,9 @@ class AccessibilityServiceController @AssistedInject constructor( devicesAdapter: DevicesAdapter, suAdapter: SuAdapter, settingsRepository: PreferenceRepository, - systemBridgeSetupController: SystemBridgeSetupController + systemBridgeSetupController: SystemBridgeSetupController, + keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, + inputEventHub: InputEventHub ) : BaseAccessibilityServiceController( service = service, rerouteKeyEventsControllerFactory = rerouteKeyEventsControllerFactory, @@ -42,7 +46,9 @@ class AccessibilityServiceController @AssistedInject constructor( devicesAdapter = devicesAdapter, suAdapter = suAdapter, settingsRepository = settingsRepository, - systemBridgeSetupController = systemBridgeSetupController + systemBridgeSetupController = systemBridgeSetupController, + keyEventRelayServiceWrapper = keyEventRelayServiceWrapper, + inputEventHub = inputEventHub ) { @AssistedFactory diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index 62d85ba466..96026e6a79 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -25,6 +25,7 @@ import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.data.repositories.SettingsPreferenceRepository import io.github.sds100.keymapper.system.apps.AndroidPackageManagerAdapter import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.root.SuAdapterImpl @@ -79,6 +80,9 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { @Inject lateinit var logRepository: LogRepository + @Inject + lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl + private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() } private val userManager: UserManager? by lazy { getSystemService() } @@ -184,6 +188,8 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { }.launchIn(appCoroutineScope) autoGrantPermissionController.start() + + keyEventRelayServiceWrapper.bind() } abstract fun getMainActivityClass(): Class<*> diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 3c6d498964..6d0f8fc426 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -215,6 +215,7 @@ abstract class BaseMainActivity : AppCompatActivity() { override fun onGenericMotionEvent(event: MotionEvent?): Boolean { event ?: return super.onGenericMotionEvent(event) + // TODO send this to inputeventhub val consume = recordTriggerController.onActivityMotionEvent(KMGamePadEvent(event)) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt index 6e51de948d..76e14e299a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt @@ -14,6 +14,8 @@ import io.github.sds100.keymapper.base.backup.BackupManager import io.github.sds100.keymapper.base.backup.BackupManagerImpl import io.github.sds100.keymapper.base.constraints.GetConstraintErrorUseCase import io.github.sds100.keymapper.base.constraints.GetConstraintErrorUseCaseImpl +import io.github.sds100.keymapper.base.input.InputEventHub +import io.github.sds100.keymapper.base.input.InputEventHubImpl import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCaseController import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase @@ -25,6 +27,8 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCaseImpl import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCaseImpl +import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector +import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjectorImpl import io.github.sds100.keymapper.base.system.inputmethod.ShowHideInputMethodUseCase import io.github.sds100.keymapper.base.system.inputmethod.ShowHideInputMethodUseCaseImpl import io.github.sds100.keymapper.base.system.inputmethod.ShowInputMethodPickerUseCase @@ -45,6 +49,8 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.common.utils.DefaultUuidGenerator import io.github.sds100.keymapper.common.utils.UuidGenerator import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl import io.github.sds100.keymapper.system.notifications.NotificationAdapter import javax.inject.Singleton @@ -134,4 +140,16 @@ abstract class BaseSingletonHiltModule { @Binds @Singleton abstract fun bindDialogProvider(impl: DialogProviderImpl): DialogProvider + + @Binds + @Singleton + abstract fun bindInputEventHub(impl: InputEventHubImpl): InputEventHub + + @Binds + @Singleton + abstract fun keyEventRelayServiceWrapper(impl: KeyEventRelayServiceWrapperImpl): KeyEventRelayServiceWrapper + + @Binds + @Singleton + abstract fun imeInputEvenInjector(impl: ImeInputEventInjectorImpl): ImeInputEventInjector } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 690635ed06..367dd23464 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -87,7 +87,6 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( private val shell: ShellAdapter, private val intentAdapter: IntentAdapter, private val getActionErrorUseCase: GetActionErrorUseCase, - @Assisted private val keyMapperImeMessenger: ImeInputEventInjector, private val packageManagerAdapter: PackageManagerAdapter, private val appShortcutAdapter: AppShortcutAdapter, @@ -116,7 +115,6 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( interface Factory { fun create( accessibilityService: IAccessibilityService, - imeInputEventInjector: ImeInputEventInjector, ): PerformActionsUseCaseImpl } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt index 6ca2e953a9..449b7d614b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt @@ -3,5 +3,6 @@ package io.github.sds100.keymapper.base.input enum class InputEventDetectionSource { ACCESSIBILITY_SERVICE, INPUT_METHOD, - SYSTEM_BRIDGE + SYSTEM_BRIDGE, + MAIN_ACTIVITY } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 8eb34b248b..771d68cdfa 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -3,22 +3,31 @@ package io.github.sds100.keymapper.base.input import android.os.Build import android.os.RemoteException import android.view.KeyEvent +import io.github.sds100.keymapper.base.BuildConfig import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.success +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -26,15 +35,18 @@ import javax.inject.Singleton @Singleton class InputEventHubImpl @Inject constructor( + private val coroutineScope: CoroutineScope, private val systemBridgeManager: SystemBridgeManager, - private val imeInputEventInjector: ImeInputEventInjector + private val imeInputEventInjector: ImeInputEventInjector, + private val preferenceRepository: PreferenceRepository ) : InputEventHub, IEvdevCallback.Stub() { companion object { private const val INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2 } - private val callbacks: ConcurrentHashMap = ConcurrentHashMap() + private val clients: ConcurrentHashMap = + ConcurrentHashMap() private var systemBridge: ISystemBridge? = null @@ -56,6 +68,15 @@ class InputEventHubImpl @Inject constructor( } } + private val logInputEventsEnabled: StateFlow = + preferenceRepository.get(Keys.log).map { isLogEnabled -> + if (isLogEnabled == true) { + isLogEnabled + } else { + BuildConfig.DEBUG + } + }.stateIn(coroutineScope, SharingStarted.Eagerly, false) + init { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { systemBridgeManager.registerConnection(systemBridgeConnection) @@ -77,27 +98,100 @@ class InputEventHubImpl @Inject constructor( ) } + override fun onInputEvent( + event: KMInputEvent, + detectionSource: InputEventDetectionSource + ): Boolean { + + val uniqueEvent: KMInputEvent = if (event is KMKeyEvent) { + makeUniqueKeyEvent(event) + } else { + event + } + + if (logInputEventsEnabled.value) { + logInputEvent(uniqueEvent) + } + + return false + } + + /** + * Sometimes key events are sent with an unknown key code so to make it unique, + * this will set a unique key code to the key event that won't conflict. + */ + private fun makeUniqueKeyEvent(event: KMKeyEvent): KMKeyEvent { + // Guard to ignore processing when not applicable + if (event.keyCode != KeyEvent.KEYCODE_UNKNOWN) { + return event + } + + // Don't offset negative values + val scanCodeOffset: Int = if (event.scanCode >= 0) { + InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET + } else { + 0 + } + + return event.copy( + // Fallback to scanCode when keyCode is unknown as it's typically more unique + // Add offset to go past possible keyCode values + keyCode = event.scanCode + scanCodeOffset, + ) + } + + private fun logInputEvent(event: KMInputEvent) { + when (event) { + is KMEvdevEvent -> { + Timber.d( + "Evdev event: deviceId=${event.deviceId}, type=${event.type}, code=${event.code}, value=${event.value}" + ) + } + + is KMGamePadEvent -> { + Timber.d( + "GamePad event: deviceId=${event.device?.id}, axisHatX=${event.axisHatX}, axisHatY=${event.axisHatY}" + ) + } + + is KMKeyEvent -> { + when (event.action) { + KeyEvent.ACTION_DOWN -> { + Timber.d("Key down: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.device?.id}, metaState=${event.metaState}, source=${event.source}") + } + + KeyEvent.ACTION_UP -> { + Timber.d("Key up: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.device?.id}, metaState=${event.metaState}, source=${event.source}") + } + + else -> { + Timber.w("Unknown key event action: ${event.action}") + } + } + } + } + } + override fun registerClient( clientId: String, callback: InputEventHubCallback ) { - if (callbacks.contains(clientId)) { + if (clients.contains(clientId)) { throw IllegalArgumentException("This client already has a callback registered!") } - callbacks[clientId] = CallbackContext(callback, mutableSetOf()) + clients[clientId] = CallbackContext(callback, mutableSetOf()) } override fun unregisterClient(clientId: String) { // TODO ungrab the evdev devices - callbacks.remove(clientId) + clients.remove(clientId) } override fun setCallbackDevices( clientId: String, - deviceIds: Array + deviceIds: Array ) { - TODO() } override suspend fun injectEvent(event: KMInputEvent): KMResult { @@ -193,11 +287,18 @@ interface InputEventHub { /** * Set the devices that a client wants to listen to. */ - fun setCallbackDevices(clientId: String, deviceIds: Array) + fun setCallbackDevices(clientId: String, deviceIds: Array) /** * Inject an input event. This may either use the key event relay service or the system * bridge depending on the permissions granted to Key Mapper. */ suspend fun injectEvent(event: KMInputEvent): KMResult + + /** + * Send an input event to the connected clients. + * + * @return whether the input event was consumed by a client. + */ + fun onInputEvent(event: KMInputEvent, detectionSource: InputEventDetectionSource): Boolean } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt index deddfd064d..60191de2d2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt @@ -47,7 +47,6 @@ import kotlinx.coroutines.runBlocking import timber.log.Timber class DetectKeyMapsUseCaseImpl @AssistedInject constructor( - @Assisted private val imeInputEventInjector: ImeInputEventInjector, @Assisted private val accessibilityService: IAccessibilityService, @@ -71,7 +70,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( fun create( accessibilityService: IAccessibilityService, coroutineScope: CoroutineScope, - imeInputEventInjector: ImeInputEventInjector, ): DetectKeyMapsUseCaseImpl } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt index 26aff964e4..adb49b4a78 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.launch class RerouteKeyEventsController @AssistedInject constructor( @Assisted private val coroutineScope: CoroutineScope, - @Assisted private val keyMapperImeMessenger: ImeInputEventInjector, private val useCaseFactory: RerouteKeyEventsUseCaseImpl.Factory, ) { @@ -31,7 +30,6 @@ class RerouteKeyEventsController @AssistedInject constructor( interface Factory { fun create( coroutineScope: CoroutineScope, - keyMapperImeMessenger: ImeInputEventInjector, ): RerouteKeyEventsController } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index 6914cd4bfe..a2dfbdeab0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -11,7 +11,6 @@ import android.graphics.Path import android.graphics.Point import android.os.Build import android.view.KeyEvent -import android.view.MotionEvent import android.view.accessibility.AccessibilityEvent import androidx.core.content.getSystemService import androidx.core.os.bundleOf @@ -22,21 +21,16 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.input.InputEventDetectionSource -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjectorImpl -import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.MathUtils import io.github.sds100.keymapper.common.utils.PinchScreenType import io.github.sds100.keymapper.common.utils.Success -import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter -import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -50,11 +44,6 @@ abstract class BaseAccessibilityService : IAccessibilityService, SavedStateRegistryOwner { - companion object { - - private const val CALLBACK_ID_ACCESSIBILITY_SERVICE = "accessibility_service" - } - @Inject lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl @@ -142,54 +131,6 @@ abstract class BaseAccessibilityService : } } - private val relayServiceCallback: IKeyEventRelayServiceCallback = - object : IKeyEventRelayServiceCallback.Stub() { - override fun onKeyEvent(event: KeyEvent?): Boolean { - event ?: return false - - val device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) } - - return getController() - ?.onKeyEventFromIme( - KMKeyEvent( - keyCode = event.keyCode, - action = event.action, - metaState = event.metaState, - scanCode = event.scanCode, - device = device, - repeatCount = event.repeatCount, - source = event.source, - eventTime = event.eventTime - ), - ) ?: false - } - - override fun onMotionEvent(event: MotionEvent?): Boolean { - event ?: return false - - return getController() - ?.onMotionEventFromIme(KMGamePadEvent(event)) - ?: return false - } - } - - val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl by lazy { - KeyEventRelayServiceWrapperImpl( - ctx = this, - id = CALLBACK_ID_ACCESSIBILITY_SERVICE, - servicePackageName = packageName, - callback = relayServiceCallback, - ) - } - - val imeInputEventInjector by lazy { - ImeInputEventInjectorImpl( - this, - keyEventRelayService = keyEventRelayServiceWrapper, - inputMethodAdapter = inputMethodAdapter, - ) - } - override val lifecycle: Lifecycle get() = lifecycleRegistry @@ -212,8 +153,6 @@ abstract class BaseAccessibilityService : } } } - - keyEventRelayServiceWrapper.onCreate() } override fun onServiceConnected() { @@ -272,8 +211,6 @@ abstract class BaseAccessibilityService : .unregisterFingerprintGestureCallback(fingerprintGestureCallback) } - keyEventRelayServiceWrapper.onDestroy() - Timber.i("Accessibility service: onDestroy") super.onDestroy() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 61c3b8575b..a209b1d33a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -4,14 +4,17 @@ import android.accessibilityservice.AccessibilityServiceInfo import android.content.res.Configuration import android.os.Build import android.view.KeyEvent +import android.view.MotionEvent import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import androidx.lifecycle.lifecycleScope +import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.actions.PerformActionsUseCaseImpl import io.github.sds100.keymapper.base.actions.TestActionEvent import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCaseImpl import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent @@ -22,6 +25,7 @@ import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.trigger.RecordTriggerEvent +import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.minusFlag @@ -32,9 +36,9 @@ import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -71,7 +75,9 @@ abstract class BaseAccessibilityServiceController( private val devicesAdapter: DevicesAdapter, private val suAdapter: SuAdapter, private val settingsRepository: PreferenceRepository, - private val systemBridgeSetupController: SystemBridgeSetupController + private val systemBridgeSetupController: SystemBridgeSetupController, + private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, + private val inputEventHub: InputEventHub ) { companion object { @@ -80,17 +86,16 @@ abstract class BaseAccessibilityServiceController( */ private const val RECORD_TRIGGER_TIMER_LENGTH = 5 private const val DEFAULT_NOTIFICATION_TIMEOUT = 200L + private const val CALLBACK_ID_ACCESSIBILITY_SERVICE = "accessibility_service" } private val performActionsUseCase = performActionsUseCaseFactory.create( accessibilityService = service, - imeInputEventInjector = service.imeInputEventInjector, ) private val detectKeyMapsUseCase = detectKeyMapsUseCaseFactory.create( accessibilityService = service, coroutineScope = service.lifecycleScope, - imeInputEventInjector = service.imeInputEventInjector, ) val detectConstraintsUseCase = detectConstraintsUseCaseFactory.create(service) @@ -109,9 +114,9 @@ abstract class BaseAccessibilityServiceController( detectConstraintsUseCase, ) + // TODO val rerouteKeyEventsController = rerouteKeyEventsControllerFactory.create( service.lifecycleScope, - service.imeInputEventInjector, ) val accessibilityNodeRecorder = accessibilityNodeRecorderFactory.create(service) @@ -192,9 +197,40 @@ abstract class BaseAccessibilityServiceController( private val inputEvents: SharedFlow = service.accessibilityServiceAdapter.eventsToService + private val outputEvents: MutableSharedFlow = service.accessibilityServiceAdapter.eventReceiver + + private val relayServiceCallback: IKeyEventRelayServiceCallback = + object : IKeyEventRelayServiceCallback.Stub() { + override fun onKeyEvent(event: KeyEvent?): Boolean { + event ?: return false + + val device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) } + + return onKeyEventFromIme( + KMKeyEvent( + keyCode = event.keyCode, + action = event.action, + metaState = event.metaState, + scanCode = event.scanCode, + device = device, + repeatCount = event.repeatCount, + source = event.source, + eventTime = event.eventTime + ), + ) + } + + override fun onMotionEvent(event: MotionEvent?): Boolean { + event ?: return false + + return onMotionEventFromIme(KMGamePadEvent(event)) + } + } + + init { serviceFlags.onEach { flags -> // check that it isn't null because this can only be called once the service is bound @@ -360,92 +396,72 @@ abstract class BaseAccessibilityServiceController( denyFingerprintGestureDetection() } } + + keyEventRelayServiceWrapper.registerClient( + CALLBACK_ID_ACCESSIBILITY_SERVICE, + relayServiceCallback + ) } open fun onDestroy() { + keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_ACCESSIBILITY_SERVICE) + accessibilityNodeRecorder.teardown() } open fun onConfigurationChanged(newConfig: Configuration) { } - /** - * Returns an MyKeyEvent which is either the same or more unique - */ - private fun getUniqueEvent(event: KMKeyEvent): KMKeyEvent { - // Guard to ignore processing when not applicable - if (event.keyCode != KeyEvent.KEYCODE_UNKNOWN) return event - - // Don't offset negative values - val scanCodeOffset: Int = if (event.scanCode >= 0) { - InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET - } else { - 0 - } - - val eventProxy = event.copy( - // Fallback to scanCode when keyCode is unknown as it's typically more unique - // Add offset to go past possible keyCode values - keyCode = event.scanCode + scanCodeOffset, - ) - - return eventProxy - } - fun onKeyEvent( event: KMKeyEvent, detectionSource: InputEventDetectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ): Boolean { - val detailedLogInfo = event.toString() - - if (recordingTrigger) { - if (event.action == KeyEvent.ACTION_DOWN) { - Timber.d("Recorded key ${KeyEvent.keyCodeToString(event.keyCode)}, $detailedLogInfo") - - val uniqueEvent: KMKeyEvent = getUniqueEvent(event) - - service.lifecycleScope.launch { - outputEvents.emit( - RecordTriggerEvent.RecordedTriggerKey( - uniqueEvent.keyCode, - uniqueEvent.device, - detectionSource, - ), - ) - } - } - - return true - } - - if (isPaused.value) { - when (event.action) { - KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") - KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") - } - } else { - try { - var consume: Boolean - val uniqueEvent: KMKeyEvent = getUniqueEvent(event) - - consume = keyMapController.onKeyEvent(uniqueEvent) - - if (!consume) { - consume = rerouteKeyEventsController.onKeyEvent(uniqueEvent) - } - - when (uniqueEvent.action) { - KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(uniqueEvent.keyCode)} - consumed: $consume, $detailedLogInfo") - KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(uniqueEvent.keyCode)} - consumed: $consume, $detailedLogInfo") - } - - return consume - } catch (e: Exception) { - Timber.e(e) - } - } - - return false + return inputEventHub.onInputEvent(event, detectionSource) + +// val detailedLogInfo = event.toString() +// +// if (recordingTrigger) { +// // TODO recordtriggercontroller will observe inputeventhub +// if (event.action == KeyEvent.ACTION_DOWN) { +// Timber.d("Recorded key ${KeyEvent.keyCodeToString(event.keyCode)}, $detailedLogInfo") +// +// val uniqueEvent: KMKeyEvent = getUniqueEvent(event) +// +// service.lifecycleScope.launch { +// outputEvents.emit( +// RecordTriggerEvent.RecordedTriggerKey( +// uniqueEvent.keyCode, +// uniqueEvent.device, +// detectionSource, +// ), +// ) +// } +// } +// +// return true +// } +// +// // TODO move paused check to KeyMapController +// if (isPaused.value) { +// when (event.action) { +// KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") +// KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") +// } +// } else { +// try { +// var consume: Boolean +// +// consume = keyMapController.onKeyEvent(uniqueEvent) +// +// if (!consume) { +// consume = rerouteKeyEventsController.onKeyEvent(uniqueEvent) +// } +// +// return consume +// } catch (e: Exception) { +// Timber.e(e) +// } +// } } fun onKeyEventFromIme(event: KMKeyEvent): Boolean { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt index acbae7543f..6ebe31de6e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt @@ -6,6 +6,7 @@ import android.os.Build import android.os.SystemClock import android.view.KeyCharacterMap import android.view.KeyEvent +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.system.inputevents.InputEventInjector import io.github.sds100.keymapper.system.inputevents.createKeyEvent @@ -13,13 +14,16 @@ import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton /** * This class handles communicating with the Key Mapper input method services * so key events and text can be inputted. */ -class ImeInputEventInjectorImpl( - private val ctx: Context, +@Singleton +class ImeInputEventInjectorImpl @Inject constructor( + @ApplicationContext private val ctx: Context, private val keyEventRelayService: KeyEventRelayServiceWrapper, private val inputMethodAdapter: InputMethodAdapter, ) : ImeInputEventInjector { @@ -43,7 +47,7 @@ class ImeInputEventInjectorImpl( private const val CALLBACK_ID_INPUT_METHOD = "input_method" } - // TODO delete this and InputKeyModel + // TODO replace with a method that accepts KMKeyEvent override suspend fun inputKeyEvent(model: InputKeyModel) { Timber.d("Inject key event with input method ${KeyEvent.keyCodeToString(model.keyCode)}, $model") diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt similarity index 95% rename from sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt index 11adfd9381..63ca029459 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SysBridgeHiltModule.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt @@ -12,7 +12,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -abstract class SysBridgeHiltModule { +abstract class SystemBridgeHiltModule { @Singleton @Binds diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt index 24dccae1b1..a6d1e94c93 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt @@ -9,20 +9,17 @@ import android.os.IBinder import android.os.RemoteException import android.view.KeyEvent import android.view.MotionEvent +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.api.IKeyEventRelayService import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback -import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper - -/** - * This handles connecting to the relay service and exposes an interface - * so other parts of the app can get a reference to the service even when it isn't - * bound yet. This class is copied to the Key Mapper GUI Keyboard app as well. - */ -class KeyEventRelayServiceWrapperImpl( - private val ctx: Context, - private val id: String, - private val servicePackageName: String, - private val callback: IKeyEventRelayServiceCallback, +import io.github.sds100.keymapper.common.BuildConfigProvider +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class KeyEventRelayServiceWrapperImpl @Inject constructor( + @ApplicationContext private val ctx: Context, + private val buildConfigProvider: BuildConfigProvider ) : KeyEventRelayServiceWrapper { private val keyEventRelayServiceLock: Any = Any() @@ -36,7 +33,6 @@ class KeyEventRelayServiceWrapperImpl( ) { synchronized(keyEventRelayServiceLock) { keyEventRelayService = IKeyEventRelayService.Stub.asInterface(service) - keyEventRelayService?.registerCallback(callback, id) } } @@ -51,14 +47,6 @@ class KeyEventRelayServiceWrapperImpl( } } - fun onCreate() { - bind() - } - - fun onDestroy() { - unbind() - } - override fun sendKeyEvent( event: KeyEvent, targetPackageName: String, @@ -93,11 +81,22 @@ class KeyEventRelayServiceWrapperImpl( } } - private fun bind() { + override fun registerClient(id: String, callback: IKeyEventRelayServiceCallback) { + keyEventRelayService?.registerCallback(callback, id) + } + + override fun unregisterClient(id: String) { + keyEventRelayService?.unregisterCallback(id) + } + + fun bind() { try { val relayServiceIntent = Intent() val component = - ComponentName(servicePackageName, "io.github.sds100.keymapper.api.KeyEventRelayService") + ComponentName( + buildConfigProvider.packageName, + "io.github.sds100.keymapper.api.KeyEventRelayService" + ) relayServiceIntent.setComponent(component) val isSuccess = ctx.bindService(relayServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE) @@ -111,13 +110,8 @@ class KeyEventRelayServiceWrapperImpl( } } - private fun unbind() { - // Unregister the callback if this input method is unbinding - // from the relay service. This should not happen in onServiceDisconnected - // because the connection is already broken at that point and it - // will fail. + fun unbind() { try { - keyEventRelayService?.unregisterCallback(id) ctx.unbindService(serviceConnection) } catch (e: RemoteException) { // do nothing @@ -129,6 +123,8 @@ class KeyEventRelayServiceWrapperImpl( } interface KeyEventRelayServiceWrapper { + fun registerClient(id: String, callback: IKeyEventRelayServiceCallback) + fun unregisterClient(id: String) fun sendKeyEvent(event: KeyEvent, targetPackageName: String, callbackId: String): Boolean fun sendMotionEvent(event: MotionEvent, targetPackageName: String, callbackId: String): Boolean } \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt index 470d1707c5..d9cc83e40b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt @@ -116,14 +116,8 @@ class KeyMapperImeService : InputMethodService() { } } - private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl by lazy { - KeyEventRelayServiceWrapperImpl( - ctx = this, - id = CALLBACK_ID_INPUT_METHOD, - servicePackageName = buildConfigProvider.packageName, - callback = keyEventReceiverCallback, - ) - } + @Inject + lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper override fun onCreate() { super.onCreate() @@ -141,7 +135,10 @@ class KeyMapperImeService : InputMethodService() { ContextCompat.RECEIVER_NOT_EXPORTED, ) - keyEventRelayServiceWrapper.onCreate() + keyEventRelayServiceWrapper.registerClient( + CALLBACK_ID_INPUT_METHOD, + keyEventReceiverCallback + ) } override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) { @@ -211,7 +208,7 @@ class KeyMapperImeService : InputMethodService() { override fun onDestroy() { unregisterReceiver(broadcastReceiver) - keyEventRelayServiceWrapper.onDestroy() + keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_INPUT_METHOD) super.onDestroy() } From 7ac36c88fe2f86bb58a3e99806993ceee0bfc032 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 Aug 2025 21:20:18 +0100 Subject: [PATCH 058/215] #1394 RecordTriggerController now handles recording keys from the InputEventHub rather than the accessibility service --- .../BaseAccessibilityServiceController.kt | 105 ++---------- .../base/trigger/RecordTriggerController.kt | 150 ++++++++++++++---- .../base/trigger/RecordTriggerEvent.kt | 31 ---- .../base/trigger/TriggerKeyDevice.kt | 1 + .../system/devices/AndroidDevicesAdapter.kt | 4 + .../system/devices/DevicesAdapter.kt | 1 + .../system/inputevents/KMEvdevEvent.kt | 16 +- .../system/inputevents/KMKeyEvent.kt | 6 +- 8 files changed, 153 insertions(+), 161 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index a209b1d33a..8a73450f8a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -20,11 +20,9 @@ import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.keymaps.detection.DetectScreenOffKeyEventsController -import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController -import io.github.sds100.keymapper.base.trigger.RecordTriggerEvent import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.hasFlag @@ -41,7 +39,6 @@ import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -58,7 +55,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -81,10 +77,6 @@ abstract class BaseAccessibilityServiceController( ) { companion object { - /** - * 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 const val CALLBACK_ID_ACCESSIBILITY_SERVICE = "accessibility_service" } @@ -132,14 +124,6 @@ abstract class BaseAccessibilityServiceController( } } - private var recordingTriggerJob: Job? = null - - private val recordingTrigger: Boolean - get() = recordingTriggerJob != null && recordingTriggerJob?.isActive == true - - private val recordDpadMotionEventTracker: DpadMotionEventTracker = - DpadMotionEventTracker() - val isPaused: StateFlow = pauseKeyMapsUseCase.isPaused .stateIn(service.lifecycleScope, SharingStarted.Eagerly, false) @@ -486,47 +470,20 @@ abstract class BaseAccessibilityServiceController( } fun onMotionEventFromIme(event: KMGamePadEvent): Boolean { - if (isPaused.value) { - return false - } - - if (recordingTrigger) { - val dpadKeyEvents = recordDpadMotionEventTracker.convertMotionEvent(event) - - var consume = false - - for (keyEvent in dpadKeyEvents) { - if (keyEvent.action == KeyEvent.ACTION_DOWN) { - Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") - - service.lifecycleScope.launch { - outputEvents.emit( - RecordTriggerEvent.RecordedTriggerKey( - keyEvent.keyCode, - keyEvent.device, - InputEventDetectionSource.INPUT_METHOD, - ), - ) - } - } - - // Consume the key event if it is an DOWN or UP. - consume = true - } - - if (consume) { - return true - } - } - - try { - val consume = keyMapController.onMotionEvent(event) - - return consume - } catch (e: Exception) { - Timber.e(e) - return false - } + // TODO keymapcontroller will observe inputeventhub and check if a trigger is being recorded +// if (isPaused.value || record) { +// return false +// } +// +// try { +// val consume = keyMapController.onMotionEvent(event) +// +// return consume +// } catch (e: Exception) { +// Timber.e(e) +// return false +// } + return false } open fun onAccessibilityEvent(event: AccessibilityEvent) { @@ -591,27 +548,8 @@ abstract class BaseAccessibilityServiceController( open fun onEventFromUi(event: AccessibilityServiceEvent) { Timber.d("Service received event from UI: $event") - when (event) { - is RecordTriggerEvent.StartRecordingTrigger -> - if (!recordingTrigger) { - recordDpadMotionEventTracker.reset() - recordingTriggerJob = recordTriggerJob() - } - - is RecordTriggerEvent.StopRecordingTrigger -> { - val wasRecordingTrigger = recordingTrigger - - recordingTriggerJob?.cancel() - recordingTriggerJob = null - recordDpadMotionEventTracker.reset() - - if (wasRecordingTrigger) { - service.lifecycleScope.launch { - outputEvents.emit(RecordTriggerEvent.OnStoppedRecordingTrigger) - } - } - } + when (event) { is TestActionEvent -> service.lifecycleScope.launch { performActionsUseCase.perform( event.action, @@ -647,19 +585,6 @@ abstract class BaseAccessibilityServiceController( } } - private fun recordTriggerJob() = service.lifecycleScope.launch { - repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> - if (isActive) { - val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration - outputEvents.emit(RecordTriggerEvent.OnIncrementRecordTriggerTimer(timeLeft)) - - delay(1000) - } - } - - outputEvents.emit(RecordTriggerEvent.OnStoppedRecordingTrigger) - } - private fun requestFingerprintGestureDetection() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Timber.d("Accessibility service: request fingerprint gesture detection") diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 5e269944c0..24a85fd439 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -2,74 +2,138 @@ package io.github.sds100.keymapper.base.trigger import android.view.KeyEvent import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHub +import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.isError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter +import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent +import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMInputEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class RecordTriggerControllerImpl @Inject constructor( private val coroutineScope: CoroutineScope, - private val serviceAdapter: AccessibilityServiceAdapter, -) : RecordTriggerController { + private val inputEventHub: InputEventHub, + private val accessibilityServiceAdapter: AccessibilityServiceAdapter, + private val devicesAdapter: DevicesAdapter +) : RecordTriggerController, InputEventHubCallback { + companion object { + /** + * How long should the accessibility service record a trigger in seconds. + */ + private const val RECORD_TRIGGER_TIMER_LENGTH = 5 + private const val INPUT_EVENT_HUB_ID = "record_trigger" + } + override val state = MutableStateFlow(RecordTriggerState.Idle) + private var recordingTriggerJob: Job? = null + + private val recordingTrigger: Boolean + get() = recordingTriggerJob != null && recordingTriggerJob?.isActive == true + private val recordedKeys: MutableList = mutableListOf() override val onRecordKey: MutableSharedFlow = MutableSharedFlow() + private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() - init { - serviceAdapter.eventReceiver.onEach { event -> - when (event) { - is RecordTriggerEvent.OnStoppedRecordingTrigger -> - state.value = - RecordTriggerState.Completed(recordedKeys) + override fun onInputEvent( + event: KMInputEvent, + detectionSource: InputEventDetectionSource + ): Boolean { + if (!recordingTrigger) { + return false + } + + when (event) { + is KMEvdevEvent -> { + if (!event.isKeyEvent || event.androidCode == null) { + return false + } + + val device = devicesAdapter.getInputDevice(event.deviceId) - is RecordTriggerEvent.OnIncrementRecordTriggerTimer -> - state.value = - RecordTriggerState.CountingDown(event.timeLeft) + val recordedKey = createRecordedKey(event.androidCode!!, device, detectionSource) - else -> Unit + onRecordKey(recordedKey) } - }.launchIn(coroutineScope) - - serviceAdapter.eventReceiver - .mapNotNull { - if (it is RecordTriggerEvent.RecordedTriggerKey) { - it - } else { - null + + is KMGamePadEvent -> { + val dpadKeyEvents = dpadMotionEventTracker.convertMotionEvent(event) + + for (keyEvent in dpadKeyEvents) { + if (keyEvent.action == KeyEvent.ACTION_DOWN) { + Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") + + val recordedKey = createRecordedKey( + keyEvent.keyCode, + keyEvent.device, + detectionSource + ) + onRecordKey(recordedKey) + } } } - .map { createRecordedKeyEvent(it.keyCode, it.device, it.detectionSource) } - .onEach { key -> - recordedKeys.add(key) - onRecordKey.emit(key) + + is KMKeyEvent -> { + val recordedKey = createRecordedKey(event.keyCode, event.device, detectionSource) + onRecordKey(recordedKey) } - .launchIn(coroutineScope) + } + + return true } override suspend fun startRecording(): KMResult<*> { + val serviceResult = + accessibilityServiceAdapter.send(AccessibilityServiceEvent.Ping("record_trigger")) + if (serviceResult.isError) { + return serviceResult + } + + if (recordingTrigger) { + return Success(Unit) + } + recordedKeys.clear() dpadMotionEventTracker.reset() - return serviceAdapter.send(RecordTriggerEvent.StartRecordingTrigger) + recordingTriggerJob = recordTriggerJob() + + inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) + + return Success(Unit) } override suspend fun stopRecording(): KMResult<*> { - return serviceAdapter.send(RecordTriggerEvent.StopRecordingTrigger) + recordingTriggerJob?.cancel() + recordingTriggerJob = null + dpadMotionEventTracker.reset() + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + + state.update { RecordTriggerState.Completed(recordedKeys) } + + return Success(Unit) } /** @@ -91,7 +155,7 @@ class RecordTriggerControllerImpl @Inject constructor( } if (keyEvent.action == KeyEvent.ACTION_UP) { - val recordedKey = createRecordedKeyEvent( + val recordedKey = createRecordedKey( keyEvent.keyCode, keyEvent.device, InputEventDetectionSource.INPUT_METHOD, @@ -106,7 +170,12 @@ class RecordTriggerControllerImpl @Inject constructor( return true } - private fun createRecordedKeyEvent( + private fun onRecordKey(recordedKey: RecordedKey) { + recordedKeys.add(recordedKey) + runBlocking { onRecordKey.emit(recordedKey) } + } + + private fun createRecordedKey( keyCode: Int, device: InputDeviceInfo?, detectionSource: InputEventDetectionSource, @@ -119,6 +188,21 @@ class RecordTriggerControllerImpl @Inject constructor( return RecordedKey(keyCode, triggerKeyDevice, detectionSource) } + + private fun recordTriggerJob(): Job = coroutineScope.launch { + repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> + if (isActive) { + val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration + + state.update { RecordTriggerState.CountingDown(timeLeft) } + + delay(1000) + } + } + + stopRecording() + } + } interface RecordTriggerController { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt deleted file mode 100644 index d6bb5a3df8..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -import android.os.Parcelable -import io.github.sds100.keymapper.base.input.InputEventDetectionSource -import io.github.sds100.keymapper.common.utils.InputDeviceInfo -import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable - -sealed class RecordTriggerEvent : AccessibilityServiceEvent() { - @Parcelize - @Serializable - data class RecordedTriggerKey( - val keyCode: Int, - val device: InputDeviceInfo?, - val detectionSource: InputEventDetectionSource, - ) : RecordTriggerEvent(), - Parcelable - - @Serializable - data object StartRecordingTrigger : RecordTriggerEvent() - - @Serializable - data object StopRecordingTrigger : RecordTriggerEvent() - - @Serializable - data class OnIncrementRecordTriggerTimer(val timeLeft: Int) : RecordTriggerEvent() - - @Serializable - data object OnStoppedRecordingTrigger : RecordTriggerEvent() -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt index 131155354b..db9c5a999a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable sealed class TriggerKeyDevice : Comparable { override fun compareTo(other: TriggerKeyDevice) = this.javaClass.name.compareTo(other.javaClass.name) + // TODO add descriptor and device name @Serializable data object Internal : TriggerKeyDevice() diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index 55807178c1..c40dbd4ad7 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -139,6 +139,10 @@ class AndroidDevicesAdapter @Inject constructor( return KMError.DeviceNotFound(descriptor) } + override fun getInputDevice(deviceId: Int): InputDeviceInfo? { + return InputDevice.getDevice(deviceId)?.let { InputDeviceUtils.createInputDeviceInfo(it) } + } + private fun updateInputDevices() { val devices = mutableListOf() diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt index 41df17686b..f82be90881 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt @@ -17,4 +17,5 @@ interface DevicesAdapter { fun deviceHasKey(id: Int, keyCode: Int): Boolean fun getInputDeviceName(descriptor: String): KMResult + fun getInputDevice(deviceId: Int): InputDeviceInfo? } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt index 03e6587109..8ee420c219 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt @@ -7,7 +7,19 @@ data class KMEvdevEvent( val value: Int, // This is only non null when receiving an event. If sending an event - // then the time does not need to be set. + // then these values do not need to be set. + val androidCode: Int? = null, val timeSec: Long? = null, val timeUsec: Long? = null -) : KMInputEvent +) : KMInputEvent { + + // Look at input-event-codes.h for where these are defined. + // EV_SYN + val isSynEvent: Boolean = type == 0 + + // EV_KEY + val isKeyEvent: Boolean = type == 1 + + // EV_REL + val isRelEvent: Boolean = type == 2 +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 41fc86a66c..22fa1163f0 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -24,11 +24,7 @@ data class KMKeyEvent( action = keyEvent.action, metaState = keyEvent.metaState, scanCode = keyEvent.scanCode, - device = if (keyEvent.device == null) { - null - } else { - InputDeviceUtils.createInputDeviceInfo(keyEvent.device) - }, + device = keyEvent.device?.let { InputDeviceUtils.createInputDeviceInfo(it) }, repeatCount = keyEvent.repeatCount, source = keyEvent.source, eventTime = keyEvent.eventTime From 1b8ba4fe979363bb3f231457a058ed98f955c3e9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 6 Aug 2025 00:03:05 +0100 Subject: [PATCH 059/215] #1394 InputEventHub: specify which evdev devices to grab --- .../keymapper/base/input/InputEventHub.kt | 60 +++++++++++++------ .../base/trigger/RecordTriggerController.kt | 10 ++++ .../keymapper/sysbridge/ISystemBridge.aidl | 10 ++-- sysbridge/src/main/cpp/libevdev_jni.cpp | 37 +++++++++--- .../sysbridge/service/SystemBridge.kt | 27 ++++++--- 5 files changed, 109 insertions(+), 35 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 771d68cdfa..78c60f34a9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -9,6 +9,7 @@ import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.success import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository @@ -17,6 +18,7 @@ import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError +import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent @@ -38,14 +40,15 @@ class InputEventHubImpl @Inject constructor( private val coroutineScope: CoroutineScope, private val systemBridgeManager: SystemBridgeManager, private val imeInputEventInjector: ImeInputEventInjector, - private val preferenceRepository: PreferenceRepository + private val preferenceRepository: PreferenceRepository, + private val devicesAdapter: DevicesAdapter ) : InputEventHub, IEvdevCallback.Stub() { companion object { private const val INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2 } - private val clients: ConcurrentHashMap = + private val clients: ConcurrentHashMap = ConcurrentHashMap() private var systemBridge: ISystemBridge? = null @@ -96,6 +99,8 @@ class InputEventHubImpl @Inject constructor( "Evdev event: deviceId=${deviceId}, timeSec=$timeSec, timeUsec=$timeUsec, " + "type=$type, code=$code, value=$value, androidCode=$androidCode" ) + + } override fun onInputEvent( @@ -174,24 +179,27 @@ class InputEventHubImpl @Inject constructor( override fun registerClient( clientId: String, - callback: InputEventHubCallback + callback: InputEventHubCallback, ) { if (clients.contains(clientId)) { throw IllegalArgumentException("This client already has a callback registered!") } - - clients[clientId] = CallbackContext(callback, mutableSetOf()) } override fun unregisterClient(clientId: String) { - // TODO ungrab the evdev devices clients.remove(clientId) + invalidateGrabbedEvdevDevices() } - override fun setCallbackDevices( - clientId: String, - deviceIds: Array - ) { + override fun setGrabbedEvdevDevices(clientId: String, deviceDescriptors: List) { + if (!clients.contains(clientId)) { + throw IllegalArgumentException("This client already has a callback registered!") + } + + clients[clientId] = + clients[clientId]!!.copy(grabbedEvdevDevices = deviceDescriptors.toSet()) + + invalidateGrabbedEvdevDevices() } override suspend fun injectEvent(event: KMInputEvent): KMResult { @@ -210,6 +218,20 @@ class InputEventHubImpl @Inject constructor( } } + private fun invalidateGrabbedEvdevDevices() { + val descriptors: Set = clients.values.flatMap { it.grabbedEvdevDevices }.toSet() + + systemBridge?.ungrabAllEvdevDevices() + + val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() ?: return + + for (descriptor in descriptors) { + val device = inputDevices.find { it.descriptor == descriptor } ?: continue + + systemBridge?.grabEvdevDevice(device.id) + } + } + private fun injectEvdevEvent(event: KMEvdevEvent): KMResult { val systemBridge = this.systemBridge @@ -255,7 +277,7 @@ class InputEventHubImpl @Inject constructor( return Success(true) } else { try { - return systemBridge.injectEvent( + return systemBridge.injectInputEvent( event.toKeyEvent(), INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH ).success() @@ -265,12 +287,12 @@ class InputEventHubImpl @Inject constructor( } } - private class CallbackContext( + private data class ClientContext( val callback: InputEventHubCallback, /** - * The input events from devices that this callback subscribes to. + * The descriptors of the evdev devices that this client wants to grab. */ - val deviceIds: MutableSet + val grabbedEvdevDevices: Set ) } @@ -281,13 +303,17 @@ interface InputEventHub { * come from the key event relay service, accessibility service, or system bridge * depending on the type of event and Key Mapper's permissions. */ - fun registerClient(clientId: String, callback: InputEventHubCallback) + fun registerClient( + clientId: String, + callback: InputEventHubCallback, + ) + fun unregisterClient(clientId: String) /** - * Set the devices that a client wants to listen to. + * Set the evdev devices that a client wants to listen to. */ - fun setCallbackDevices(clientId: String, deviceIds: Array) + fun setGrabbedEvdevDevices(clientId: String, deviceDescriptors: List) /** * Inject an input event. This may either use the key event relay service or the system diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 24a85fd439..b68ddd2301 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -8,6 +8,7 @@ import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.isError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent @@ -118,10 +119,19 @@ class RecordTriggerControllerImpl @Inject constructor( recordedKeys.clear() dpadMotionEventTracker.reset() + recordingTriggerJob = recordTriggerJob() inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) + val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() + + // Grab all evdev devices + if (inputDevices != null) { + val allDeviceDescriptors = inputDevices.map { it.descriptor }.toList() + inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, allDeviceDescriptors) + } + return Success(Unit) } diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 969703f2ad..09b7a8a74d 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -12,8 +12,10 @@ interface ISystemBridge { void destroy() = 16777114; boolean grabEvdevDevice(int deviceId) = 1; - void registerEvdevCallback(IEvdevCallback callback) = 2; - void unregisterEvdevCallback() = 3; - boolean writeEvdevEvent(int deviceId, int type, int code, int value) = 4; - boolean injectEvent(in InputEvent event, int mode) = 5; + boolean ungrabEvdevDevice(int deviceId) = 2; + boolean ungrabAllEvdevDevices() = 3; + void registerEvdevCallback(IEvdevCallback callback) = 4; + void unregisterEvdevCallback() = 5; + boolean writeEvdevEvent(int deviceId, int type, int code, int value) = 6; + boolean injectInputEvent(in InputEvent event, int mode) = 7; } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index f71d80d845..f021551e31 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -47,6 +47,8 @@ struct DeviceContext { struct android::KeyLayoutMap keyLayoutMap; }; +void ungrabDevice(jint device_id); + static int epollFd = -1; static int commandEventFd = -1; @@ -164,7 +166,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { extern "C" JNIEXPORT jboolean JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDevice(JNIEnv *env, +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNative(JNIEnv *env, jobject thiz, jobject jInputDeviceIdentifier) { jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); @@ -373,14 +375,10 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo close(epollFd); } -extern "C" -JNIEXPORT void JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDevice(JNIEnv *env, - jobject thiz, - jint device_id) { +void ungrabDevice(int deviceId) { Command cmd; cmd.type = UNGRAB; - cmd.data = UngrabData{device_id}; + cmd.data = UngrabData{deviceId}; std::lock_guard lock(commandMutex); commandQueue.push(cmd); @@ -393,6 +391,14 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDevice } } +extern "C" +JNIEXPORT void JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, + jobject thiz, + jint device_id) { + ungrabDevice(device_id); +} + extern "C" JNIEXPORT void JNICALL @@ -420,4 +426,21 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNa jint code, jint value) { // TODO: implement writeEvdevEvent() +} +extern "C" +JNIEXPORT void JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDevicesNative( + JNIEnv *env, + jobject thiz) { + std::lock_guard evdevLock(evdevDevicesMutex); + std::vector deviceIds; + + for (auto pair: *evdevDevices) { + deviceIds.push_back(pair.second.deviceId); + } + + std::lock_guard commandlock(commandMutex); + for (int id: deviceIds) { + ungrabDevice(id); + } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 9f28231ca8..e082544189 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -38,11 +38,12 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. // TODO return error code and map this to a SystemBridgeError in key mapper - external fun grabEvdevDevice( + external fun grabEvdevDeviceNative( deviceIdentifier: InputDeviceIdentifier ): Boolean - external fun ungrabEvdevDevice(deviceId: Int) + external fun ungrabEvdevDeviceNative(deviceId: Int) + external fun ungrabAllEvdevDevicesNative() external fun writeEvdevEventNative(deviceId: Int, type: Int, code: Int, value: Int): Boolean external fun startEvdevEventLoop(callback: IBinder) @@ -247,6 +248,21 @@ internal class SystemBridge : ISystemBridge.Stub() { override fun grabEvdevDevice( deviceId: Int, ): Boolean { + grabEvdevDeviceNative(buildInputDeviceIdentifier(deviceId)) + return true + } + + override fun ungrabEvdevDevice(deviceId: Int): Boolean { + ungrabEvdevDeviceNative(deviceId) + return true + } + + override fun ungrabAllEvdevDevices(): Boolean { + ungrabAllEvdevDevicesNative() + return true + } + + private fun buildInputDeviceIdentifier(deviceId: Int): InputDeviceIdentifier { val inputDevice = inputManager.getInputDevice(deviceId) val deviceIdentifier = InputDeviceIdentifier( @@ -258,13 +274,10 @@ internal class SystemBridge : ISystemBridge.Stub() { bus = inputDevice.getDeviceBus(), bluetoothAddress = inputDevice.getBluetoothAddress() ) - - grabEvdevDevice(deviceIdentifier) - - return true + return deviceIdentifier } - override fun injectEvent(event: InputEvent?, mode: Int): Boolean { + override fun injectInputEvent(event: InputEvent?, mode: Int): Boolean { return inputManager.injectInputEvent(event, mode) } From 0df8ed0ae5aa0eb27939bf533be8469321ddd633 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 6 Aug 2025 17:41:41 +0100 Subject: [PATCH 060/215] #1394 AndroidDevicesAdapter: log information about input devices when they update --- .../keymapper/common/utils/InputDeviceUtils.kt | 17 +++++++++++++++++ .../system/devices/AndroidDevicesAdapter.kt | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt index e7f4fa5ef5..6692cf1fbd 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt @@ -6,6 +6,23 @@ import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method object InputDeviceUtils { + val SOURCE_NAMES: Map = mapOf( + InputDevice.SOURCE_DPAD to "DPAD", + InputDevice.SOURCE_GAMEPAD to "GAMEPAD", + InputDevice.SOURCE_JOYSTICK to "JOYSTICK", + InputDevice.SOURCE_KEYBOARD to "KEYBOARD", + InputDevice.SOURCE_MOUSE to "MOUSE", + InputDevice.SOURCE_TOUCHSCREEN to "TOUCHSCREEN", + InputDevice.SOURCE_TOUCHPAD to "TOUCHPAD", + InputDevice.SOURCE_TRACKBALL to "TRACKBALL", + InputDevice.SOURCE_CLASS_BUTTON to "BUTTON", + InputDevice.SOURCE_CLASS_JOYSTICK to "JOYSTICK", + InputDevice.SOURCE_CLASS_POINTER to "POINTER", + InputDevice.SOURCE_CLASS_POSITION to "POSITION", + InputDevice.SOURCE_CLASS_TRACKBALL to "TRACKBALL", + + ) + fun appendDeviceDescriptorToName(descriptor: String, name: String): String = "$name ${descriptor.substring(0..4)}" diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index c40dbd4ad7..0104c51756 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -38,6 +39,7 @@ class AndroidDevicesAdapter @Inject constructor( private val permissionAdapter: PermissionAdapter, private val coroutineScope: CoroutineScope, ) : DevicesAdapter { + private val ctx = context.applicationContext private val inputManager = ctx.getSystemService() @@ -149,6 +151,13 @@ class AndroidDevicesAdapter @Inject constructor( for (id in InputDevice.getDeviceIds()) { val device = InputDevice.getDevice(id) ?: continue + val supportedSources: String = InputDeviceUtils.SOURCE_NAMES + .filter { device.supportsSource(it.key) } + .values + .joinToString() + + Timber.i("Input device: $id ${device.name} Vendor=${device.vendorId} Product=${device.productId} Sources=$supportedSources") + devices.add(InputDeviceUtils.createInputDeviceInfo(device)) } From f170ed90adc1accd96d9dbd7944d2a465c6778e1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 6 Aug 2025 19:23:36 +0100 Subject: [PATCH 061/215] #1394 recording triggers from evdev events works --- .../base/input/EvdevKeyEventTracker.kt | 46 ++++++++++++ .../keymapper/base/input/InputDeviceCache.kt | 37 ++++++++++ .../base/input/InputEventDetectionSource.kt | 2 +- .../keymapper/base/input/InputEventHub.kt | 69 ++++++++++++++---- .../trigger/BaseConfigTriggerViewModel.kt | 7 +- .../base/trigger/RecordTriggerController.kt | 73 +++++++++---------- .../keymapper/common/utils/InputDeviceInfo.kt | 7 +- .../common/utils/InputDeviceUtils.kt | 1 + sysbridge/src/main/cpp/libevdev_jni.cpp | 22 ++++-- .../sysbridge/service/SystemBridge.kt | 24 ++++-- .../system/inputevents/KMEvdevEvent.kt | 2 +- .../system/inputevents/KMGamePadEvent.kt | 2 + .../system/inputevents/KMInputEvent.kt | 4 +- .../system/inputevents/KMKeyEvent.kt | 6 +- 14 files changed, 227 insertions(+), 75 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/InputDeviceCache.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt new file mode 100644 index 0000000000..443fd17e1d --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt @@ -0,0 +1,46 @@ +package io.github.sds100.keymapper.base.input + +import android.os.SystemClock +import android.view.InputDevice +import android.view.KeyEvent +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent + +/** + * This keeps track of which evdev events have been sent so evdev events + * can be converted into key events with the correct metastate. + */ +class EvdevKeyEventTracker( + private val inputDeviceCache: InputDeviceCache +) { + + fun toKeyEvent(event: KMEvdevEvent): KMKeyEvent? { + if (!event.isKeyEvent) { + return null + } + + if (event.androidCode == null) { + return null + } + + val action = when (event.value) { + 0 -> KeyEvent.ACTION_UP + 1 -> KeyEvent.ACTION_DOWN + 2 -> KeyEvent.ACTION_MULTIPLE + else -> throw IllegalArgumentException("Unknown evdev event value for keycode: ${event.value}") + } + + val inputDevice = inputDeviceCache.getById(event.deviceId) + + return KMKeyEvent( + keyCode = event.androidCode!!, + action = action, + metaState = 0, // TODO handle keeping track of metastate + scanCode = event.code, + device = inputDevice, + repeatCount = 0, // TODO does this need handling? + source = inputDevice?.sources ?: InputDevice.SOURCE_UNKNOWN,// TODO + eventTime = SystemClock.uptimeMillis() + ) + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputDeviceCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputDeviceCache.kt new file mode 100644 index 0000000000..3489c1967d --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputDeviceCache.kt @@ -0,0 +1,37 @@ +package io.github.sds100.keymapper.base.input + +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.system.devices.DevicesAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class InputDeviceCache( + private val coroutineScope: CoroutineScope, + private val devicesAdapter: DevicesAdapter +) { + + private val inputDevicesById: StateFlow> = + devicesAdapter.connectedInputDevices + .filterIsInstance>>() + .map { state -> state.data.associateBy { it.id } } + .stateIn(coroutineScope, SharingStarted.Eagerly, emptyMap()) + + private val inputDevicesByDescriptor: StateFlow> = + devicesAdapter.connectedInputDevices + .filterIsInstance>>() + .map { state -> state.data.associateBy { it.descriptor } } + .stateIn(coroutineScope, SharingStarted.Eagerly, emptyMap()) + + fun getById(id: Int): InputDeviceInfo? { + return inputDevicesById.value[id] + } + + fun getByDescriptor(descriptor: String): InputDeviceInfo? { + return inputDevicesByDescriptor.value.values.firstOrNull { it.descriptor == descriptor } + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt index 449b7d614b..882b26c9bf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt @@ -3,6 +3,6 @@ package io.github.sds100.keymapper.base.input enum class InputEventDetectionSource { ACCESSIBILITY_SERVICE, INPUT_METHOD, - SYSTEM_BRIDGE, + EVDEV, MAIN_ACTIVITY } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 78c60f34a9..fe5e39c6ad 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -41,7 +41,7 @@ class InputEventHubImpl @Inject constructor( private val systemBridgeManager: SystemBridgeManager, private val imeInputEventInjector: ImeInputEventInjector, private val preferenceRepository: PreferenceRepository, - private val devicesAdapter: DevicesAdapter + private val devicesAdapter: DevicesAdapter, ) : InputEventHub, IEvdevCallback.Stub() { companion object { @@ -58,6 +58,7 @@ class InputEventHubImpl @Inject constructor( Timber.i("InputEventHub connected to SystemBridge") systemBridge = service + service.registerEvdevCallback(this@InputEventHubImpl) } override fun onServiceDisconnected(service: ISystemBridge) { @@ -71,6 +72,10 @@ class InputEventHubImpl @Inject constructor( } } + private val inputDeviceCache: InputDeviceCache = + InputDeviceCache(coroutineScope, devicesAdapter) + private val evdevKeyEventTracker: EvdevKeyEventTracker = EvdevKeyEventTracker(inputDeviceCache) + private val logInputEventsEnabled: StateFlow = preferenceRepository.get(Keys.log).map { isLogEnabled -> if (isLogEnabled == true) { @@ -95,19 +100,25 @@ class InputEventHubImpl @Inject constructor( value: Int, androidCode: Int ) { - Timber.d( - "Evdev event: deviceId=${deviceId}, timeSec=$timeSec, timeUsec=$timeUsec, " + - "type=$type, code=$code, value=$value, androidCode=$androidCode" - ) + val evdevEvent = KMEvdevEvent(deviceId, type, code, value, androidCode, timeSec, timeUsec) + + if (logInputEventsEnabled.value) { + logInputEvent(evdevEvent) + } + val keyEvent: KMKeyEvent? = evdevKeyEventTracker.toKeyEvent(evdevEvent) + if (keyEvent != null) { + onInputEvent(keyEvent, InputEventDetectionSource.EVDEV) + } else { + onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) + } } override fun onInputEvent( event: KMInputEvent, detectionSource: InputEventDetectionSource ): Boolean { - val uniqueEvent: KMInputEvent = if (event is KMKeyEvent) { makeUniqueKeyEvent(event) } else { @@ -118,7 +129,27 @@ class InputEventHubImpl @Inject constructor( logInputEvent(uniqueEvent) } - return false + if (detectionSource == InputEventDetectionSource.EVDEV && event.deviceId == null) { + return false + } + + var consume = false + + for (clientContext in clients.values) { + if (detectionSource == InputEventDetectionSource.EVDEV) { + val deviceDescriptor = inputDeviceCache.getById(event.deviceId!!)?.descriptor + + // Only send events from evdev devices to the client if they grabbed it + + if (clientContext.grabbedEvdevDevices.contains(deviceDescriptor)) { + consume = consume || clientContext.callback.onInputEvent(event, detectionSource) + } + } else { + consume = consume || clientContext.callback.onInputEvent(event, detectionSource) + } + } + + return consume } /** @@ -181,9 +212,11 @@ class InputEventHubImpl @Inject constructor( clientId: String, callback: InputEventHubCallback, ) { - if (clients.contains(clientId)) { + if (clients.containsKey(clientId)) { throw IllegalArgumentException("This client already has a callback registered!") } + + clients[clientId] = ClientContext(callback, emptySet()) } override fun unregisterClient(clientId: String) { @@ -192,8 +225,8 @@ class InputEventHubImpl @Inject constructor( } override fun setGrabbedEvdevDevices(clientId: String, deviceDescriptors: List) { - if (!clients.contains(clientId)) { - throw IllegalArgumentException("This client already has a callback registered!") + if (!clients.containsKey(clientId)) { + throw IllegalArgumentException("This client is not registered!") } clients[clientId] = @@ -221,14 +254,20 @@ class InputEventHubImpl @Inject constructor( private fun invalidateGrabbedEvdevDevices() { val descriptors: Set = clients.values.flatMap { it.grabbedEvdevDevices }.toSet() - systemBridge?.ungrabAllEvdevDevices() + try { + systemBridge?.ungrabAllEvdevDevices() - val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() ?: return + val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() ?: return - for (descriptor in descriptors) { - val device = inputDevices.find { it.descriptor == descriptor } ?: continue + for (descriptor in descriptors) { + val device = inputDevices.find { it.descriptor == descriptor } ?: continue - systemBridge?.grabEvdevDevice(device.id) + val grabResult = systemBridge?.grabEvdevDevice(device.id) + + Timber.i("Grabbed evdev device ${device.name}: $grabResult") + } + } catch (_: RemoteException) { + Timber.e("Failed to grab device. Is the system bridge dead?") } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 21a1381234..f5b580f2e5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -250,8 +250,9 @@ abstract class BaseConfigTriggerViewModel( FingerprintGestureType.SWIPE_RIGHT to getString(R.string.fingerprint_gesture_right), ) - val selectedType = showDialog("pick_assistant_type", DialogModel.SingleChoice(listItems)) - ?: return@launch + val selectedType = + showDialog("pick_assistant_type", DialogModel.SingleChoice(listItems)) + ?: return@launch config.addFingerprintGesture(type = selectedType) } @@ -610,7 +611,7 @@ abstract class BaseConfigTriggerViewModel( is RecordTriggerState.Completed, RecordTriggerState.Idle, - -> recordTrigger.startRecording() + -> recordTrigger.startRecording() } // Show dialog if the accessibility service is disabled or crashed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index b68ddd2301..6334715d91 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -19,13 +19,13 @@ import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -69,15 +69,9 @@ class RecordTriggerControllerImpl @Inject constructor( when (event) { is KMEvdevEvent -> { - if (!event.isKeyEvent || event.androidCode == null) { - return false - } - - val device = devicesAdapter.getInputDevice(event.deviceId) - - val recordedKey = createRecordedKey(event.androidCode!!, device, detectionSource) - - onRecordKey(recordedKey) + // Do nothing if receiving an evdev event that hasn't already been + // converted into a key event + return false } is KMGamePadEvent -> { @@ -95,15 +89,18 @@ class RecordTriggerControllerImpl @Inject constructor( onRecordKey(recordedKey) } } + return true } is KMKeyEvent -> { - val recordedKey = createRecordedKey(event.keyCode, event.device, detectionSource) - onRecordKey(recordedKey) + if (event.action == KeyEvent.ACTION_DOWN) { + val recordedKey = + createRecordedKey(event.keyCode, event.device, detectionSource) + onRecordKey(recordedKey) + } + return true } } - - return true } override suspend fun startRecording(): KMResult<*> { @@ -117,32 +114,18 @@ class RecordTriggerControllerImpl @Inject constructor( return Success(Unit) } - recordedKeys.clear() - dpadMotionEventTracker.reset() - recordingTriggerJob = recordTriggerJob() - inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) - - val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() - - // Grab all evdev devices - if (inputDevices != null) { - val allDeviceDescriptors = inputDevices.map { it.descriptor }.toList() - inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, allDeviceDescriptors) - } - return Success(Unit) } override suspend fun stopRecording(): KMResult<*> { - recordingTriggerJob?.cancel() - recordingTriggerJob = null dpadMotionEventTracker.reset() inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) - state.update { RecordTriggerState.Completed(recordedKeys) } + recordingTriggerJob?.cancel() + recordingTriggerJob = null return Success(Unit) } @@ -199,20 +182,34 @@ class RecordTriggerControllerImpl @Inject constructor( return RecordedKey(keyCode, triggerKeyDevice, detectionSource) } - private fun recordTriggerJob(): Job = coroutineScope.launch { + // Run on a different thread in case the main thread is locked up while recording and + // the evdev devices aren't ungrabbed. + private fun recordTriggerJob(): Job = coroutineScope.launch(Dispatchers.Default) { + recordedKeys.clear() + dpadMotionEventTracker.reset() + + inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this@RecordTriggerControllerImpl) + + val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() + + // Grab all evdev devices + if (inputDevices != null) { + val allDeviceDescriptors = inputDevices.map { it.descriptor }.toList() + inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, allDeviceDescriptors) + } + repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> - if (isActive) { - val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration + val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration - state.update { RecordTriggerState.CountingDown(timeLeft) } + state.update { RecordTriggerState.CountingDown(timeLeft) } - delay(1000) - } + delay(1000) } - stopRecording() + dpadMotionEventTracker.reset() + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + state.update { RecordTriggerState.Completed(recordedKeys) } } - } interface RecordTriggerController { diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt index 055012e1ae..bb2f541446 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt @@ -12,4 +12,9 @@ data class InputDeviceInfo( val id: Int, val isExternal: Boolean, val isGameController: Boolean, -) : Parcelable \ No newline at end of file + val sources: Int +) : Parcelable { + fun supportsSource(source: Int): Boolean { + return sources and source == source + } +} \ No newline at end of file diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt index 6692cf1fbd..d1e39d6102 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt @@ -32,6 +32,7 @@ object InputDeviceUtils { inputDevice.id, inputDevice.isExternalCompat, isGameController = inputDevice.controllerNumber != 0, + sources = inputDevice.sources ) } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index f021551e31..58b969939b 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -167,8 +167,8 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { extern "C" JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNative(JNIEnv *env, - jobject thiz, - jobject jInputDeviceIdentifier) { + jobject thiz, + jobject jInputDeviceIdentifier) { jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); jfieldID idFieldId = env->GetFieldID(inputDeviceIdentifierClass, "id", "I"); android::InputDeviceIdentifier identifier = convertJInputDeviceIdentifier(env, @@ -333,6 +333,8 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo struct epoll_event events[MAX_EPOLL_EVENTS]; bool running = true; + LOGI("Start evdev event loop"); + while (running) { int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); @@ -376,6 +378,8 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo } void ungrabDevice(int deviceId) { + LOGI("Ungrab device %d", deviceId); + Command cmd; cmd.type = UNGRAB; cmd.data = UngrabData{deviceId}; @@ -394,8 +398,8 @@ void ungrabDevice(int deviceId) { extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, - jobject thiz, - jint device_id) { + jobject thiz, + jint device_id) { ungrabDevice(device_id); } @@ -432,14 +436,16 @@ JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDevicesNative( JNIEnv *env, jobject thiz) { - std::lock_guard evdevLock(evdevDevicesMutex); std::vector deviceIds; - for (auto pair: *evdevDevices) { - deviceIds.push_back(pair.second.deviceId); + { + std::lock_guard evdevLock(evdevDevicesMutex); + + for (auto pair: *evdevDevices) { + deviceIds.push_back(pair.second.deviceId); + } } - std::lock_guard commandlock(commandMutex); for (int id: deviceIds) { ungrabDevice(id); } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index e082544189..6c509a97c4 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -165,14 +165,14 @@ internal class SystemBridge : ISystemBridge.Stub() { private val evdevCallbackLock: Any = Any() private var evdevCallback: IEvdevCallback? = null private val evdevCallbackDeathRecipient: IBinder.DeathRecipient = IBinder.DeathRecipient { - Log.d(TAG, "EvdevCallback binder died") + Log.i(TAG, "EvdevCallback binder died") stopEvdevEventLoop() } init { @SuppressLint("UnsafeDynamicallyLoadedCode") System.load("${System.getProperty("keymapper_sysbridge.library.path")}/libevdev.so") - Log.d(TAG, "SystemBridge started") + Log.i(TAG, "SystemBridge started") waitSystemService("package") waitSystemService(Context.ACTIVITY_SERVICE) @@ -211,7 +211,7 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO ungrab all evdev devices // TODO ungrab all evdev devices if no key mapper app is bound to the service override fun destroy() { - Log.d(TAG, "SystemBridge destroyed") + Log.i(TAG, "SystemBridge destroyed") // Must be last line in this method because it halts the JVM. exitProcess(0) @@ -220,6 +220,8 @@ internal class SystemBridge : ISystemBridge.Stub() { override fun registerEvdevCallback(callback: IEvdevCallback?) { callback ?: return + Log.i(TAG, "Register evdev callback") + val binder = callback.asBinder() if (this.evdevCallback != null) { @@ -248,8 +250,20 @@ internal class SystemBridge : ISystemBridge.Stub() { override fun grabEvdevDevice( deviceId: Int, ): Boolean { - grabEvdevDeviceNative(buildInputDeviceIdentifier(deviceId)) - return true + // Can not filter touchscreens because the volume and power buttons in the emulator come through touchscreen devices. + +// val inputDevice = inputManager.getInputDevice(deviceId); +// +// if (inputDevice == null) { +// return false; +// } +// +// if (inputDevice.supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) { +// Log.e(TAG, "Key Mapper does not permit touchscreens to be grabbed") +// return false; +// } + + return grabEvdevDeviceNative(buildInputDeviceIdentifier(deviceId)) } override fun ungrabEvdevDevice(deviceId: Int): Boolean { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt index 8ee420c219..458c731697 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt @@ -1,7 +1,7 @@ package io.github.sds100.keymapper.system.inputevents data class KMEvdevEvent( - val deviceId: Int, + override val deviceId: Int, val type: Int, val code: Int, val value: Int, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt index cc25146b73..35f12eac84 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt @@ -16,6 +16,8 @@ data class KMGamePadEvent( val axisHatY: Float, ) : KMInputEvent { + override val deviceId: Int? = device?.id + constructor(event: MotionEvent) : this( eventTime = event.eventTime, metaState = event.metaState, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt index 1e12eaed08..7d65550b06 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt @@ -1,3 +1,5 @@ package io.github.sds100.keymapper.system.inputevents -sealed interface KMInputEvent \ No newline at end of file +sealed interface KMInputEvent { + val deviceId: Int? +} \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 22fa1163f0..4de620afb3 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -16,9 +16,11 @@ data class KMKeyEvent( val device: InputDeviceInfo?, val repeatCount: Int, val source: Int, - val eventTime: Long + val eventTime: Long, ) : KMInputEvent { + override val deviceId: Int? = device?.id + constructor(keyEvent: KeyEvent) : this( keyCode = keyEvent.keyCode, action = keyEvent.action, @@ -27,7 +29,7 @@ data class KMKeyEvent( device = keyEvent.device?.let { InputDeviceUtils.createInputDeviceInfo(it) }, repeatCount = keyEvent.repeatCount, source = keyEvent.source, - eventTime = keyEvent.eventTime + eventTime = keyEvent.eventTime, ) fun toKeyEvent(): KeyEvent { From 418a42dd6ca88b5cc69022a3128148559519b347 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 6 Aug 2025 23:10:22 +0100 Subject: [PATCH 062/215] fix (InputEventUtils): add DPAD_CENTER to list of dpad buttons --- .../keymapper/system/inputevents/InputEventUtils.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt index 27b0a8dbea..3507778954 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt @@ -334,7 +334,8 @@ object InputEventUtils { "KEY_SEARCH" to KeyEvent.KEYCODE_SEARCH, ) - fun canDetectKeyWhenScreenOff(keyCode: Int): Boolean = GET_EVENT_LABEL_TO_KEYCODE.any { it.second == keyCode } + fun canDetectKeyWhenScreenOff(keyCode: Int): Boolean = + GET_EVENT_LABEL_TO_KEYCODE.any { it.second == keyCode } val MODIFIER_KEYCODES: Set get() = setOf( @@ -391,7 +392,7 @@ object InputEventUtils { KeyEvent.KEYCODE_BUTTON_14, KeyEvent.KEYCODE_BUTTON_15, KeyEvent.KEYCODE_BUTTON_16, - -> return true + -> return true else -> return false } @@ -454,7 +455,8 @@ object InputEventUtils { code == KeyEvent.KEYCODE_DPAD_UP_LEFT || code == KeyEvent.KEYCODE_DPAD_UP_RIGHT || code == KeyEvent.KEYCODE_DPAD_DOWN_LEFT || - code == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + code == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT || + code == KeyEvent.KEYCODE_DPAD_CENTER } fun isGamepadButton(keyCode: Int): Boolean { @@ -490,7 +492,7 @@ object InputEventUtils { KeyEvent.KEYCODE_BUTTON_14, KeyEvent.KEYCODE_BUTTON_15, KeyEvent.KEYCODE_BUTTON_16, - -> true + -> true else -> false } From d5e24255b0d017e2f8ba33d48c7672ae029f1b29 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 7 Aug 2025 21:31:01 +0100 Subject: [PATCH 063/215] #1394 create EvdevTriggerKey and refactor a lot of code to handle it and input devices are now non null for KMKeyEvents --- .../sds100/keymapper/base/BaseMainActivity.kt | 4 +- .../base/input/EvdevKeyEventTracker.kt | 4 +- .../base/keymaps/ConfigKeyMapUseCase.kt | 123 ++- .../sds100/keymapper/base/keymaps/KeyMap.kt | 6 +- .../base/keymaps/KeyMapListItemCreator.kt | 60 +- .../base/keymaps/KeyMapListViewModel.kt | 3 + .../keymaps/detection/KeyMapController.kt | 109 ++- .../accessibility/BaseAccessibilityService.kt | 4 +- .../BaseAccessibilityServiceController.kt | 47 +- .../trigger/BaseConfigTriggerViewModel.kt | 95 +- .../base/trigger/BaseTriggerScreen.kt | 2 +- .../trigger/ChooseTriggerKeyDeviceModel.kt | 2 +- .../keymapper/base/trigger/EvdevTriggerKey.kt | 74 ++ .../base/trigger/KeyEventTriggerDevice.kt | 40 + ...odeTriggerKey.kt => KeyEventTriggerKey.kt} | 64 +- .../base/trigger/RecordTriggerController.kt | 29 +- .../keymapper/base/trigger/RecordedKey.kt | 5 +- .../sds100/keymapper/base/trigger/Trigger.kt | 11 +- .../base/trigger/TriggerErrorSnapshot.kt | 10 +- .../keymapper/base/trigger/TriggerKey.kt | 3 +- .../base/trigger/TriggerKeyDevice.kt | 48 - .../base/trigger/TriggerKeyListItem.kt | 8 +- .../trigger/TriggerKeyOptionsBottomSheet.kt | 6 +- .../keymapper/base/ConfigKeyMapUseCaseTest.kt | 819 ++++++++++++------ .../base/actions/PerformActionsUseCaseTest.kt | 6 + ...onfigKeyServiceEventActionViewModelTest.kt | 3 + .../keymaps/DpadMotionEventTrackerTest.kt | 2 + .../base/keymaps/KeyMapControllerTest.kt | 265 +++--- .../base/repositories/KeyMapRepositoryTest.kt | 11 - .../base/system/devices/FakeDevicesAdapter.kt | 4 + .../base/trigger/TriggerKeyDeviceTest.kt | 42 + .../keymapper/base/utils/KeyMapUtils.kt | 15 +- .../data/entities/EvdevTriggerKeyEntity.kt | 43 + ...yEntity.kt => KeyEventTriggerKeyEntity.kt} | 6 +- .../data/entities/TriggerKeyEntity.kt | 41 +- .../keymapper/data/migration/Migration1To2.kt | 4 +- .../keymapper/data/migration/Migration6To7.kt | 6 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 2 +- .../system/inputevents/InputEventUtils.kt | 2 +- .../system/inputevents/KMGamePadEvent.kt | 24 +- .../system/inputevents/KMKeyEvent.kt | 30 +- 41 files changed, 1374 insertions(+), 708 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt rename base/src/main/java/io/github/sds100/keymapper/base/trigger/{KeyCodeTriggerKey.kt => KeyEventTriggerKey.kt} (59%) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt create mode 100644 base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt create mode 100644 data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt rename data/src/main/java/io/github/sds100/keymapper/data/entities/{KeyCodeTriggerKeyEntity.kt => KeyEventTriggerKeyEntity.kt} (89%) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 6d0f8fc426..a85a600022 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -216,8 +216,8 @@ abstract class BaseMainActivity : AppCompatActivity() { event ?: return super.onGenericMotionEvent(event) // TODO send this to inputeventhub - val consume = - recordTriggerController.onActivityMotionEvent(KMGamePadEvent(event)) + val gamepadEvent = KMGamePadEvent.fromMotionEvent(event) ?: return false + val consume = recordTriggerController.onActivityMotionEvent(gamepadEvent) return if (consume) { true diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt index 443fd17e1d..1a86d320ef 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt @@ -30,7 +30,7 @@ class EvdevKeyEventTracker( else -> throw IllegalArgumentException("Unknown evdev event value for keycode: ${event.value}") } - val inputDevice = inputDeviceCache.getById(event.deviceId) + val inputDevice = inputDeviceCache.getById(event.deviceId) ?: return null return KMKeyEvent( keyCode = event.androidCode!!, @@ -39,7 +39,7 @@ class EvdevKeyEventTracker( scanCode = event.code, device = inputDevice, repeatCount = 0, // TODO does this need handling? - source = inputDevice?.sources ?: InputDevice.SOURCE_UNKNOWN,// TODO + source = inputDevice.sources ?: InputDevice.SOURCE_UNKNOWN,// TODO eventTime = SystemClock.uptimeMillis() ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt index 818a312875..05a426cad1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt @@ -8,16 +8,16 @@ import io.github.sds100.keymapper.base.constraints.Constraint import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.floating.FloatingButtonEntityMapper -import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.KMResult @@ -253,7 +253,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( // 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? FloatingButtonKey } + .filterIsInstance() .any { keyToCompare -> keyToCompare.buttonUid == buttonUid } val button = floatingButtonRepository.get(buttonUid) @@ -352,10 +352,11 @@ class ConfigKeyMapUseCaseController @Inject constructor( trigger.copy(keys = newKeys, mode = newMode) } - override fun addKeyCodeTriggerKey( + override fun addKeyEventTriggerKey( keyCode: Int, - device: TriggerKeyDevice, - detectionSource: InputEventDetectionSource, + scanCode: Int, + device: KeyEventTriggerDevice, + requiresIme: Boolean ) = editTrigger { trigger -> val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -366,9 +367,9 @@ class ConfigKeyMapUseCaseController @Inject constructor( // 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 } + .filterIsInstance() .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) + keyToCompare.keyCode == keyCode && keyToCompare.device?.isSameDevice(device) == true } var consumeKeyEvent = true @@ -378,15 +379,63 @@ class ConfigKeyMapUseCaseController @Inject constructor( consumeKeyEvent = false } - val triggerKey = KeyCodeTriggerKey( + val triggerKey = KeyEventTriggerKey( keyCode = keyCode, device = device, clickType = clickType, consumeEvent = consumeKeyEvent, - detectionSource = detectionSource, + requiresIme = requiresIme, ) - var newKeys = trigger.keys.plus(triggerKey) + var newKeys = trigger.keys.filter { it !is EvdevTriggerKey }.plus(triggerKey) + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys */ + newKeys.size == 2 && !containsKey -> { + newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } + TriggerMode.Parallel(triggerKey.clickType) + } + + else -> trigger.mode + } + + trigger.copy(keys = newKeys, mode = newMode) + } + + override fun addEvdevTriggerKey( + keyCode: Int, + scanCode: Int, + deviceDescriptor: String, + deviceName: String + ) = editTrigger { trigger -> + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys + .filterIsInstance() + .any { keyToCompare -> + keyToCompare.keyCode == keyCode && keyToCompare.deviceDescriptor == deviceDescriptor + } + + val triggerKey = EvdevTriggerKey( + keyCode = keyCode, + scanCode = scanCode, + deviceDescriptor = deviceDescriptor, + deviceName = deviceName, + clickType = clickType, + consumeEvent = true, + ) + + var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey }.plus(triggerKey) val newMode = when { trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence @@ -452,12 +501,16 @@ class ConfigKeyMapUseCaseController @Inject constructor( 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( + is KeyEventTriggerKey -> Pair( key.keyCode, key.device, ) is FloatingButtonKey -> key.buttonUid + is EvdevTriggerKey -> Pair( + key.keyCode, + key.deviceDescriptor, + ) } } @@ -554,19 +607,19 @@ class ConfigKeyMapUseCaseController @Inject constructor( } } - override fun setTriggerKeyDevice(keyUid: String, device: TriggerKeyDevice) { + override fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) { editTriggerKey(keyUid) { key -> - if (key is KeyCodeTriggerKey) { - key.copy(device = device) - } else { - key + if (key !is KeyEventTriggerKey) { + throw IllegalArgumentException("You can not set the device for non KeyEventTriggerKeys.") } + + key.copy(device = device) } } override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) { editTriggerKey(keyUid) { key -> - if (key is KeyCodeTriggerKey) { + if (key is KeyEventTriggerKey) { key.copy(consumeEvent = consumeKeyEvent) } else { key @@ -648,8 +701,8 @@ class ConfigKeyMapUseCaseController @Inject constructor( editTrigger { it.copy(showToast = enabled) } } - override fun getAvailableTriggerKeyDevices(): List { - val externalTriggerKeyDevices = sequence { + override fun getAvailableTriggerKeyDevices(): List { + val externalKeyEventTriggerDevices = sequence { val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() ?: emptyList() @@ -667,15 +720,15 @@ class ConfigKeyMapUseCaseController @Inject constructor( device.name } - yield(TriggerKeyDevice.External(device.descriptor, name)) + yield(KeyEventTriggerDevice.External(device.descriptor, name)) } } } return sequence { - yield(TriggerKeyDevice.Internal) - yield(TriggerKeyDevice.Any) - yieldAll(externalTriggerKeyDevices) + yield(KeyEventTriggerDevice.Internal) + yield(KeyEventTriggerDevice.Any) + yieldAll(externalKeyEventTriggerDevices) }.toList() } @@ -797,7 +850,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( false } else { trigger.keys - .mapNotNull { it as? KeyCodeTriggerKey } + .mapNotNull { it as? KeyEventTriggerKey } .any { InputEventUtils.isDpadKeyCode(it.keyCode) } } @@ -998,15 +1051,23 @@ interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { suspend fun sendServiceEvent(event: AccessibilityServiceEvent): KMResult<*> // trigger - fun addKeyCodeTriggerKey( + fun addKeyEventTriggerKey( keyCode: Int, - device: TriggerKeyDevice, - detectionSource: InputEventDetectionSource, + scanCode: Int, + device: KeyEventTriggerDevice, + requiresIme: Boolean ) suspend fun addFloatingButtonTriggerKey(buttonUid: String) fun addAssistantTriggerKey(type: AssistantTriggerType) fun addFingerprintGesture(type: FingerprintGestureType) + fun addEvdevTriggerKey( + keyCode: Int, + scanCode: Int, + deviceDescriptor: String, + deviceName: String, + ) + fun removeTriggerKey(uid: String) fun getTriggerKey(uid: String): TriggerKey? fun moveTriggerKey(fromIndex: Int, toIndex: Int) @@ -1024,7 +1085,7 @@ interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { fun setTriggerDoublePress() fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) - fun setTriggerKeyDevice(keyUid: String, device: TriggerKeyDevice) + fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) @@ -1039,7 +1100,7 @@ interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { fun setTriggerFromOtherAppsEnabled(enabled: Boolean) fun setShowToastEnabled(enabled: Boolean) - fun getAvailableTriggerKeyDevices(): List + fun getAvailableTriggerKeyDevices(): List val floatingButtonToUse: MutableStateFlow suspend fun getFloatingLayoutCount(): Int diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt index c0e05384aa..b7d543ef5c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt @@ -67,7 +67,7 @@ fun KeyMap.requiresImeKeyEventForwarding(): Boolean { actionList.any { it.data is ActionData.AnswerCall || it.data is ActionData.EndCall } val hasVolumeKeys = trigger.keys - .mapNotNull { it as? io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey } + .mapNotNull { it as? io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey } .any { it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || it.keyCode == KeyEvent.KEYCODE_VOLUME_UP @@ -83,7 +83,7 @@ fun KeyMap.requiresImeKeyEventForwarding(): Boolean { * is incoming. */ fun KeyMap.requiresImeKeyEventForwardingInPhoneCall(triggerKey: TriggerKey): Boolean { - if (triggerKey !is io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey) { + if (triggerKey !is io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey) { return false } @@ -91,7 +91,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? io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey } + .mapNotNull { it as? io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey } .any { it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || it.keyCode == KeyEvent.KEYCODE_VOLUME_UP diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt index 4359ee8ea2..c6c6ecae72 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt @@ -9,16 +9,17 @@ import io.github.sds100.keymapper.base.actions.ActionUiHelper import io.github.sds100.keymapper.base.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper -import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode import io.github.sds100.keymapper.base.utils.InputEventStrings import io.github.sds100.keymapper.base.utils.isFixable @@ -56,12 +57,14 @@ class KeyMapListItemCreator( val triggerKeys = keyMap.trigger.keys.map { key -> when (key) { is AssistantTriggerKey -> assistantTriggerKeyName(key) - is io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -> keyCodeTriggerKeyName( + is KeyEventTriggerKey -> keyEventTriggerKeyName( key, showDeviceDescriptors, ) + is FloatingButtonKey -> floatingButtonKeyName(key) is FingerprintTriggerKey -> fingerprintKeyName(key) + is EvdevTriggerKey -> evdevTriggerKeyName(key, showDeviceDescriptors) } } @@ -243,8 +246,8 @@ class KeyMapListItemCreator( } } - private fun keyCodeTriggerKeyName( - key: io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey, + private fun keyEventTriggerKeyName( + key: KeyEventTriggerKey, showDeviceDescriptors: Boolean, ): String = buildString { when (key.clickType) { @@ -256,9 +259,9 @@ class KeyMapListItemCreator( append(InputEventStrings.keyCodeToString(key.keyCode)) val deviceName = when (key.device) { - is TriggerKeyDevice.Internal -> null - is TriggerKeyDevice.Any -> getString(R.string.any_device) - is TriggerKeyDevice.External -> { + is KeyEventTriggerDevice.Internal -> null + is KeyEventTriggerDevice.Any -> getString(R.string.any_device) + is KeyEventTriggerDevice.External -> { if (showDeviceDescriptors) { InputDeviceUtils.appendDeviceDescriptorToName( key.device.descriptor, @@ -272,8 +275,8 @@ class KeyMapListItemCreator( val parts = mutableListOf() - if (deviceName != null || key.detectionSource == InputEventDetectionSource.INPUT_METHOD || !key.consumeEvent) { - if (key.detectionSource == InputEventDetectionSource.INPUT_METHOD) { + if (deviceName != null || key.requiresIme || !key.consumeEvent) { + if (key.requiresIme) { parts.add(getString(R.string.flag_detect_from_input_method)) } @@ -293,6 +296,43 @@ class KeyMapListItemCreator( } } + private fun evdevTriggerKeyName( + key: EvdevTriggerKey, + showDeviceDescriptors: Boolean, + ): String = buildString { + when (key.clickType) { + ClickType.LONG_PRESS -> append(longPressString).append(" ") + ClickType.DOUBLE_PRESS -> append(doublePressString).append(" ") + else -> Unit + } + + append(InputEventStrings.keyCodeToString(key.keyCode)) + + val deviceName = if (showDeviceDescriptors) { + InputDeviceUtils.appendDeviceDescriptorToName( + key.deviceDescriptor, + key.deviceName, + ) + } else { + key.deviceName + } + + + val parts = buildList { + add(deviceName) + + if (!key.consumeEvent) { + add(getString(R.string.flag_dont_override_default_action)) + } + } + + if (parts.isNotEmpty()) { + append(" (") + append(parts.joinToString(separator = " $midDot ")) + append(")") + } + } + private fun assistantTriggerKeyName(key: AssistantTriggerKey): String = buildString { when (key.clickType) { ClickType.DOUBLE_PRESS -> append(doublePressString).append(" ") diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt index 90c49ecea1..7f3749effe 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt @@ -360,6 +360,9 @@ class KeyMapListViewModel( onAutomaticBackupResult(result) } } + + // TODO REMOVE + onNewKeyMapClick() } private fun buildSelectingAppBarState( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt index c6078bb9ca..09c853d485 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt @@ -12,16 +12,19 @@ import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.constraints.isSatisfied import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHub +import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.minusFlag @@ -29,10 +32,12 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -44,7 +49,9 @@ class KeyMapController( private val useCase: DetectKeyMapsUseCase, private val performActionsUseCase: PerformActionsUseCase, private val detectConstraints: DetectConstraintsUseCase, -) { + private val inputEventHub: InputEventHub, + isPausedFlow: Flow +) : InputEventHubCallback { companion object { // the states for keys awaiting a double press @@ -63,6 +70,8 @@ class KeyMapController( ) || trigger.mode is TriggerMode.Parallel + + private const val INPUT_EVENT_HUB_ID = "key_map_controller" } private fun loadKeyMaps(value: List) { @@ -84,7 +93,7 @@ class KeyMapController( } else { detectKeyMaps = true - val longPressSequenceTriggerKeys = mutableListOf() + val longPressSequenceTriggerKeys = mutableListOf() val doublePressKeys = mutableListOf() @@ -102,7 +111,7 @@ class KeyMapController( val parallelTriggerActionPerformers = mutableMapOf() val parallelTriggerModifierKeyIndices = mutableListOf>() - val triggerKeysThatSendRepeatedKeyEvents = mutableSetOf() + val triggerKeysThatSendRepeatedKeyEvents = mutableSetOf() // Only process key maps that can be triggered val validKeyMaps = value.filter { @@ -112,14 +121,14 @@ class KeyMapController( for ((triggerIndex, model) in validKeyMaps.withIndex()) { val keyMap = model.keyMap // TRIGGER STUFF - keyMap.trigger.keys.forEachIndexed { keyIndex, key -> - if (key is KeyCodeTriggerKey && key.detectionSource == InputEventDetectionSource.INPUT_METHOD && key.consumeEvent) { + for ((keyIndex, key) in keyMap.trigger.keys.withIndex()) { + if (key is KeyEventTriggerKey && key.requiresIme && key.consumeEvent) { triggerKeysThatSendRepeatedKeyEvents.add(key) } if (keyMap.trigger.mode == TriggerMode.Sequence && key.clickType == ClickType.LONG_PRESS && - key is KeyCodeTriggerKey + key is KeyEventTriggerKey ) { if (keyMap.trigger.keys.size > 1) { longPressSequenceTriggerKeys.add(key) @@ -133,17 +142,17 @@ class KeyMapController( } when (key) { - is KeyCodeTriggerKey -> when (key.device) { - TriggerKeyDevice.Internal -> { + is KeyEventTriggerKey -> when (key.device) { + KeyEventTriggerDevice.Internal -> { detectInternalEvents = true } - TriggerKeyDevice.Any -> { + KeyEventTriggerDevice.Any -> { detectInternalEvents = true detectExternalEvents = true } - is TriggerKeyDevice.External -> { + is KeyEventTriggerDevice.External -> { detectExternalEvents = true } } @@ -328,7 +337,7 @@ class KeyMapController( val trigger = triggers[triggerIndex] trigger.keys.forEachIndexed { keyIndex, key -> - if (key is KeyCodeTriggerKey && isModifierKey(key.keyCode)) { + if (key is KeyEventTriggerKey && isModifierKey(key.keyCode)) { parallelTriggerModifierKeyIndices.add(triggerIndex to keyIndex) } } @@ -336,6 +345,10 @@ class KeyMapController( reset() + triggers.flatMap { trigger -> + trigger.keys.filterIsInstance() + }.toList() + this.triggers = triggers.toTypedArray() this.triggerActions = triggerActions.toTypedArray() this.triggerConstraints = triggerConstraints.toTypedArray() @@ -372,7 +385,7 @@ class KeyMapController( this.triggerKeysThatSendRepeatedKeyEvents = triggerKeysThatSendRepeatedKeyEvents - reset() + } } @@ -385,7 +398,7 @@ class KeyMapController( /** * All sequence events that have the long press click type. */ - private var longPressSequenceTriggerKeys: Array = arrayOf() + private var longPressSequenceTriggerKeys: Array = arrayOf() /** * All double press keys and the index of their corresponding trigger. first is the event and second is @@ -513,7 +526,7 @@ class KeyMapController( * * NOTE: This only contains the trigger keys that are flagged to consume the key event. */ - private var triggerKeysThatSendRepeatedKeyEvents: Set = emptySet() + private var triggerKeysThatSendRepeatedKeyEvents: Set = emptySet() private var parallelTriggerActionPerformers: Map = emptyMap() @@ -560,6 +573,9 @@ class KeyMapController( private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() + private val isPaused: StateFlow = + isPausedFlow.stateIn(coroutineScope, SharingStarted.Eagerly, true) + init { coroutineScope.launch { useCase.allKeyMapList.collectLatest { keyMapList -> @@ -567,6 +583,28 @@ class KeyMapController( loadKeyMaps(keyMapList) } } + + inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) + } + + fun teardown() { + reset() + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + } + + override fun onInputEvent( + event: KMInputEvent, + detectionSource: InputEventDetectionSource + ): Boolean { + if (isPaused.value) { + return false + } + + if (event is KMKeyEvent) { + return onKeyEvent(event) + } else { + return false + } } fun onMotionEvent(event: KMGamePadEvent): Boolean { @@ -603,10 +641,8 @@ class KeyMapController( val device = keyEvent.device - if (device != null) { - if ((device.isExternal && !detectExternalEvents) || (!device.isExternal && !detectInternalEvents)) { - return false - } + if ((device.isExternal && !detectExternalEvents) || (!device.isExternal && !detectInternalEvents)) { + return false } return onKeyEventPostFilter(keyEvent) @@ -619,7 +655,7 @@ class KeyMapController( for ((triggerIndex, eventIndex) in parallelTriggerModifierKeyIndices) { val key = triggers[triggerIndex].keys[eventIndex] - if (key !is KeyCodeTriggerKey) { + if (key !is KeyEventTriggerKey) { continue } @@ -631,7 +667,7 @@ class KeyMapController( val device = keyEvent.device - val event = if (device != null && device.isExternal) { + val event = if (device.isExternal) { KeyCodeEvent( keyCode = keyEvent.keyCode, clickType = null, @@ -734,7 +770,7 @@ class KeyMapController( consumeEvent = true } - key is KeyCodeTriggerKey && event is KeyCodeEvent -> + key is KeyEventTriggerKey && event is KeyCodeEvent -> if (key.keyCode == event.keyCode && key.consumeEvent) { consumeEvent = true } @@ -1511,6 +1547,8 @@ class KeyMapController( performActionsAfterSequenceTriggerTimeout.forEach { (_, job) -> job.cancel() } performActionsAfterSequenceTriggerTimeout.clear() + + inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList()) } /** @@ -1663,20 +1701,22 @@ class KeyMapController( } private fun TriggerKey.matchesEvent(event: Event): Boolean { - if (this is KeyCodeTriggerKey && event is KeyCodeEvent) { + if (this is KeyEventTriggerKey && event is KeyCodeEvent) { return when (this.device) { - TriggerKeyDevice.Any -> this.keyCode == event.keyCode && this.clickType == event.clickType - is TriggerKeyDevice.External -> + KeyEventTriggerDevice.Any -> this.keyCode == event.keyCode && this.clickType == event.clickType + is KeyEventTriggerDevice.External -> this.keyCode == event.keyCode && event.descriptor != null && event.descriptor == this.device.descriptor && this.clickType == event.clickType - TriggerKeyDevice.Internal -> + KeyEventTriggerDevice.Internal -> this.keyCode == event.keyCode && event.descriptor == null && this.clickType == event.clickType } + } else if (this is EvdevTriggerKey && event is KeyCodeEvent) { + return this.keyCode == event.keyCode && event.clickType == this.clickType && event.descriptor == this.deviceDescriptor } else if (this is AssistantTriggerKey && event is AssistantEvent) { return if (this.type == AssistantTriggerType.ANY || event.type == AssistantTriggerType.ANY) { this.clickType == event.clickType @@ -1693,20 +1733,20 @@ class KeyMapController( } private fun TriggerKey.matchesWithOtherKey(otherKey: TriggerKey): Boolean { - if (this is KeyCodeTriggerKey && otherKey is KeyCodeTriggerKey) { + if (this is KeyEventTriggerKey && otherKey is KeyEventTriggerKey) { return when (this.device) { - TriggerKeyDevice.Any -> + KeyEventTriggerDevice.Any -> this.keyCode == otherKey.keyCode && this.clickType == otherKey.clickType - is TriggerKeyDevice.External -> + is KeyEventTriggerDevice.External -> this.keyCode == otherKey.keyCode && this.device == otherKey.device && this.clickType == otherKey.clickType - TriggerKeyDevice.Internal -> + KeyEventTriggerDevice.Internal -> this.keyCode == otherKey.keyCode && - otherKey.device == TriggerKeyDevice.Internal && + otherKey.device == KeyEventTriggerDevice.Internal && this.clickType == otherKey.clickType } } else if (this is AssistantTriggerKey && otherKey is AssistantTriggerKey) { @@ -1758,7 +1798,7 @@ class KeyMapController( KeyEvent.KEYCODE_SYM, KeyEvent.KEYCODE_NUM, KeyEvent.KEYCODE_FUNCTION, - -> true + -> true else -> false } @@ -1789,9 +1829,6 @@ class KeyMapController( private data class KeyCodeEvent( val keyCode: Int, override val clickType: ClickType?, - /** - * null if not an external device - */ val descriptor: String?, val deviceId: Int, val scanCode: Int, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index a2dfbdeab0..d31d24a081 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -244,8 +244,10 @@ abstract class BaseAccessibilityService : override fun onKeyEvent(event: KeyEvent?): Boolean { event ?: return super.onKeyEvent(event) + val kmKeyEvent = KMKeyEvent.fromKeyEvent(event) ?: return false + return getController()?.onKeyEvent( - KMKeyEvent(event), + kmKeyEvent, InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) ?: false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 8a73450f8a..853b2fe59b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -23,7 +23,6 @@ import io.github.sds100.keymapper.base.keymaps.detection.DetectScreenOffKeyEvent import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController -import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.minusFlag @@ -97,6 +96,8 @@ abstract class BaseAccessibilityServiceController( detectKeyMapsUseCase, performActionsUseCase, detectConstraintsUseCase, + inputEventHub, + pauseKeyMapsUseCase.isPaused ) val triggerKeyMapFromOtherAppsController = TriggerKeyMapFromOtherAppsController( @@ -191,26 +192,16 @@ abstract class BaseAccessibilityServiceController( override fun onKeyEvent(event: KeyEvent?): Boolean { event ?: return false - val device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) } - - return onKeyEventFromIme( - KMKeyEvent( - keyCode = event.keyCode, - action = event.action, - metaState = event.metaState, - scanCode = event.scanCode, - device = device, - repeatCount = event.repeatCount, - source = event.source, - eventTime = event.eventTime - ), - ) + val kmKeyEvent = KMKeyEvent.fromKeyEvent(event) ?: return false + return onKeyEventFromIme(kmKeyEvent) } override fun onMotionEvent(event: MotionEvent?): Boolean { event ?: return false - return onMotionEventFromIme(KMGamePadEvent(event)) + val gamePadEvent = KMGamePadEvent.fromMotionEvent(event) + ?: return false + return onMotionEventFromIme(gamePadEvent) } } @@ -388,8 +379,8 @@ abstract class BaseAccessibilityServiceController( } open fun onDestroy() { + keyMapController.teardown() keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_ACCESSIBILITY_SERVICE) - accessibilityNodeRecorder.teardown() } @@ -404,27 +395,6 @@ abstract class BaseAccessibilityServiceController( // val detailedLogInfo = event.toString() // -// if (recordingTrigger) { -// // TODO recordtriggercontroller will observe inputeventhub -// if (event.action == KeyEvent.ACTION_DOWN) { -// Timber.d("Recorded key ${KeyEvent.keyCodeToString(event.keyCode)}, $detailedLogInfo") -// -// val uniqueEvent: KMKeyEvent = getUniqueEvent(event) -// -// service.lifecycleScope.launch { -// outputEvents.emit( -// RecordTriggerEvent.RecordedTriggerKey( -// uniqueEvent.keyCode, -// uniqueEvent.device, -// detectionSource, -// ), -// ) -// } -// } -// -// return true -// } -// // // TODO move paused check to KeyMapController // if (isPaused.value) { // when (event.action) { @@ -448,6 +418,7 @@ abstract class BaseAccessibilityServiceController( // } } + // TODO handle somewhere else fun onKeyEventFromIme(event: KMKeyEvent): Boolean { /* Issue #850 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index f5b580f2e5..2719744426 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -23,6 +23,7 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.ProductId import io.github.sds100.keymapper.base.purchasing.PurchasingManager import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.base.trigger.TriggerKeyListItemModel.* import io.github.sds100.keymapper.base.utils.InputEventStrings import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem @@ -287,7 +288,6 @@ abstract class BaseConfigTriggerViewModel( createListItems( keyMap, showDeviceDescriptors, - triggerKeyShortcuts.size, triggerErrorSnapshot, ) val isReorderingEnabled = trigger.keys.size > 1 @@ -355,11 +355,11 @@ abstract class BaseConfigTriggerViewModel( val showClickTypes = trigger.mode is TriggerMode.Sequence when (key) { - is KeyCodeTriggerKey -> { + is KeyEventTriggerKey -> { val showDeviceDescriptors = displayKeyMap.showDeviceDescriptors.first() val deviceListItems: List = config.getAvailableTriggerKeyDevices() - .map { device: TriggerKeyDevice -> + .map { device: KeyEventTriggerDevice -> buildDeviceListItem( device = device, showDeviceDescriptors = showDeviceDescriptors, @@ -367,7 +367,7 @@ abstract class BaseConfigTriggerViewModel( ) } - return TriggerKeyOptionsState.KeyCode( + return TriggerKeyOptionsState.KeyEvent( doNotRemapChecked = !key.consumeEvent, clickType = key.clickType, showClickTypes = showClickTypes, @@ -396,30 +396,38 @@ abstract class BaseConfigTriggerViewModel( clickType = key.clickType, ) } + + is EvdevTriggerKey -> { + return TriggerKeyOptionsState.EvdevEvent( + doNotRemapChecked = !key.consumeEvent, + clickType = key.clickType, + showClickTypes = showClickTypes + ) + } } } } } private fun buildDeviceListItem( - device: TriggerKeyDevice, + device: KeyEventTriggerDevice, isChecked: Boolean, showDeviceDescriptors: Boolean, ): CheckBoxListItem { return when (device) { - TriggerKeyDevice.Any -> CheckBoxListItem( + KeyEventTriggerDevice.Any -> CheckBoxListItem( id = DEVICE_ID_ANY, isChecked = isChecked, label = getString(R.string.any_device), ) - TriggerKeyDevice.Internal -> CheckBoxListItem( + KeyEventTriggerDevice.Internal -> CheckBoxListItem( id = DEVICE_ID_INTERNAL, isChecked = isChecked, label = getString(R.string.this_device), ) - is TriggerKeyDevice.External -> { + is KeyEventTriggerDevice.External -> { val name = if (showDeviceDescriptors) { InputDeviceUtils.appendDeviceDescriptorToName( device.descriptor, @@ -472,7 +480,29 @@ abstract class BaseConfigTriggerViewModel( private suspend fun onRecordTriggerKey(key: RecordedKey) { // Add the trigger key before showing the dialog so it doesn't // need to be dismissed before it is added. - config.addKeyCodeTriggerKey(key.keyCode, key.device, key.detectionSource) + when (key.detectionSource) { + InputEventDetectionSource.EVDEV -> config.addEvdevTriggerKey( + key.keyCode, + key.scanCode, + key.deviceDescriptor, + key.deviceName + ) + + InputEventDetectionSource.ACCESSIBILITY_SERVICE, + InputEventDetectionSource.INPUT_METHOD, + InputEventDetectionSource.MAIN_ACTIVITY -> { + val triggerDevice = if (key.isExternalDevice) { + KeyEventTriggerDevice.External(key.deviceDescriptor, key.deviceName) + } else { + KeyEventTriggerDevice.Internal + } + + config.addKeyEventTriggerKey( + key.keyCode, key.scanCode, triggerDevice, + key.detectionSource != InputEventDetectionSource.ACCESSIBILITY_SERVICE + ) + } + } if (key.keyCode >= InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET || key.keyCode < 0) { if (onboarding.shownKeyCodeToScanCodeTriggerExplanation) { @@ -565,15 +595,15 @@ abstract class BaseConfigTriggerViewModel( fun onSelectTriggerKeyDevice(descriptor: String) { triggerKeyOptionsUid.value?.let { triggerKeyUid -> val device = when (descriptor) { - DEVICE_ID_ANY -> TriggerKeyDevice.Any - DEVICE_ID_INTERNAL -> TriggerKeyDevice.Internal + DEVICE_ID_ANY -> KeyEventTriggerDevice.Any + DEVICE_ID_INTERNAL -> KeyEventTriggerDevice.Internal else -> { val device = config.getAvailableTriggerKeyDevices() - .filterIsInstance() + .filterIsInstance() .firstOrNull { it.descriptor == descriptor } ?: return - TriggerKeyDevice.External( + KeyEventTriggerDevice.External( device.descriptor, device.name, ) @@ -660,7 +690,6 @@ abstract class BaseConfigTriggerViewModel( private fun createListItems( keyMap: KeyMap, showDeviceDescriptors: Boolean, - shortcutCount: Int, triggerErrorSnapshot: TriggerErrorSnapshot, ): List { val trigger = keyMap.trigger @@ -681,7 +710,7 @@ abstract class BaseConfigTriggerViewModel( } when (key) { - is AssistantTriggerKey -> TriggerKeyListItemModel.Assistant( + is AssistantTriggerKey -> Assistant( id = key.uid, assistantType = key.type, clickType = clickType, @@ -689,7 +718,7 @@ abstract class BaseConfigTriggerViewModel( error = error, ) - is FingerprintTriggerKey -> TriggerKeyListItemModel.FingerprintGesture( + is FingerprintTriggerKey -> FingerprintGesture( id = key.uid, gestureType = key.type, clickType = clickType, @@ -697,7 +726,7 @@ abstract class BaseConfigTriggerViewModel( error = error, ) - is KeyCodeTriggerKey -> TriggerKeyListItemModel.KeyCode( + is KeyEventTriggerKey -> KeyEvent( id = key.uid, keyName = getTriggerKeyName(key), clickType = clickType, @@ -711,13 +740,13 @@ abstract class BaseConfigTriggerViewModel( is FloatingButtonKey -> { if (key.button == null) { - TriggerKeyListItemModel.FloatingButtonDeleted( + FloatingButtonDeleted( id = key.uid, clickType = clickType, linkType = linkType, ) } else { - TriggerKeyListItemModel.FloatingButton( + FloatingButton( id = key.uid, buttonName = key.button.appearance.text, layoutName = key.button.layoutName, @@ -727,12 +756,14 @@ abstract class BaseConfigTriggerViewModel( ) } } + + is EvdevTriggerKey -> TODO() } } } private fun getTriggerKeyExtraInfo( - key: KeyCodeTriggerKey, + key: KeyEventTriggerKey, showDeviceDescriptors: Boolean, ): String { return buildString { @@ -745,11 +776,11 @@ abstract class BaseConfigTriggerViewModel( } } - private fun getTriggerKeyName(key: KeyCodeTriggerKey): String { + private fun getTriggerKeyName(key: KeyEventTriggerKey): String { return buildString { append(InputEventStrings.keyCodeToString(key.keyCode)) - if (key.detectionSource == InputEventDetectionSource.INPUT_METHOD) { + if (key.requiresIme) { val midDot = getString(R.string.middot) append(" $midDot ${getString(R.string.flag_detect_from_input_method)}") } @@ -757,12 +788,12 @@ abstract class BaseConfigTriggerViewModel( } private fun getTriggerKeyDeviceName( - device: TriggerKeyDevice, + device: KeyEventTriggerDevice, showDeviceDescriptors: Boolean, ): String = when (device) { - is TriggerKeyDevice.Internal -> getString(R.string.this_device) - is TriggerKeyDevice.Any -> getString(R.string.any_device) - is TriggerKeyDevice.External -> { + is KeyEventTriggerDevice.Internal -> getString(R.string.this_device) + is KeyEventTriggerDevice.Any -> getString(R.string.any_device) + is KeyEventTriggerDevice.External -> { if (showDeviceDescriptors) { InputDeviceUtils.appendDeviceDescriptorToName( device.descriptor, @@ -837,7 +868,7 @@ sealed class TriggerKeyListItemModel { abstract val error: TriggerError? abstract val clickType: ClickType - data class KeyCode( + data class KeyEvent( override val id: String, override val linkType: LinkType, val keyName: String, @@ -886,7 +917,7 @@ sealed class TriggerKeyOptionsState { abstract val showClickTypes: Boolean abstract val showLongPressClickType: Boolean - data class KeyCode( + data class KeyEvent( val doNotRemapChecked: Boolean = false, override val clickType: ClickType, override val showClickTypes: Boolean, @@ -895,6 +926,14 @@ sealed class TriggerKeyOptionsState { override val showLongPressClickType: Boolean = true } + data class EvdevEvent( + val doNotRemapChecked: Boolean = false, + override val clickType: ClickType, + override val showClickTypes: Boolean, + ) : TriggerKeyOptionsState() { + override val showLongPressClickType: Boolean = true + } + data class Assistant( val assistantType: AssistantTriggerType, override val clickType: ClickType, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 48b54e7ae8..97b6f78c6a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -580,7 +580,7 @@ private fun TriggerModeRadioGroup( } private val sampleList = listOf( - TriggerKeyListItemModel.KeyCode( + TriggerKeyListItemModel.KeyEvent( id = "id1", keyName = "Volume Up", clickType = ClickType.SHORT_PRESS, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ChooseTriggerKeyDeviceModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ChooseTriggerKeyDeviceModel.kt index efc216cd3f..be95ca0e54 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ChooseTriggerKeyDeviceModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ChooseTriggerKeyDeviceModel.kt @@ -2,5 +2,5 @@ package io.github.sds100.keymapper.base.trigger data class ChooseTriggerKeyDeviceModel( val triggerKeyUid: String, - val devices: List, + val devices: List, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt new file mode 100644 index 0000000000..1fdd315d53 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt @@ -0,0 +1,74 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.common.utils.hasFlag +import io.github.sds100.keymapper.common.utils.withFlag +import io.github.sds100.keymapper.data.entities.EvdevTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.TriggerKeyEntity +import java.util.UUID + +/** + * This must be a different class to KeyEventTriggerKey because trigger keys from evdev events + * must come from one device, even if it is internal, and can not come from any device. The input + * devices must be grabbed so that Key Mapper can remap them. + */ +data class EvdevTriggerKey( + override val uid: String = UUID.randomUUID().toString(), + val keyCode: Int, + val scanCode: Int, + val deviceDescriptor: String, + val deviceName: String, + override val clickType: ClickType = ClickType.SHORT_PRESS, + override val consumeEvent: Boolean = true, +) : TriggerKey() { + override val allowedDoublePress: Boolean = true + override val allowedLongPress: Boolean = true + + companion object { + fun fromEntity(entity: EvdevTriggerKeyEntity): TriggerKey { + val clickType = when (entity.clickType) { + TriggerKeyEntity.SHORT_PRESS -> ClickType.SHORT_PRESS + TriggerKeyEntity.LONG_PRESS -> ClickType.LONG_PRESS + TriggerKeyEntity.DOUBLE_PRESS -> ClickType.DOUBLE_PRESS + else -> ClickType.SHORT_PRESS + } + + val consumeEvent = + !entity.flags.hasFlag(EvdevTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + + return EvdevTriggerKey( + uid = entity.uid, + keyCode = entity.keyCode, + scanCode = entity.scanCode, + deviceDescriptor = entity.deviceDescriptor, + deviceName = entity.deviceName, + clickType = clickType, + consumeEvent = consumeEvent, + ) + } + + fun toEntity(key: EvdevTriggerKey): EvdevTriggerKeyEntity { + val clickType = when (key.clickType) { + ClickType.SHORT_PRESS -> TriggerKeyEntity.SHORT_PRESS + ClickType.LONG_PRESS -> TriggerKeyEntity.LONG_PRESS + ClickType.DOUBLE_PRESS -> TriggerKeyEntity.DOUBLE_PRESS + } + + var flags = 0 + + if (!key.consumeEvent) { + flags = flags.withFlag(EvdevTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + } + + return EvdevTriggerKeyEntity( + keyCode = key.keyCode, + scanCode = key.scanCode, + deviceDescriptor = key.deviceDescriptor, + deviceName = key.deviceName, + clickType = clickType, + flags = flags, + uid = key.uid + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt new file mode 100644 index 0000000000..2bb4b456b8 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt @@ -0,0 +1,40 @@ +package io.github.sds100.keymapper.base.trigger + +import kotlinx.serialization.Serializable + +@Serializable +sealed class KeyEventTriggerDevice() : Comparable { + override fun compareTo(other: KeyEventTriggerDevice) = + this.javaClass.name.compareTo(other.javaClass.name) + + @Serializable + data object Internal : KeyEventTriggerDevice() + + @Serializable + data object Any : KeyEventTriggerDevice() + + @Serializable + data class External(val descriptor: String, val name: String) : KeyEventTriggerDevice() { + override fun compareTo(other: KeyEventTriggerDevice): Int { + if (other !is External) { + return super.compareTo(other) + } + + return compareValuesBy( + this, + other, + { it.name }, + { it.descriptor }, + ) + } + } + + fun isSameDevice(other: KeyEventTriggerDevice): Boolean { + if (other is External && this is External) { + return other.descriptor == this.descriptor + } else { + return true + } + } + +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt similarity index 59% rename from base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt rename to base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt index afa7f7c3ea..6518845303 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -1,22 +1,26 @@ package io.github.sds100.keymapper.base.trigger -import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.withFlag -import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity import io.github.sds100.keymapper.data.entities.TriggerKeyEntity import kotlinx.serialization.Serializable import java.util.UUID @Serializable -data class KeyCodeTriggerKey( +data class KeyEventTriggerKey( override val uid: String = UUID.randomUUID().toString(), val keyCode: Int, - val device: TriggerKeyDevice, + val device: KeyEventTriggerDevice, override val clickType: ClickType, override val consumeEvent: Boolean = true, - val detectionSource: InputEventDetectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, + /** + * Whether this key can only be detected by an input method. Some keys, such as DPAD buttons, + * do not send key events to the accessibility service. + */ + val requiresIme: Boolean = false, + val scanCode: Int? = null, ) : TriggerKey() { override val allowedLongPress: Boolean = true @@ -24,16 +28,16 @@ data class KeyCodeTriggerKey( override fun toString(): String { val deviceString = when (device) { - TriggerKeyDevice.Any -> "any" - is TriggerKeyDevice.External -> "external" - TriggerKeyDevice.Internal -> "internal" + KeyEventTriggerDevice.Any -> "any" + is KeyEventTriggerDevice.External -> "external" + KeyEventTriggerDevice.Internal -> "internal" } return "KeyCodeTriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeEvent) " } // key code -> click type -> device -> consume key event override fun compareTo(other: TriggerKey) = when (other) { - is KeyCodeTriggerKey -> compareValuesBy( + is KeyEventTriggerKey -> compareValuesBy( this, other, { it.keyCode }, @@ -46,11 +50,11 @@ data class KeyCodeTriggerKey( } companion object { - fun fromEntity(entity: KeyCodeTriggerKeyEntity): TriggerKey { + fun fromEntity(entity: KeyEventTriggerKeyEntity): TriggerKey { val device = when (entity.deviceId) { - KeyCodeTriggerKeyEntity.DEVICE_ID_THIS_DEVICE -> TriggerKeyDevice.Internal - KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE -> TriggerKeyDevice.Any - else -> TriggerKeyDevice.External( + KeyEventTriggerKeyEntity.DEVICE_ID_THIS_DEVICE -> KeyEventTriggerDevice.Internal + KeyEventTriggerKeyEntity.DEVICE_ID_ANY_DEVICE -> KeyEventTriggerDevice.Any + else -> KeyEventTriggerDevice.External( entity.deviceId, entity.deviceName ?: "", ) @@ -64,34 +68,31 @@ data class KeyCodeTriggerKey( } val consumeEvent = - !entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + !entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) - val detectionSource = - if (entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD)) { - InputEventDetectionSource.INPUT_METHOD - } else { - InputEventDetectionSource.ACCESSIBILITY_SERVICE - } + val requiresIme = + entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) - return KeyCodeTriggerKey( + return KeyEventTriggerKey( uid = entity.uid, keyCode = entity.keyCode, device = device, clickType = clickType, consumeEvent = consumeEvent, - detectionSource = detectionSource, + requiresIme = requiresIme, + scanCode = entity.scanCode ) } - fun toEntity(key: KeyCodeTriggerKey): KeyCodeTriggerKeyEntity { + fun toEntity(key: KeyEventTriggerKey): KeyEventTriggerKeyEntity { val deviceId = when (key.device) { - TriggerKeyDevice.Any -> KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE - is TriggerKeyDevice.External -> key.device.descriptor - TriggerKeyDevice.Internal -> KeyCodeTriggerKeyEntity.DEVICE_ID_THIS_DEVICE + KeyEventTriggerDevice.Any -> KeyEventTriggerKeyEntity.DEVICE_ID_ANY_DEVICE + is KeyEventTriggerDevice.External -> key.device.descriptor + KeyEventTriggerDevice.Internal -> KeyEventTriggerKeyEntity.DEVICE_ID_THIS_DEVICE } val deviceName = - if (key.device is TriggerKeyDevice.External) { + if (key.device is KeyEventTriggerDevice.External) { key.device.name } else { null @@ -106,20 +107,21 @@ data class KeyCodeTriggerKey( var flags = 0 if (!key.consumeEvent) { - flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + flags = flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) } - if (key.detectionSource == InputEventDetectionSource.INPUT_METHOD) { - flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) + if (key.requiresIme) { + flags = flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) } - return KeyCodeTriggerKeyEntity( + return KeyEventTriggerKeyEntity( keyCode = key.keyCode, deviceId = deviceId, deviceName = deviceName, clickType = clickType, flags = flags, uid = key.uid, + scanCode = key.scanCode ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 6334715d91..49b12d1d7b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -5,7 +5,6 @@ import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker -import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.dataOrNull @@ -82,8 +81,7 @@ class RecordTriggerControllerImpl @Inject constructor( Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") val recordedKey = createRecordedKey( - keyEvent.keyCode, - keyEvent.device, + keyEvent, detectionSource ) onRecordKey(recordedKey) @@ -95,7 +93,7 @@ class RecordTriggerControllerImpl @Inject constructor( is KMKeyEvent -> { if (event.action == KeyEvent.ACTION_DOWN) { val recordedKey = - createRecordedKey(event.keyCode, event.device, detectionSource) + createRecordedKey(event, detectionSource) onRecordKey(recordedKey) } return true @@ -149,8 +147,7 @@ class RecordTriggerControllerImpl @Inject constructor( if (keyEvent.action == KeyEvent.ACTION_UP) { val recordedKey = createRecordedKey( - keyEvent.keyCode, - keyEvent.device, + keyEvent, InputEventDetectionSource.INPUT_METHOD, ) @@ -169,17 +166,17 @@ class RecordTriggerControllerImpl @Inject constructor( } private fun createRecordedKey( - keyCode: Int, - device: InputDeviceInfo?, - detectionSource: InputEventDetectionSource, + keyEvent: KMKeyEvent, + detectionSource: InputEventDetectionSource ): RecordedKey { - val triggerKeyDevice = if (device != null && device.isExternal) { - TriggerKeyDevice.External(device.descriptor, device.name) - } else { - TriggerKeyDevice.Internal - } - - return RecordedKey(keyCode, triggerKeyDevice, detectionSource) + return RecordedKey( + keyCode = keyEvent.keyCode, + scanCode = keyEvent.scanCode, + deviceDescriptor = keyEvent.device.descriptor, + deviceName = keyEvent.device.name, + isExternalDevice = keyEvent.device.isExternal, + detectionSource = detectionSource + ) } // Run on a different thread in case the main thread is locked up while recording and diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt index e6d59fd552..9cf7e89993 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt @@ -4,6 +4,9 @@ import io.github.sds100.keymapper.base.input.InputEventDetectionSource data class RecordedKey( val keyCode: Int, - val device: TriggerKeyDevice, + val scanCode: Int, + val deviceDescriptor: String, + val deviceName: String, + val isExternalDevice: Boolean, val detectionSource: InputEventDetectionSource, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt index 2bba80a7ae..7e6b661027 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt @@ -7,10 +7,11 @@ import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.entities.AssistantTriggerKeyEntity import io.github.sds100.keymapper.data.entities.EntityExtra +import io.github.sds100.keymapper.data.entities.EvdevTriggerKeyEntity import io.github.sds100.keymapper.data.entities.FingerprintTriggerKeyEntity import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout import io.github.sds100.keymapper.data.entities.FloatingButtonKeyEntity -import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity import io.github.sds100.keymapper.data.entities.TriggerEntity import io.github.sds100.keymapper.data.entities.getData import io.github.sds100.keymapper.system.inputevents.InputEventUtils @@ -49,7 +50,7 @@ data class Trigger( fun isDetectingWhenScreenOffAllowed(): Boolean { return keys.isNotEmpty() && keys.all { - it is KeyCodeTriggerKey && + it is KeyEventTriggerKey && InputEventUtils.canDetectKeyWhenScreenOff( it.keyCode, ) @@ -89,7 +90,7 @@ object TriggerEntityMapper { val keys = entity.keys.map { key -> when (key) { is AssistantTriggerKeyEntity -> AssistantTriggerKey.fromEntity(key) - is KeyCodeTriggerKeyEntity -> KeyCodeTriggerKey.fromEntity( + is KeyEventTriggerKeyEntity -> KeyEventTriggerKey.fromEntity( key, ) is FloatingButtonKeyEntity -> { @@ -98,6 +99,7 @@ object TriggerEntityMapper { } is FingerprintTriggerKeyEntity -> FingerprintTriggerKey.fromEntity(key) + is EvdevTriggerKeyEntity -> EvdevTriggerKey.fromEntity(key) } } @@ -204,11 +206,12 @@ object TriggerEntityMapper { val keys = trigger.keys.map { key -> when (key) { is AssistantTriggerKey -> AssistantTriggerKey.toEntity(key) - is KeyCodeTriggerKey -> KeyCodeTriggerKey.toEntity( + is KeyEventTriggerKey -> KeyEventTriggerKey.toEntity( key, ) is FloatingButtonKey -> FloatingButtonKey.toEntity(key) is FingerprintTriggerKey -> FingerprintTriggerKey.toEntity(key) + is EvdevTriggerKey -> EvdevTriggerKey.toEntity(key) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt index ef979f425d..3ec72589d2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.trigger import android.os.Build import android.view.KeyEvent -import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.keymaps.requiresImeKeyEventForwardingInPhoneCall import io.github.sds100.keymapper.base.purchasing.ProductId @@ -55,7 +54,7 @@ data class TriggerErrorSnapshot( } val requiresDndAccess = - key is KeyCodeTriggerKey && key.keyCode in keysThatRequireDndAccess + key is KeyEventTriggerKey && key.keyCode in keysThatRequireDndAccess if (requiresDndAccess) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isDndAccessGranted) { @@ -71,11 +70,8 @@ data class TriggerErrorSnapshot( } val containsDpadKey = - key is KeyCodeTriggerKey && - InputEventUtils.isDpadKeyCode( - key.keyCode, - ) && - key.detectionSource == InputEventDetectionSource.INPUT_METHOD + key is KeyEventTriggerKey && + InputEventUtils.isDpadKeyCode(key.keyCode) && key.requiresIme if (showDpadImeSetupError && !isKeyMapperImeChosen && containsDpadKey) { return TriggerError.DPAD_IME_NOT_SELECTED diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt index 1239059181..7ba6463ac2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt @@ -19,9 +19,10 @@ sealed class TriggerKey : Comparable { fun setClickType(clickType: ClickType): TriggerKey = when (this) { is AssistantTriggerKey -> copy(clickType = clickType) - is KeyCodeTriggerKey -> copy(clickType = clickType) + is KeyEventTriggerKey -> copy(clickType = clickType) is FloatingButtonKey -> copy(clickType = clickType) is FingerprintTriggerKey -> copy(clickType = clickType) + is EvdevTriggerKey -> copy(clickType = clickType) } override fun compareTo(other: TriggerKey) = this.javaClass.name.compareTo(other.javaClass.name) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt deleted file mode 100644 index db9c5a999a..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -import io.github.sds100.keymapper.common.utils.InputDeviceInfo -import kotlinx.serialization.Serializable - -@Serializable -sealed class TriggerKeyDevice : Comparable { - override fun compareTo(other: TriggerKeyDevice) = this.javaClass.name.compareTo(other.javaClass.name) - - // TODO add descriptor and device name - @Serializable - data object Internal : TriggerKeyDevice() - - @Serializable - data object Any : TriggerKeyDevice() - - @Serializable - data class External(val descriptor: String, val name: String) : TriggerKeyDevice() { - override fun compareTo(other: TriggerKeyDevice): Int { - if (other !is External) { - return super.compareTo(other) - } - - return compareValuesBy( - this, - other, - { it.name }, - { it.descriptor }, - ) - } - } - - fun isSameDevice(other: TriggerKeyDevice): Boolean { - if (other is External && this is External) { - return other.descriptor == this.descriptor - } else { - return true - } - } - - fun isSameDevice(device: InputDeviceInfo): Boolean { - if (this is External && device.isExternal) { - return device.descriptor == this.descriptor - } else { - return true - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index 4e38181428..f3e26528e6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -137,7 +137,7 @@ fun TriggerKeyListItem( model.buttonName, ) - is TriggerKeyListItemModel.KeyCode -> model.keyName + is TriggerKeyListItemModel.KeyEvent -> model.keyName is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource(R.string.trigger_error_floating_button_deleted_title) @@ -159,7 +159,7 @@ fun TriggerKeyListItem( } val tertiaryText = when (model) { - is TriggerKeyListItemModel.KeyCode -> model.extraInfo + is TriggerKeyListItemModel.KeyEvent -> model.extraInfo is TriggerKeyListItemModel.FloatingButton -> model.layoutName else -> null @@ -324,7 +324,7 @@ private fun ErrorTextColumn( @Composable private fun KeyCodePreview() { TriggerKeyListItem( - model = TriggerKeyListItemModel.KeyCode( + model = TriggerKeyListItemModel.KeyEvent( id = "id", keyName = "Volume Up", clickType = ClickType.SHORT_PRESS, @@ -342,7 +342,7 @@ private fun KeyCodePreview() { @Composable private fun NoDragPreview() { TriggerKeyListItem( - model = TriggerKeyListItemModel.KeyCode( + model = TriggerKeyListItemModel.KeyEvent( id = "id", keyName = "Volume Up", clickType = ClickType.LONG_PRESS, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index e104ecf8c9..4a53c2d2e3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -96,7 +96,7 @@ fun TriggerKeyOptionsBottomSheet( Spacer(modifier = Modifier.height(8.dp)) - if (state is TriggerKeyOptionsState.KeyCode) { + if (state is TriggerKeyOptionsState.KeyEvent) { CheckBoxText( modifier = Modifier.padding(8.dp), text = stringResource(R.string.flag_dont_override_default_action), @@ -146,7 +146,7 @@ fun TriggerKeyOptionsBottomSheet( } } - if (state is TriggerKeyOptionsState.KeyCode) { + if (state is TriggerKeyOptionsState.KeyEvent) { Text( modifier = Modifier.padding(horizontal = 16.dp), text = stringResource(R.string.trigger_key_device_header), @@ -296,7 +296,7 @@ private fun Preview() { TriggerKeyOptionsBottomSheet( sheetState = sheetState, - state = TriggerKeyOptionsState.KeyCode( + state = TriggerKeyOptionsState.KeyEvent( doNotRemapChecked = true, clickType = ClickType.DOUBLE_PRESS, showClickTypes = true, diff --git a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt index 2d035f401c..da3447c85e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt @@ -4,16 +4,20 @@ import android.view.KeyEvent import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.constraints.Constraint -import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCaseController import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey +import io.github.sds100.keymapper.base.trigger.FloatingButtonKey +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode +import io.github.sds100.keymapper.base.utils.parallelTrigger +import io.github.sds100.keymapper.base.utils.sequenceTrigger import io.github.sds100.keymapper.base.utils.singleKeyTrigger import io.github.sds100.keymapper.base.utils.triggerKey import io.github.sds100.keymapper.common.utils.State @@ -28,6 +32,7 @@ import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.instanceOf import org.hamcrest.Matchers.`is` +import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock @@ -54,42 +59,303 @@ class ConfigKeyMapUseCaseTest { } @Test - fun `Do not allow setting double press for parallel trigger with side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Any device can not be selected for evdev trigger key`() = + runTest(testDispatcher) { + val triggerKey = EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + deviceDescriptor = "keyboard0", + deviceName = "Keyboard", + clickType = ClickType.SHORT_PRESS, + consumeEvent = true + ) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.keyMap.value = State.Data( + KeyMap( + trigger = sequenceTrigger( + triggerKey, + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + deviceDescriptor = "keyboard0", + deviceName = "Keyboard", + clickType = ClickType.SHORT_PRESS, + consumeEvent = true + ) + ) + ) + ) - useCase.setTriggerDoublePress() + assertThrows(IllegalArgumentException::class.java) { + useCase.setTriggerKeyDevice(triggerKey.uid, KeyEventTriggerDevice.Any) + } + } - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } + @Test + fun `Adding a non evdev key deletes all evdev keys in the trigger`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data( + KeyMap( + trigger = parallelTrigger( + FloatingButtonKey( + buttonUid = "floating_button", + button = null, + clickType = ClickType.SHORT_PRESS + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + deviceDescriptor = "keyboard0", + deviceName = "Keyboard 0", + ), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 100, + deviceDescriptor = "gpio", + deviceName = "GPIO", + ), + ) + ) + ) + + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.keys, hasSize(3)) + assertThat(trigger.keys[0], instanceOf(FloatingButtonKey::class.java)) + assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + assertThat(trigger.keys[2], instanceOf(KeyEventTriggerKey::class.java)) + assertThat( + (trigger.keys[2] as KeyEventTriggerKey).requiresIme, `is`(false) + ) + } @Test - fun `Do not allow setting long press for parallel trigger with side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Adding an evdev key deletes all non evdev keys in the trigger`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data( + KeyMap( + trigger = parallelTrigger( + FloatingButtonKey( + buttonUid = "floating_button", + button = null, + clickType = ClickType.SHORT_PRESS + ), + triggerKey( + KeyEvent.KEYCODE_VOLUME_UP, + KeyEventTriggerDevice.Internal + ), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS + ), + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEventTriggerDevice.Internal + ) + ) + ) + ) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addEvdevTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + "keyboard0", + "Keyboard" + ) - useCase.setTriggerLongPress() + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.keys, hasSize(3)) + assertThat(trigger.keys[0], instanceOf(FloatingButtonKey::class.java)) + assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + assertThat(trigger.keys[2], instanceOf(EvdevTriggerKey::class.java)) + assertThat( + (trigger.keys[2] as EvdevTriggerKey).deviceDescriptor, `is`("keyboard0") + ) + } - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } + @Test + fun `Converting a sequence trigger to parallel trigger removes duplicate evdev keys`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data( + KeyMap( + trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + deviceDescriptor = "keyboard0", + deviceName = "Keyboard", + clickType = ClickType.SHORT_PRESS, + consumeEvent = true + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + deviceDescriptor = "keyboard0", + deviceName = "Keyboard", + clickType = ClickType.SHORT_PRESS, + consumeEvent = true + ) + ) + ) + ) + + useCase.setParallelTriggerMode() + + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + deviceDescriptor = "keyboard0", + deviceName = "Keyboard", + clickType = ClickType.SHORT_PRESS, + consumeEvent = true + ) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.keys, hasSize(1)) + assertThat(trigger.keys[0], instanceOf(EvdevTriggerKey::class.java)) + assertThat( + (trigger.keys[0] as EvdevTriggerKey).keyCode, + `is`(KeyEvent.KEYCODE_VOLUME_DOWN) + ) + assertThat((trigger.keys[0] as EvdevTriggerKey).deviceDescriptor, `is`("keyboard0")) + } + + @Test + fun `Adding the same evdev trigger key from same device makes the trigger a sequence`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addEvdevTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + "keyboard0", + "Keyboard" + ) + + useCase.addEvdevTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + "keyboard0", + "Keyboard" + ) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding an evdev trigger key to a sequence trigger keeps it sequence`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data( + KeyMap( + trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + deviceDescriptor = "keyboard0", + deviceName = "Keyboard", + clickType = ClickType.SHORT_PRESS, + consumeEvent = true + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + deviceDescriptor = "keyboard0", + deviceName = "Keyboard", + clickType = ClickType.SHORT_PRESS, + consumeEvent = true + ) + ) + ) + ) + + // Add a third key and it should still be a sequence trigger now + useCase.addEvdevTriggerKey( + KeyEvent.KEYCODE_VOLUME_UP, + 0, + "keyboard0", + "Keyboard" + ) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding the same evdev trigger key code from different devices keeps the trigger parallel`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addEvdevTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + "keyboard0", + "Keyboard 0" + ) + + useCase.addEvdevTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + "keyboard1", + "Keyboard 1" + ) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(ClickType.SHORT_PRESS))) + + } + + @Test + fun `Do not allow setting double press for parallel trigger with side key`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + + useCase.setTriggerDoublePress() + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting long press for parallel trigger with side key`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + + useCase.setTriggerLongPress() + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } @Test fun `Do not allow setting double press for side key`() = runTest(testDispatcher) { @@ -118,223 +384,245 @@ class ConfigKeyMapUseCaseTest { } @Test - fun `Set click type to short press if side key added to double press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Set click type to short press if side key added to double press volume button`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) - useCase.setTriggerDoublePress() + useCase.setTriggerDoublePress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } @Test - fun `Set click type to short press if fingerprint gestures added to double press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Set click type to short press if fingerprint gestures added to double press volume button`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) - useCase.setTriggerDoublePress() + useCase.setTriggerDoublePress() - useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) + useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } @Test - fun `Set click type to short press if side key added to long press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Set click type to short press if side key added to long press volume button`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) - useCase.setTriggerLongPress() + useCase.setTriggerLongPress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } @Test - fun `Set click type to short press if fingerprint gestures added to long press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Set click type to short press if fingerprint gestures added to long press volume button`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) - useCase.setTriggerLongPress() + useCase.setTriggerLongPress() - useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) + useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } @Test - fun `Enable hold down option for key event actions when the trigger is a DPAD button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_DPAD_LEFT, - TriggerKeyDevice.Any, - InputEventDetectionSource.INPUT_METHOD, - ) + fun `Enable hold down option for key event actions when the trigger is a DPAD button`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + 0, + KeyEventTriggerDevice.Internal, + true + ) - useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) + useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) - val actionList = useCase.keyMap.value.dataOrNull()!!.actionList - assertThat(actionList[0].holdDown, `is`(true)) - assertThat(actionList[0].repeat, `is`(false)) - } + val actionList = useCase.keyMap.value.dataOrNull()!!.actionList + assertThat(actionList[0].holdDown, `is`(true)) + assertThat(actionList[0].repeat, `is`(false)) + } /** * This ensures that it isn't possible to have two or more assistant triggers when the mode is parallel. */ @Test - fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) - useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) - useCase.setParallelTriggerMode() + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) + useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) + useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) + useCase.setParallelTriggerMode() - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(2)) - assertThat( - trigger.keys[0], - instanceOf(io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey::class.java), - ) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.keys, hasSize(2)) + assertThat( + trigger.keys[0], + instanceOf(KeyEventTriggerKey::class.java), + ) + assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + } @Test - fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) - useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) - useCase.setParallelTriggerMode() + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) + useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) + useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) + useCase.setParallelTriggerMode() - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(2)) - assertThat( - trigger.keys[0], - instanceOf(io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey::class.java), - ) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.keys, hasSize(2)) + assertThat( + trigger.keys[0], + instanceOf(KeyEventTriggerKey::class.java), + ) + assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + } @Test - fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.setTriggerLongPress() + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_UP, + 0, + KeyEventTriggerDevice.Internal, + false + ) + useCase.setTriggerLongPress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } @Test - fun `Set click type to short press when adding assistant key to double press trigger key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Set click type to short press when adding assistant key to double press trigger key`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.setTriggerDoublePress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) + useCase.setTriggerDoublePress() + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } @Test - fun `Set click type to short press when adding assistant key to long press trigger key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + fun `Set click type to short press when adding assistant key to long press trigger key`() = + runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.setTriggerLongPress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false + ) + useCase.setTriggerLongPress() + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } @Test - fun `Do not allow long press for parallel trigger with assistant key`() = runTest(testDispatcher) { - val keyMap = KeyMap( - trigger = Trigger( - mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS), - keys = listOf( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), - AssistantTriggerKey( - type = AssistantTriggerType.ANY, - clickType = ClickType.SHORT_PRESS, + fun `Do not allow long press for parallel trigger with assistant key`() = + runTest(testDispatcher) { + val keyMap = KeyMap( + trigger = Trigger( + mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS), + keys = listOf( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), ), ), - ), - ) + ) - useCase.keyMap.value = State.Data(keyMap) - useCase.setTriggerLongPress() + useCase.keyMap.value = State.Data(keyMap) + useCase.setTriggerLongPress() - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } /** * Issue #753. If a modifier key is used as a trigger then it the @@ -362,10 +650,11 @@ class ConfigKeyMapUseCaseTest { useCase.keyMap.value = State.Data(KeyMap()) // WHEN - useCase.addKeyCodeTriggerKey( + useCase.addKeyEventTriggerKey( modifierKeyCode, - TriggerKeyDevice.Internal, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, + 0, + KeyEventTriggerDevice.Internal, + false ) // THEN @@ -379,102 +668,108 @@ class ConfigKeyMapUseCaseTest { * Issue #753. */ @Test - fun `when add non-modifier key trigger, do ont enable do not remap option`() = runTest(testDispatcher) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) + fun `when add non-modifier key trigger, do ont enable do not remap option`() = + runTest(testDispatcher) { + // GIVEN + useCase.keyMap.value = State.Data(KeyMap()) - // WHEN - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, - detectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, - ) + // WHEN + useCase.addKeyEventTriggerKey( + KeyEvent.KEYCODE_A, + 0, + KeyEventTriggerDevice.Internal, + false + ) - // THEN - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + // THEN + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys[0].consumeEvent, `is`(true)) - } + assertThat(trigger.keys[0].consumeEvent, `is`(true)) + } /** * Issue #852. Add a phone ringing constraint when you add an action * to answer a phone call. */ @Test - fun `when add answer phone call action, then add phone ringing constraint`() = runTest(testDispatcher) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) - val action = ActionData.AnswerCall + fun `when add answer phone call action, then add phone ringing constraint`() = + runTest(testDispatcher) { + // GIVEN + useCase.keyMap.value = State.Data(KeyMap()) + val action = ActionData.AnswerCall - // WHEN - useCase.addAction(action) + // WHEN + useCase.addAction(action) - // THEN - val keyMap = useCase.keyMap.value.dataOrNull()!! - assertThat( - keyMap.constraintState.constraints, - contains(instanceOf(Constraint.PhoneRinging::class.java)), - ) - } + // THEN + val keyMap = useCase.keyMap.value.dataOrNull()!! + assertThat( + keyMap.constraintState.constraints, + contains(instanceOf(Constraint.PhoneRinging::class.java)), + ) + } /** * Issue #852. Add a in phone call constraint when you add an action * to end a phone call. */ @Test - fun `when add end phone call action, then add in phone call constraint`() = runTest(testDispatcher) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) - val action = ActionData.EndCall + fun `when add end phone call action, then add in phone call constraint`() = + runTest(testDispatcher) { + // GIVEN + useCase.keyMap.value = State.Data(KeyMap()) + val action = ActionData.EndCall - // WHEN - useCase.addAction(action) + // WHEN + useCase.addAction(action) - // THEN - val keyMap = useCase.keyMap.value.dataOrNull()!! - assertThat( - keyMap.constraintState.constraints, - contains(instanceOf(Constraint.InPhoneCall::class.java)), - ) - } + // THEN + val keyMap = useCase.keyMap.value.dataOrNull()!! + assertThat( + keyMap.constraintState.constraints, + contains(instanceOf(Constraint.InPhoneCall::class.java)), + ) + } /** * issue #593 */ @Test - fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = runTest(testDispatcher) { - // given - val action = Action( - data = ActionData.TapScreen(100, 100, null), - holdDown = true, - ) + fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = + runTest(testDispatcher) { + // given + val action = Action( + data = ActionData.TapScreen(100, 100, null), + holdDown = true, + ) - val keyMap = KeyMap( - 0, - trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_0)), - actionList = listOf(action), - ) + val keyMap = KeyMap( + 0, + trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_0)), + actionList = listOf(action), + ) - // when - useCase.keyMap.value = State.Data(keyMap) + // when + useCase.keyMap.value = State.Data(keyMap) - // then - assertThat(useCase.keyMap.value.dataOrNull()!!.actionList, `is`(listOf(action))) - } + // then + assertThat(useCase.keyMap.value.dataOrNull()!!.actionList, `is`(listOf(action))) + } @Test - fun `add modifier key event action, enable hold down option and disable repeat option`() = runTest(testDispatcher) { - InputEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addAction(ActionData.InputKeyEvent(keyCode)) - - useCase.keyMap.value.dataOrNull()!!.actionList - .single() - .let { - assertThat(it.holdDown, `is`(true)) - assertThat(it.repeat, `is`(false)) - } + fun `add modifier key event action, enable hold down option and disable repeat option`() = + runTest(testDispatcher) { + InputEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addAction(ActionData.InputKeyEvent(keyCode)) + + useCase.keyMap.value.dataOrNull()!!.actionList + .single() + .let { + assertThat(it.holdDown, `is`(true)) + assertThat(it.repeat, `is`(false)) + } + } } - } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index e84ddf7e10..7270772a6b 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -119,6 +119,7 @@ class PerformActionsUseCaseTest { id = 1, isExternal = true, isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD ) fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad)) @@ -187,6 +188,7 @@ class PerformActionsUseCaseTest { id = 1, isExternal = true, isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD ) val fakeKeyboard = InputDeviceInfo( @@ -195,6 +197,7 @@ class PerformActionsUseCaseTest { id = 2, isExternal = true, isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD ) fakeDevicesAdapter.connectedInputDevices.value = @@ -251,6 +254,7 @@ class PerformActionsUseCaseTest { id = 10, isExternal = true, isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD ), InputDeviceInfo( @@ -259,6 +263,7 @@ class PerformActionsUseCaseTest { id = 11, isExternal = true, isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD ), ), ) @@ -303,6 +308,7 @@ class PerformActionsUseCaseTest { id = 10, isExternal = true, isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD ), ), ) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt index 4673782f5e..2a60387a7a 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.actions.keyevents +import android.view.InputDevice import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.github.sds100.keymapper.base.actions.keyevent.ConfigKeyEventActionViewModel import io.github.sds100.keymapper.base.actions.keyevent.ConfigKeyEventUseCase @@ -71,6 +72,7 @@ class ConfigKeyServiceEventActionViewModelTest { id = 0, isExternal = false, isGameController = false, + sources = InputDevice.SOURCE_KEYBOARD ) val fakeDevice2 = InputDeviceInfo( @@ -79,6 +81,7 @@ class ConfigKeyServiceEventActionViewModelTest { id = 1, isExternal = false, isGameController = false, + sources = InputDevice.SOURCE_KEYBOARD ) // WHEN diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt index a346bc987b..0447de4325 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt @@ -27,6 +27,7 @@ class DpadMotionEventTrackerTest { name = "Controller 1", isExternal = true, isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD ) private val CONTROLLER_2_DEVICE = InputDeviceInfo( @@ -35,6 +36,7 @@ class DpadMotionEventTrackerTest { name = "Controller 2", isExternal = true, isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD ) } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt index bb88b1adda..c77c64d251 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.keymaps +import android.view.InputDevice import android.view.KeyEvent import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.github.sds100.keymapper.base.actions.Action @@ -12,16 +13,15 @@ import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase -import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode import io.github.sds100.keymapper.base.utils.TestConstraintSnapshot import io.github.sds100.keymapper.base.utils.parallelTrigger @@ -74,19 +74,27 @@ class KeyMapControllerTest { companion object { private const val FAKE_KEYBOARD_DEVICE_ID = 123 private const val FAKE_KEYBOARD_DESCRIPTOR = "fake_keyboard" - private val FAKE_KEYBOARD_TRIGGER_KEY_DEVICE = TriggerKeyDevice.External( + private val FAKE_KEYBOARD_TRIGGER_KEY_DEVICE = KeyEventTriggerDevice.External( descriptor = FAKE_KEYBOARD_DESCRIPTOR, name = "Fake Keyboard", ) + private val FAKE_KEYBOARD_DEVICE = InputDeviceInfo( + descriptor = FAKE_KEYBOARD_DESCRIPTOR, + name = "Fake Keyboard", + id = FAKE_KEYBOARD_DEVICE_ID, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_KEYBOARD + ) private const val FAKE_HEADPHONE_DESCRIPTOR = "fake_headphone" - private val FAKE_HEADPHONE_TRIGGER_KEY_DEVICE = TriggerKeyDevice.External( + private val FAKE_HEADPHONE_TRIGGER_KEY_DEVICE = KeyEventTriggerDevice.External( descriptor = FAKE_HEADPHONE_DESCRIPTOR, name = "Fake Headphones", ) private const val FAKE_CONTROLLER_DESCRIPTOR = "fake_controller" - private val FAKE_CONTROLLER_TRIGGER_KEY_DEVICE = TriggerKeyDevice.External( + private val FAKE_CONTROLLER_TRIGGER_KEY_DEVICE = KeyEventTriggerDevice.External( descriptor = FAKE_CONTROLLER_DESCRIPTOR, name = "Fake Controller", ) @@ -96,6 +104,7 @@ class KeyMapControllerTest { id = 0, isExternal = true, isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD ) private const val FAKE_PACKAGE_NAME = "test_package" @@ -213,6 +222,8 @@ class KeyMapControllerTest { detectKeyMapsUseCase, performActionsUseCase, detectConstraintsUseCase, + inputEventHub = mock(), + isPausedFlow = MutableStateFlow(false) ) } @@ -846,7 +857,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = InputEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -877,7 +888,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = InputEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -912,7 +923,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = InputEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -941,7 +952,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.LONG_PRESS, - detectionSource = InputEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -968,7 +979,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = InputEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -998,7 +1009,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = InputEventDetectionSource.INPUT_METHOD, + requiresIme = true, ), ) @@ -1047,7 +1058,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.LONG_PRESS, - detectionSource = InputEventDetectionSource.INPUT_METHOD, + requiresIme = true, ), ) @@ -2602,7 +2613,7 @@ class KeyMapControllerTest { val triggerAnyDevice = singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_A, - device = TriggerKeyDevice.Any, + device = KeyEventTriggerDevice.Any, ), ) @@ -2750,7 +2761,7 @@ class KeyMapControllerTest { val firstTrigger = sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, + device = KeyEventTriggerDevice.Any, ), triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) @@ -2759,7 +2770,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_HOME), triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, + device = KeyEventTriggerDevice.Any, ), triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) @@ -2773,7 +2784,7 @@ class KeyMapControllerTest { mockTriggerKeyInput( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, + device = KeyEventTriggerDevice.Any, ), ) mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_UP)) @@ -2797,13 +2808,14 @@ class KeyMapControllerTest { KeyMap(0, trigger = homeTrigger, actionList = listOf(TEST_ACTION)), ) - val consumedHomeDown = inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_DOWN, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, null) + val consumedHomeDown = + inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_DOWN, FAKE_KEYBOARD_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_KEYBOARD_DEVICE) advanceUntilIdle() - inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_UP, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, null) + inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_UP, FAKE_KEYBOARD_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, FAKE_KEYBOARD_DEVICE) assertThat(consumedHomeDown, `is`(true)) @@ -2821,13 +2833,17 @@ class KeyMapControllerTest { ) val consumedRecentsDown = - inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_DOWN, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, null) + inputKeyEvent( + KeyEvent.KEYCODE_APP_SWITCH, + KeyEvent.ACTION_DOWN, + FAKE_KEYBOARD_DEVICE + ) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_KEYBOARD_DEVICE) advanceUntilIdle() - inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_UP, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, null) + inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_UP, FAKE_KEYBOARD_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, FAKE_KEYBOARD_DEVICE) assertThat(consumedRecentsDown, `is`(true)) } @@ -2966,7 +2982,7 @@ class KeyMapControllerTest { listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when - (trigger.keys[1] as KeyCodeTriggerKey).let { + (trigger.keys[1] as KeyEventTriggerKey).let { inputKeyEvent( it.keyCode, KeyEvent.ACTION_DOWN, @@ -2974,7 +2990,7 @@ class KeyMapControllerTest { ) } - (trigger.keys[1] as KeyCodeTriggerKey).let { + (trigger.keys[1] as KeyEventTriggerKey).let { val consumed = inputKeyEvent( it.keyCode, KeyEvent.ACTION_UP, @@ -3016,7 +3032,7 @@ class KeyMapControllerTest { listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when - for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { inputKeyEvent( key.keyCode, KeyEvent.ACTION_DOWN, @@ -3026,7 +3042,7 @@ class KeyMapControllerTest { var consumedUpCount = 0 - for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( key.keyCode, @@ -3055,7 +3071,7 @@ class KeyMapControllerTest { listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when - for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { inputKeyEvent( key.keyCode, KeyEvent.ACTION_DOWN, @@ -3067,7 +3083,7 @@ class KeyMapControllerTest { var consumedUpCount = 0 - for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( key.keyCode, @@ -3262,7 +3278,7 @@ class KeyMapControllerTest { singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, ), ), ), @@ -3271,7 +3287,7 @@ class KeyMapControllerTest { sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, ), ), ), @@ -3295,7 +3311,7 @@ class KeyMapControllerTest { // WHEN var consumedCount = 0 - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( 999, @@ -3321,7 +3337,7 @@ class KeyMapControllerTest { var consumedCount = 0 - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( key.keyCode, @@ -3346,7 +3362,7 @@ class KeyMapControllerTest { var consumedCount = 0 - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( key.keyCode, @@ -3367,14 +3383,14 @@ class KeyMapControllerTest { "undefined single short-press this-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, consume = false, ), ), "undefined single long-press this-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3382,7 +3398,7 @@ class KeyMapControllerTest { "undefined single double-press this-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3391,14 +3407,14 @@ class KeyMapControllerTest { "undefined single short-press any-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, consume = false, ), ), "undefined single long-press any-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3406,7 +3422,7 @@ class KeyMapControllerTest { "undefined single double-press any-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3415,19 +3431,19 @@ class KeyMapControllerTest { "sequence multiple short-press this-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), @@ -3436,19 +3452,19 @@ class KeyMapControllerTest { "sequence multiple long-press this-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3457,19 +3473,19 @@ class KeyMapControllerTest { "sequence multiple double-press this-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3478,19 +3494,19 @@ class KeyMapControllerTest { "sequence multiple mix this-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3532,13 +3548,13 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), @@ -3553,13 +3569,13 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3574,13 +3590,13 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.DOUBLE_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3595,13 +3611,13 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3610,7 +3626,7 @@ class KeyMapControllerTest { "sequence multiple mix mixed-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3622,7 +3638,7 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3637,19 +3653,19 @@ class KeyMapControllerTest { "parallel multiple short-press this-device, do not consume" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), @@ -3658,19 +3674,19 @@ class KeyMapControllerTest { "parallel multiple long-press this-device, do not consume" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3721,13 +3737,13 @@ class KeyMapControllerTest { "parallel multiple short-press mix-device, do not consume" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, consume = false, ), @@ -3742,13 +3758,13 @@ class KeyMapControllerTest { "parallel multiple long-press mix-device, do not consume" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3778,20 +3794,20 @@ class KeyMapControllerTest { "undefined single short-press this-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, ), ), "undefined single long-press this-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), ), "undefined single double-press this-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), @@ -3799,20 +3815,20 @@ class KeyMapControllerTest { "undefined single short-press any-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, ), ), "undefined single long-press any-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, ), ), "undefined single double-press any-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.DOUBLE_PRESS, ), ), @@ -3820,68 +3836,68 @@ class KeyMapControllerTest { "sequence multiple short-press this-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), ), "sequence multiple long-press this-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), ), "sequence multiple double-press this-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), "sequence multiple mix this-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), @@ -3916,12 +3932,12 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), ), @@ -3933,12 +3949,12 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), ), @@ -3950,12 +3966,12 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.DOUBLE_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), @@ -3967,19 +3983,19 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), "sequence multiple mix mixed-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, ), triggerKey( @@ -3989,7 +4005,7 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( @@ -4002,34 +4018,34 @@ class KeyMapControllerTest { "parallel multiple short-press this-device" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), ), "parallel multiple long-press this-device" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), ), @@ -4070,12 +4086,12 @@ class KeyMapControllerTest { "parallel multiple short-press mix-device" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, ), triggerKey( @@ -4087,12 +4103,12 @@ class KeyMapControllerTest { "parallel multiple long-press mix-device" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, ), triggerKey( @@ -4140,7 +4156,7 @@ class KeyMapControllerTest { } private suspend fun mockTriggerKeyInput(key: TriggerKey, delay: Long? = null) { - if (key !is KeyCodeTriggerKey) { + if (key !is KeyEventTriggerKey) { return } @@ -4206,7 +4222,7 @@ class KeyMapControllerTest { private fun inputKeyEvent( keyCode: Int, action: Int, - device: InputDeviceInfo? = null, + device: InputDeviceInfo = FAKE_KEYBOARD_DEVICE, metaState: Int? = null, scanCode: Int = 0, repeatCount: Int = 0, @@ -4228,16 +4244,16 @@ class KeyMapControllerTest { delay: Long? = null, ) { require(trigger.mode is TriggerMode.Parallel) - require(trigger.keys.all { it is KeyCodeTriggerKey }) + require(trigger.keys.all { it is KeyEventTriggerKey }) for (key in trigger.keys) { - if (key !is KeyCodeTriggerKey) { + if (key !is KeyEventTriggerKey) { continue } - val deviceDescriptor = triggerKeyDeviceToInputDevice(key.device) + val inputDevice = triggerKeyDeviceToInputDevice(key.device) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) } if (delay != null) { @@ -4251,7 +4267,7 @@ class KeyMapControllerTest { } for (key in trigger.keys) { - if (key !is KeyCodeTriggerKey) { + if (key !is KeyEventTriggerKey) { continue } @@ -4262,32 +4278,35 @@ class KeyMapControllerTest { } private fun triggerKeyDeviceToInputDevice( - device: TriggerKeyDevice, + device: KeyEventTriggerDevice, deviceId: Int = 0, isGameController: Boolean = false, ): InputDeviceInfo = when (device) { - TriggerKeyDevice.Any -> InputDeviceInfo( + KeyEventTriggerDevice.Any -> InputDeviceInfo( descriptor = "any_device", name = "any_device_name", isExternal = false, id = deviceId, isGameController = isGameController, + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD ) - is TriggerKeyDevice.External -> InputDeviceInfo( + is KeyEventTriggerDevice.External -> InputDeviceInfo( descriptor = device.descriptor, name = "device_name", isExternal = true, id = deviceId, isGameController = isGameController, + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD ) - TriggerKeyDevice.Internal -> InputDeviceInfo( + KeyEventTriggerDevice.Internal -> InputDeviceInfo( descriptor = "internal_device", name = "internal_device_name", isExternal = false, id = deviceId, isGameController = isGameController, + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD ) } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/repositories/KeyMapRepositoryTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/repositories/KeyMapRepositoryTest.kt index 349772939e..9ec3715c80 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/repositories/KeyMapRepositoryTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/repositories/KeyMapRepositoryTest.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.repositories import io.github.sds100.keymapper.base.TestDispatcherProvider import io.github.sds100.keymapper.base.system.devices.FakeDevicesAdapter -import io.github.sds100.keymapper.common.utils.InputDeviceInfo 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 @@ -26,16 +25,6 @@ import org.mockito.kotlin.times @RunWith(MockitoJUnitRunner::class) class KeyMapRepositoryTest { - companion object { - private val FAKE_KEYBOARD = InputDeviceInfo( - descriptor = "fake_keyboard_descriptor", - name = "fake keyboard", - id = 1, - isExternal = true, - isGameController = false, - ) - } - private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/system/devices/FakeDevicesAdapter.kt b/base/src/test/java/io/github/sds100/keymapper/base/system/devices/FakeDevicesAdapter.kt index be3ca421a7..298a040d79 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/system/devices/FakeDevicesAdapter.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/system/devices/FakeDevicesAdapter.kt @@ -31,4 +31,8 @@ class FakeDevicesAdapter : DevicesAdapter { override fun getInputDeviceName(descriptor: String): KMResult { throw Exception() } + + override fun getInputDevice(deviceId: Int): InputDeviceInfo? { + throw NotImplementedError() + } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt new file mode 100644 index 0000000000..2e4deebe38 --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt @@ -0,0 +1,42 @@ +package io.github.sds100.keymapper.base.trigger + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.Test + +class TriggerKeyDeviceTest { + @Test + fun `external device is same as another external device`() { + val device1 = KeyEventTriggerDevice.External("keyboard0", "Keyboard 0") + val device2 = KeyEventTriggerDevice.External("keyboard0", "Keyboard 0") + assertThat(device1.isSameDevice(device2), `is`(true)) + } + + @Test + fun `external device is not the same as a different external device`() { + val device1 = KeyEventTriggerDevice.External("keyboard0", "Keyboard 0") + val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 1") + assertThat(device1.isSameDevice(device2), `is`(false)) + } + + @Test + fun `external device is not the same as a different external device with the same name`() { + val device1 = KeyEventTriggerDevice.External("keyboard0", "Keyboard 0") + val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 0") + assertThat(device1.isSameDevice(device2), `is`(false)) + } + + @Test + fun `internal device is not the same as a an external`() { + val device1 = KeyEventTriggerDevice.Internal + val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 0") + assertThat(device1.isSameDevice(device2), `is`(false)) + } + + @Test + fun `internal device is the same as an internal device`() { + val device1 = KeyEventTriggerDevice.Internal + val device2 = KeyEventTriggerDevice.Internal + assertThat(device1.isSameDevice(device2), `is`(false)) + } +} \ No newline at end of file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt index a18eb8f9f3..1fc5742b88 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt @@ -1,11 +1,10 @@ package io.github.sds100.keymapper.base.utils -import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType -import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode fun singleKeyTrigger(key: TriggerKey): Trigger = Trigger( @@ -25,14 +24,14 @@ fun sequenceTrigger(vararg keys: TriggerKey): Trigger = Trigger( fun triggerKey( keyCode: Int, - device: TriggerKeyDevice = TriggerKeyDevice.Internal, + device: KeyEventTriggerDevice = KeyEventTriggerDevice.Internal, clickType: ClickType = ClickType.SHORT_PRESS, consume: Boolean = true, - detectionSource: InputEventDetectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, -): KeyCodeTriggerKey = KeyCodeTriggerKey( + requiresIme: Boolean = false, +): KeyEventTriggerKey = KeyEventTriggerKey( keyCode = keyCode, device = device, clickType = clickType, consumeEvent = consume, - detectionSource = detectionSource, -) + requiresIme = requiresIme, +) \ No newline at end of file diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt new file mode 100644 index 0000000000..8ec0faf043 --- /dev/null +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt @@ -0,0 +1,43 @@ +package io.github.sds100.keymapper.data.entities + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.util.UUID + +@Parcelize +data class EvdevTriggerKeyEntity( + @SerializedName(NAME_KEYCODE) + val keyCode: Int, + + @SerializedName(NAME_SCANCODE) + val scanCode: Int, + + @SerializedName(NAME_DEVICE_DESCRIPTOR) + val deviceDescriptor: String, + + @SerializedName(NAME_DEVICE_NAME) + val deviceName: String, + + @SerializedName(NAME_CLICK_TYPE) + override val clickType: Int = SHORT_PRESS, + + @SerializedName(NAME_FLAGS) + val flags: Int = 0, + + @SerializedName(NAME_UID) + override val uid: String = UUID.randomUUID().toString() +) : TriggerKeyEntity(), + Parcelable { + + companion object { + // DON'T CHANGE THESE. Used for JSON serialization and parsing. + const val NAME_KEYCODE = "keyCode" + const val NAME_SCANCODE = "scanCode" + const val NAME_DEVICE_DESCRIPTOR = "deviceDescriptor" + const val NAME_DEVICE_NAME = "deviceName" + const val NAME_FLAGS = "flags" + + const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1 + } +} diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt similarity index 89% rename from data/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt rename to data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt index e26132c25a..d5eb554d6e 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt @@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize import java.util.UUID @Parcelize -data class KeyCodeTriggerKeyEntity( +data class KeyEventTriggerKeyEntity( @SerializedName(NAME_KEYCODE) val keyCode: Int, @@ -24,12 +24,16 @@ data class KeyCodeTriggerKeyEntity( @SerializedName(NAME_UID) override val uid: String = UUID.randomUUID().toString(), + + @SerializedName(NAME_SCANCODE) + val scanCode: Int? = null ) : TriggerKeyEntity(), Parcelable { companion object { // DON'T CHANGE THESE. Used for JSON serialization and parsing. const val NAME_KEYCODE = "keyCode" + const val NAME_SCANCODE = "scanCode" const val NAME_DEVICE_ID = "deviceId" const val NAME_DEVICE_NAME = "deviceName" const val NAME_FLAGS = "flags" diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt index ce98690420..a5b4719d4a 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt @@ -35,7 +35,8 @@ sealed class TriggerKeyEntity : Parcelable { val SERIALIZER: JsonSerializer = jsonSerializer { (key) -> when (key) { is AssistantTriggerKeyEntity -> Gson().toJsonTree(key) - is KeyCodeTriggerKeyEntity -> Gson().toJsonTree(key) + is KeyEventTriggerKeyEntity -> Gson().toJsonTree(key) + is EvdevTriggerKeyEntity -> Gson().toJsonTree(key) is FloatingButtonKeyEntity -> Gson().toJsonTree(key) is FingerprintTriggerKeyEntity -> Gson().toJsonTree(key) } @@ -60,6 +61,10 @@ sealed class TriggerKeyEntity : Parcelable { return@jsonDeserializer deserializeFingerprintTriggerKey(json, uid!!) } + json.obj.has(EvdevTriggerKeyEntity.NAME_DEVICE_DESCRIPTOR) -> { + return@jsonDeserializer deserializeEvdevTriggerKey(json, uid!!) + } + else -> { return@jsonDeserializer deserializeKeyCodeTriggerKey(json, uid!!) } @@ -96,17 +101,39 @@ sealed class TriggerKeyEntity : Parcelable { return FingerprintTriggerKeyEntity(type, clickType, uid) } + private fun deserializeEvdevTriggerKey( + json: JsonElement, + uid: String, + ): EvdevTriggerKeyEntity { + val keyCode by json.byInt(EvdevTriggerKeyEntity.NAME_KEYCODE) + val scanCode by json.byInt(EvdevTriggerKeyEntity.NAME_SCANCODE) + val deviceDescriptor by json.byString(EvdevTriggerKeyEntity.NAME_DEVICE_DESCRIPTOR) + val deviceName by json.byString(EvdevTriggerKeyEntity.NAME_DEVICE_NAME) + val clickType by json.byInt(NAME_CLICK_TYPE) + val flags by json.byNullableInt(EvdevTriggerKeyEntity.NAME_FLAGS) + + return EvdevTriggerKeyEntity( + keyCode, + scanCode, + deviceDescriptor, + deviceName, + clickType, + flags ?: 0, + uid, + ) + } + private fun deserializeKeyCodeTriggerKey( json: JsonElement, uid: String, - ): KeyCodeTriggerKeyEntity { - val keyCode by json.byInt(KeyCodeTriggerKeyEntity.NAME_KEYCODE) - val deviceId by json.byString(KeyCodeTriggerKeyEntity.NAME_DEVICE_ID) - val deviceName by json.byNullableString(KeyCodeTriggerKeyEntity.NAME_DEVICE_NAME) + ): KeyEventTriggerKeyEntity { + val keyCode by json.byInt(KeyEventTriggerKeyEntity.NAME_KEYCODE) + val deviceId by json.byString(KeyEventTriggerKeyEntity.NAME_DEVICE_ID) + val deviceName by json.byNullableString(KeyEventTriggerKeyEntity.NAME_DEVICE_NAME) val clickType by json.byInt(NAME_CLICK_TYPE) - val flags by json.byNullableInt(KeyCodeTriggerKeyEntity.NAME_FLAGS) + val flags by json.byNullableInt(KeyEventTriggerKeyEntity.NAME_FLAGS) - return KeyCodeTriggerKeyEntity( + return KeyEventTriggerKeyEntity( keyCode, deviceId, deviceName, diff --git a/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt b/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt index 41037750a4..df4375a50d 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt @@ -14,7 +14,7 @@ import com.google.gson.JsonObject import com.google.gson.JsonParser import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.data.entities.ActionEntity -import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity import timber.log.Timber /** @@ -103,7 +103,7 @@ object Migration1To2 { createTriggerKey2( it.asInt, - KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE, + KeyEventTriggerKeyEntity.DEVICE_ID_ANY_DEVICE, clickType, ) } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt b/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt index a137888a53..e23a70d779 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt @@ -11,7 +11,7 @@ import com.google.gson.GsonBuilder import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag -import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity import io.github.sds100.keymapper.data.entities.TriggerEntity import io.github.sds100.keymapper.data.entities.TriggerKeyEntity @@ -40,10 +40,10 @@ object Migration6To7 { val trigger = gson.fromJson(getString(triggerColumnIndex)) val newTriggerKeys = trigger.keys - .mapNotNull { it as? KeyCodeTriggerKeyEntity } + .mapNotNull { it as? KeyEventTriggerKeyEntity } .map { key -> if (trigger.flags.hasFlag(TRIGGER_FLAG_DONT_OVERRIDE_DEFAULT_ACTION)) { - key.copy(flags = key.flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT)) + key.copy(flags = key.flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT)) } else { key } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 58b969939b..1f95db335a 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -303,7 +303,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo return; } - epollFd = epoll_create1(EPOLL_CLOEXEC); + epollFd = epoll_create1(EPOLL_CLOEXEC | EPOLLWAKEUP); if (epollFd == -1) { LOGE("Failed to create epoll fd: %s", strerror(errno)); return; diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt index 669fd15ff5..638ac9dd47 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt @@ -370,7 +370,7 @@ object InputEventUtils { /** * Used for keyCode to scanCode fallback to go past possible keyCode values */ - val KEYCODE_TO_SCANCODE_OFFSET: Int = 1000 + const val KEYCODE_TO_SCANCODE_OFFSET: Int = 1000 fun isModifierKey(keyCode: Int): Boolean = keyCode in MODIFIER_KEYCODES diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt index 35f12eac84..f52c165dbc 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt @@ -11,18 +11,24 @@ import io.github.sds100.keymapper.common.utils.InputDeviceUtils data class KMGamePadEvent( val eventTime: Long, val metaState: Int, - val device: InputDeviceInfo?, + val device: InputDeviceInfo, val axisHatX: Float, val axisHatY: Float, ) : KMInputEvent { - override val deviceId: Int? = device?.id + companion object { + fun fromMotionEvent(event: MotionEvent): KMGamePadEvent? { + val device = event.device ?: return null - constructor(event: MotionEvent) : this( - eventTime = event.eventTime, - metaState = event.metaState, - device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) }, - axisHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X), - axisHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y), - ) + return KMGamePadEvent( + eventTime = event.eventTime, + metaState = event.metaState, + device = InputDeviceUtils.createInputDeviceInfo(device), + axisHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X), + axisHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y), + ) + } + } + + override val deviceId: Int = device.id } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 4de620afb3..516d3bcf84 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -13,24 +13,30 @@ data class KMKeyEvent( val action: Int, val metaState: Int, val scanCode: Int, - val device: InputDeviceInfo?, + val device: InputDeviceInfo, val repeatCount: Int, val source: Int, val eventTime: Long, ) : KMInputEvent { - override val deviceId: Int? = device?.id + companion object { + fun fromKeyEvent(keyEvent: KeyEvent): KMKeyEvent? { + val device = keyEvent.device ?: return null - constructor(keyEvent: KeyEvent) : this( - keyCode = keyEvent.keyCode, - action = keyEvent.action, - metaState = keyEvent.metaState, - scanCode = keyEvent.scanCode, - device = keyEvent.device?.let { InputDeviceUtils.createInputDeviceInfo(it) }, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source, - eventTime = keyEvent.eventTime, - ) + return KMKeyEvent( + keyCode = keyEvent.keyCode, + action = keyEvent.action, + metaState = keyEvent.metaState, + scanCode = keyEvent.scanCode, + device = InputDeviceUtils.createInputDeviceInfo(device), + repeatCount = keyEvent.repeatCount, + source = keyEvent.source, + eventTime = keyEvent.eventTime, + ) + } + } + + override val deviceId: Int = device.id fun toKeyEvent(): KeyEvent { return KeyEvent( From 5858f264ad6b66010a8d86495be8e0cca6e1018a Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 7 Aug 2025 22:26:52 +0100 Subject: [PATCH 064/215] #1394 create separate class for KeyMapAlgorithm from KeyMapController --- .../sds100/keymapper/base/keymaps/KeyMap.kt | 7 +- .../keymaps/detection/DetectKeyMapsUseCase.kt | 6 - ...KeyMapController.kt => KeyMapAlgorithm.kt} | 56 +--- .../detection/KeyMapDetectionController.kt | 84 ++++++ .../BaseAccessibilityServiceController.kt | 35 +-- ...ntrollerTest.kt => KeyMapAlgorithmTest.kt} | 247 ++++++++---------- 6 files changed, 205 insertions(+), 230 deletions(-) rename base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/{KeyMapController.kt => KeyMapAlgorithm.kt} (97%) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt rename base/src/test/java/io/github/sds100/keymapper/base/keymaps/{KeyMapControllerTest.kt => KeyMapAlgorithmTest.kt} (96%) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt index b7d543ef5c..3105e02eb9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt @@ -8,7 +8,7 @@ import io.github.sds100.keymapper.base.actions.canBeHeldDown import io.github.sds100.keymapper.base.constraints.ConstraintEntityMapper import io.github.sds100.keymapper.base.constraints.ConstraintModeEntityMapper import io.github.sds100.keymapper.base.constraints.ConstraintState -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController +import io.github.sds100.keymapper.base.keymaps.detection.KeyMapAlgorithm import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerEntityMapper import io.github.sds100.keymapper.base.trigger.TriggerKey @@ -37,13 +37,14 @@ data class KeyMap( val vibrateDuration: Int? get() = trigger.vibrateDuration - fun isRepeatingActionsAllowed(): Boolean = KeyMapController.performActionOnDown(trigger) + fun isRepeatingActionsAllowed(): Boolean = KeyMapAlgorithm.performActionOnDown(trigger) fun isChangingActionRepeatRateAllowed(action: Action): Boolean = action.repeat && isRepeatingActionsAllowed() fun isChangingActionRepeatDelayAllowed(action: Action): Boolean = action.repeat && isRepeatingActionsAllowed() - fun isHoldingDownActionAllowed(action: Action): Boolean = KeyMapController.performActionOnDown(trigger) && action.data.canBeHeldDown() + fun isHoldingDownActionAllowed(action: Action): Boolean = + KeyMapAlgorithm.performActionOnDown(trigger) && action.data.canBeHeldDown() fun isHoldingDownActionBeforeRepeatingAllowed(action: Action): Boolean = action.repeat && action.holdDown diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt index 60191de2d2..2ce1bb6528 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt @@ -27,7 +27,6 @@ import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter @@ -55,7 +54,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( private val groupRepository: GroupRepository, private val preferenceRepository: PreferenceRepository, private val suAdapter: SuAdapter, - private val displayAdapter: DisplayAdapter, private val volumeAdapter: VolumeAdapter, private val toastAdapter: ToastAdapter, private val permissionAdapter: PermissionAdapter, @@ -243,8 +241,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( } } } - - override val isScreenOn: Flow = displayAdapter.isScreenOn } interface DetectKeyMapsUseCase { @@ -273,6 +269,4 @@ interface DetectKeyMapsUseCase { scanCode: Int = 0, source: Int = InputDevice.SOURCE_UNKNOWN, ) - - val isScreenOn: Flow } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt similarity index 97% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index 09c853d485..5a2773d5e0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -11,9 +11,6 @@ import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.constraints.isSatisfied -import io.github.sds100.keymapper.base.input.InputEventDetectionSource -import io.github.sds100.keymapper.base.input.InputEventHub -import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey @@ -32,26 +29,21 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent -import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -class KeyMapController( +class KeyMapAlgorithm( private val coroutineScope: CoroutineScope, private val useCase: DetectKeyMapsUseCase, private val performActionsUseCase: PerformActionsUseCase, private val detectConstraints: DetectConstraintsUseCase, - private val inputEventHub: InputEventHub, - isPausedFlow: Flow -) : InputEventHubCallback { +) { companion object { // the states for keys awaiting a double press @@ -70,11 +62,9 @@ class KeyMapController( ) || trigger.mode is TriggerMode.Parallel - - private const val INPUT_EVENT_HUB_ID = "key_map_controller" } - private fun loadKeyMaps(value: List) { + fun loadKeyMaps(value: List) { actionMap.clear() // If there are no keymaps with actions then keys don't need to be detected. @@ -343,8 +333,6 @@ class KeyMapController( } } - reset() - triggers.flatMap { trigger -> trigger.keys.filterIsInstance() }.toList() @@ -385,7 +373,7 @@ class KeyMapController( this.triggerKeysThatSendRepeatedKeyEvents = triggerKeysThatSendRepeatedKeyEvents - + reset() } } @@ -573,40 +561,6 @@ class KeyMapController( private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() - private val isPaused: StateFlow = - isPausedFlow.stateIn(coroutineScope, SharingStarted.Eagerly, true) - - init { - coroutineScope.launch { - useCase.allKeyMapList.collectLatest { keyMapList -> - reset() - loadKeyMaps(keyMapList) - } - } - - inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) - } - - fun teardown() { - reset() - inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) - } - - override fun onInputEvent( - event: KMInputEvent, - detectionSource: InputEventDetectionSource - ): Boolean { - if (isPaused.value) { - return false - } - - if (event is KMKeyEvent) { - return onKeyEvent(event) - } else { - return false - } - } - fun onMotionEvent(event: KMGamePadEvent): Boolean { if (!detectKeyMaps) return false @@ -1547,8 +1501,6 @@ class KeyMapController( performActionsAfterSequenceTriggerTimeout.forEach { (_, job) -> job.cancel() } performActionsAfterSequenceTriggerTimeout.clear() - - inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList()) } /** diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt new file mode 100644 index 0000000000..215f42315a --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt @@ -0,0 +1,84 @@ +package io.github.sds100.keymapper.base.keymaps.detection + +import io.github.sds100.keymapper.base.actions.PerformActionsUseCase +import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHub +import io.github.sds100.keymapper.base.input.InputEventHubCallback +import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.system.inputevents.KMInputEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class KeyMapDetectionController( + private val coroutineScope: CoroutineScope, + private val detectUseCase: DetectKeyMapsUseCase, + private val performActionsUseCase: PerformActionsUseCase, + private val detectConstraints: DetectConstraintsUseCase, + private val inputEventHub: InputEventHub, + private val pauseKeyMapsUseCase: PauseKeyMapsUseCase +) : InputEventHubCallback { + companion object { + private const val INPUT_EVENT_HUB_ID = "key_map_controller" + } + + private val algorithm: KeyMapAlgorithm = + KeyMapAlgorithm(coroutineScope, detectUseCase, performActionsUseCase, detectConstraints) + + private val isPaused: StateFlow = + pauseKeyMapsUseCase.isPaused.stateIn(coroutineScope, SharingStarted.Eagerly, true) + + init { + coroutineScope.launch { + detectUseCase.allKeyMapList.collectLatest { keyMapList -> + algorithm.reset() + algorithm.loadKeyMaps(keyMapList) + } + } + + coroutineScope.launch { + isPaused.collect { isPaused -> + if (isPaused) { + reset() + } + } + } + + inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) + } + + override fun onInputEvent( + event: KMInputEvent, + detectionSource: InputEventDetectionSource + ): Boolean { + if (isPaused.value) { + return false + } + + if (event is KMKeyEvent) { + return algorithm.onKeyEvent(event) + } else { + return false + } + } + + fun onFingerprintGesture(type: FingerprintGestureType) { + algorithm.onFingerprintGesture(type) + } + + fun teardown() { + reset() + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + } + + private fun reset() { + algorithm.reset() + inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList()) + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 853b2fe59b..b129c728c1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -19,8 +19,7 @@ import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCa import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl -import io.github.sds100.keymapper.base.keymaps.detection.DetectScreenOffKeyEventsController -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController +import io.github.sds100.keymapper.base.keymaps.detection.KeyMapDetectionController import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.common.utils.firstBlocking @@ -37,7 +36,6 @@ import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import io.github.sds100.keymapper.system.root.SuAdapter -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -55,7 +53,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber abstract class BaseAccessibilityServiceController( @@ -91,13 +88,13 @@ abstract class BaseAccessibilityServiceController( val detectConstraintsUseCase = detectConstraintsUseCaseFactory.create(service) - val keyMapController = KeyMapController( + val keyMapDetectionController = KeyMapDetectionController( service.lifecycleScope, detectKeyMapsUseCase, performActionsUseCase, detectConstraintsUseCase, inputEventHub, - pauseKeyMapsUseCase.isPaused + pauseKeyMapsUseCase ) val triggerKeyMapFromOtherAppsController = TriggerKeyMapFromOtherAppsController( @@ -114,17 +111,6 @@ abstract class BaseAccessibilityServiceController( val accessibilityNodeRecorder = accessibilityNodeRecorderFactory.create(service) - private val detectScreenOffKeyEventsController = DetectScreenOffKeyEventsController( - suAdapter, - devicesAdapter, - ) { event -> - if (!isPaused.value) { - withContext(Dispatchers.Main.immediate) { - keyMapController.onKeyEvent(event) - } - } - } - val isPaused: StateFlow = pauseKeyMapsUseCase.isPaused .stateIn(service.lifecycleScope, SharingStarted.Eagerly, false) @@ -249,20 +235,9 @@ abstract class BaseAccessibilityServiceController( } pauseKeyMapsUseCase.isPaused.distinctUntilChanged().onEach { - keyMapController.reset() triggerKeyMapFromOtherAppsController.reset() }.launchIn(service.lifecycleScope) - detectKeyMapsUseCase.isScreenOn.onEach { isScreenOn -> - if (!isScreenOn) { - if (screenOffTriggersEnabled.value) { - detectScreenOffKeyEventsController.startListening(service.lifecycleScope) - } - } else { - detectScreenOffKeyEventsController.stopListening() - } - }.launchIn(service.lifecycleScope) - inputEvents.onEach { onEventFromUi(it) }.launchIn(service.lifecycleScope) @@ -379,7 +354,7 @@ abstract class BaseAccessibilityServiceController( } open fun onDestroy() { - keyMapController.teardown() + keyMapDetectionController.teardown() keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_ACCESSIBILITY_SERVICE) accessibilityNodeRecorder.teardown() } @@ -510,7 +485,7 @@ abstract class BaseAccessibilityServiceController( } fun onFingerprintGesture(type: FingerprintGestureType) { - keyMapController.onFingerprintGesture(type) + keyMapDetectionController.onFingerprintGesture(type) } private fun triggerKeyMapFromIntent(uid: String) { diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt similarity index 96% rename from base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt rename to base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index c77c64d251..663e93acaf 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -15,7 +15,7 @@ import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController +import io.github.sds100.keymapper.base.keymaps.detection.KeyMapAlgorithm import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice @@ -35,14 +35,12 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputmethod.ImeInfo import junitparams.JUnitParamsRunner import junitparams.Parameters import junitparams.naming.TestCaseName import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceTimeBy @@ -69,7 +67,7 @@ import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @RunWith(JUnitParamsRunner::class) -class KeyMapControllerTest { +class KeyMapAlgorithmTest { companion object { private const val FAKE_KEYBOARD_DEVICE_ID = 123 @@ -78,13 +76,13 @@ class KeyMapControllerTest { descriptor = FAKE_KEYBOARD_DESCRIPTOR, name = "Fake Keyboard", ) - private val FAKE_KEYBOARD_DEVICE = InputDeviceInfo( - descriptor = FAKE_KEYBOARD_DESCRIPTOR, - name = "Fake Keyboard", - id = FAKE_KEYBOARD_DEVICE_ID, - isExternal = true, + private val FAKE_INTERNAL_DEVICE = InputDeviceInfo( + descriptor = "volume_keys", + name = "Volume keys", + id = 0, + isExternal = false, isGameController = false, - sources = InputDevice.SOURCE_KEYBOARD + sources = InputDevice.SOURCE_UNKNOWN ) private const val FAKE_HEADPHONE_DESCRIPTOR = "fake_headphone" @@ -101,7 +99,7 @@ class KeyMapControllerTest { private val FAKE_CONTROLLER_INPUT_DEVICE = InputDeviceInfo( descriptor = FAKE_CONTROLLER_DESCRIPTOR, name = "Fake Controller", - id = 0, + id = 1, isExternal = true, isGameController = true, sources = InputDevice.SOURCE_GAMEPAD @@ -125,30 +123,12 @@ class KeyMapControllerTest { private val TEST_ACTION_2: Action = Action( data = ActionData.App(FAKE_PACKAGE_NAME), ) - - private val GUI_KEYBOARD_IME_INFO = ImeInfo( - id = "ime_id", - packageName = "io.github.sds100.keymapper.inputmethod.latin", - label = "Key Mapper GUI Keyboard", - isEnabled = true, - isChosen = true, - ) - - private val GBOARD_IME_INFO = ImeInfo( - id = "gboard_id", - packageName = "com.google.android.inputmethod.latin", - label = "Gboard", - isEnabled = true, - isChosen = false, - ) } - private lateinit var controller: KeyMapController + private lateinit var controller: KeyMapAlgorithm private lateinit var detectKeyMapsUseCase: DetectKeyMapsUseCase private lateinit var performActionsUseCase: PerformActionsUseCase private lateinit var detectConstraintsUseCase: DetectConstraintsUseCase - private lateinit var keyMapListFlow: MutableStateFlow> - private lateinit var detectKeyMapListFlow: MutableStateFlow> @get:Rule var instantExecutorRule = InstantTaskExecutorRule() @@ -156,19 +136,17 @@ class KeyMapControllerTest { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) + private fun loadKeyMaps(vararg keyMap: KeyMap) { + controller.loadKeyMaps(keyMap.map { DetectKeyMapModel(it) }) + } + + private fun loadKeyMaps(vararg keyMap: DetectKeyMapModel) { + controller.loadKeyMaps(keyMap.toList()) + } + @Before fun init() { - keyMapListFlow = MutableStateFlow(emptyList()) - detectKeyMapListFlow = MutableStateFlow(emptyList()) - detectKeyMapsUseCase = mock { - on { allKeyMapList } doReturn combine( - keyMapListFlow, - detectKeyMapListFlow, - ) { keyMapList, detectKeyMapList -> - keyMapList.map { DetectKeyMapModel(keyMap = it) }.plus(detectKeyMapList) - } - MutableStateFlow(VIBRATION_DURATION).apply { on { defaultVibrateDuration } doReturn this } @@ -217,20 +195,18 @@ class KeyMapControllerTest { on { getSnapshot() } doReturn TestConstraintSnapshot() } - controller = KeyMapController( + controller = KeyMapAlgorithm( testScope, detectKeyMapsUseCase, performActionsUseCase, detectConstraintsUseCase, - inputEventHub = mock(), - isPausedFlow = MutableStateFlow(false) ) } @Test fun `Do not perform if one group constraint set is not satisfied`() = runTest(testDispatcher) { val trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)) - detectKeyMapListFlow.value = listOf( + loadKeyMaps( DetectKeyMapModel( keyMap = KeyMap( trigger = trigger, @@ -278,7 +254,7 @@ class KeyMapControllerTest { fun `Perform if all group constraints and key map constraints are satisfied`() = runTest(testDispatcher) { val trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)) - detectKeyMapListFlow.value = listOf( + loadKeyMaps( DetectKeyMapModel( keyMap = KeyMap( trigger = trigger, @@ -359,7 +335,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), KeyMap(2, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), @@ -400,7 +376,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -439,7 +415,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -480,7 +456,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -521,7 +497,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -562,7 +538,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -601,7 +577,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -617,7 +593,7 @@ class KeyMapControllerTest { @Test fun `Sequence trigger with fingerprint gesture and key code`() = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( trigger = sequenceTrigger( triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), @@ -642,7 +618,7 @@ class KeyMapControllerTest { @Test fun `Input fingerprint gesture`() = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( trigger = singleKeyTrigger( FingerprintTriggerKey( @@ -675,7 +651,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(KeyEvent.KEYCODE_J), triggerKey(KeyEvent.KEYCODE_K)) val enterAction = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = sequenceTrigger, actionList = listOf(enterAction)), ) @@ -726,7 +702,7 @@ class KeyMapControllerTest { val sequenceTriggerAction2 = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = sequenceTrigger1, actionList = listOf(sequenceTriggerAction1)), KeyMap(2, trigger = sequenceTrigger2, actionList = listOf(sequenceTriggerAction2)), @@ -769,7 +745,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(KeyEvent.KEYCODE_J), triggerKey(KeyEvent.KEYCODE_K)) val enterAction = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = pasteTrigger, actionList = listOf(pasteAction)), KeyMap(2, trigger = sequenceTrigger, actionList = listOf(enterAction)), @@ -802,7 +778,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(KeyEvent.KEYCODE_J), triggerKey(KeyEvent.KEYCODE_K)) val enterAction = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = pasteTrigger, actionList = listOf(pasteAction)), KeyMap(2, trigger = sequenceTrigger, actionList = listOf(enterAction)), @@ -834,7 +810,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(KeyEvent.KEYCODE_J), triggerKey(KeyEvent.KEYCODE_K)) val enterAction = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = pasteTrigger, actionList = listOf(pasteAction)), KeyMap(2, trigger = sequenceTrigger, actionList = listOf(enterAction)), @@ -867,7 +843,7 @@ class KeyMapControllerTest { holdDown = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(action)), ) @@ -893,7 +869,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -928,7 +904,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -957,7 +933,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -984,7 +960,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -1013,7 +989,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = trigger, @@ -1062,7 +1038,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = longPressTrigger, @@ -1103,7 +1079,7 @@ class KeyMapControllerTest { ) val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff())) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = shortPressTrigger, @@ -1144,7 +1120,7 @@ class KeyMapControllerTest { ) val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff())) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = shortPressTrigger, @@ -1192,7 +1168,7 @@ class KeyMapControllerTest { ) .copy(longPressDelay = 500) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = longerTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = shorterTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -1267,7 +1243,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) var isFlashlightEnabled = false @@ -1321,7 +1297,7 @@ class KeyMapControllerTest { actionList = listOf(TEST_ACTION_2), ) - keyMapListFlow.value = listOf(keyMap1, keyMap2) + loadKeyMaps(keyMap1, keyMap2) // WHEN inOrder(performActionsUseCase) { @@ -1367,7 +1343,7 @@ class KeyMapControllerTest { val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) val actionList = listOf(Action(data = ActionData.InputKeyEvent(2))) - keyMapListFlow.value = listOf(KeyMap(trigger = trigger, actionList = actionList)) + loadKeyMaps(KeyMap(trigger = trigger, actionList = actionList)) // WHEN whenever(performActionsUseCase.getErrorSnapshot()).thenReturn(object : @@ -1405,7 +1381,7 @@ class KeyMapControllerTest { Action(data = ActionData.InputKeyEvent(2)), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = actionList), ) @@ -1435,7 +1411,7 @@ class KeyMapControllerTest { repeatLimit = 2, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = listOf(action)), ) @@ -1472,15 +1448,13 @@ class KeyMapControllerTest { data = ActionData.InputKeyEvent(keyCode = 3), ) - val keyMaps = listOf( + loadKeyMaps( KeyMap( trigger = trigger, actionList = listOf(action1, action2, action3), ), ) - keyMapListFlow.value = keyMaps - // WHEN // ensure consumed @@ -1499,7 +1473,7 @@ class KeyMapControllerTest { // GIVEN val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val keyMaps = listOf( + loadKeyMaps( KeyMap( trigger = trigger, actionList = listOf(TEST_ACTION), @@ -1510,8 +1484,6 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = keyMaps - // WHEN // ensure consumed @@ -1547,7 +1519,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(keyMap.trigger.keys[0]) @@ -1578,7 +1550,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(keyMap.trigger.keys[0]) @@ -1611,7 +1583,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(keyMap.trigger.keys[0]) @@ -1647,7 +1619,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(keyMap.trigger.keys[0], delay = 300) @@ -1675,7 +1647,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN @@ -1692,7 +1664,7 @@ class KeyMapControllerTest { @Test fun `overlapping triggers 3`() = runTest(testDispatcher) { // GIVEN - val keyMaps = listOf( + val keyMaps = arrayOf( KeyMap( trigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), @@ -1708,7 +1680,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = keyMaps + loadKeyMaps(*keyMaps) inOrder(performActionsUseCase) { // WHEN @@ -1760,7 +1732,7 @@ class KeyMapControllerTest { @Test fun `overlapping triggers 2`() = runTest(testDispatcher) { // GIVEN - val keyMaps = listOf( + val keyMaps = arrayOf( KeyMap( trigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_P), @@ -1776,7 +1748,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = keyMaps + loadKeyMaps(*keyMaps) inOrder(performActionsUseCase) { // WHEN @@ -1821,7 +1793,7 @@ class KeyMapControllerTest { @Test fun `overlapping triggers 1`() = runTest(testDispatcher) { // GIVEN - val keyMaps = listOf( + val keyMaps = arrayOf( KeyMap( trigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_CTRL_LEFT), @@ -1839,7 +1811,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = keyMaps + loadKeyMaps(*keyMaps) inOrder(performActionsUseCase) { // WHEN @@ -1923,7 +1895,7 @@ class KeyMapControllerTest { triggerKey(keyCode = 2), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( trigger = trigger, actionList = listOf(TEST_ACTION), @@ -1983,7 +1955,7 @@ class KeyMapControllerTest { triggerKey(keyCode = 2), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( trigger = trigger, actionList = listOf(TEST_ACTION), @@ -2021,7 +1993,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(triggerKey(keyCode = 2), delay = 1) @@ -2065,7 +2037,7 @@ class KeyMapControllerTest { repeat = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), ) @@ -2112,7 +2084,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) val action2 = Action(data = ActionData.InputKeyEvent(keyCode = 3)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), ) @@ -2168,7 +2140,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) val action3 = Action(data = ActionData.InputKeyEvent(keyCode = 4)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), KeyMap(2, trigger = trigger3, actionList = listOf(action3)), @@ -2223,7 +2195,7 @@ class KeyMapControllerTest { parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) val action2 = Action(data = ActionData.InputKeyEvent(keyCode = 3), repeat = true) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), ) @@ -2273,7 +2245,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) val action2 = Action(data = ActionData.InputKeyEvent(keyCode = 3)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), ) @@ -2335,7 +2307,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) val action3 = Action(data = ActionData.InputKeyEvent(keyCode = 4)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), KeyMap(2, trigger = trigger3, actionList = listOf(action3)), @@ -2382,7 +2354,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -2413,7 +2385,7 @@ class KeyMapControllerTest { holdDown = true, ) - keyMapListFlow.value = listOf(KeyMap(trigger = trigger, actionList = listOf(action))) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(action))) val metaState = KeyEvent.META_META_ON.withFlag(KeyEvent.META_META_LEFT_ON) @@ -2572,7 +2544,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_A), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = oneKeyTrigger, actionList = listOf(TEST_ACTION_2)), KeyMap(1, trigger = twoKeyTrigger, actionList = listOf(TEST_ACTION)), ) @@ -2617,7 +2589,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = triggerKeyboard, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = triggerAnyDevice, actionList = listOf(TEST_ACTION_2)), ) @@ -2637,7 +2609,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_A, FAKE_HEADPHONE_TRIGGER_KEY_DEVICE), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = triggerHeadphone, actionList = listOf(TEST_ACTION)), ) @@ -2667,7 +2639,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keymap) + loadKeyMaps(keymap) // WHEN mockTriggerKeyInput(trigger.keys[0]) @@ -2695,7 +2667,7 @@ class KeyMapControllerTest { runTest(testDispatcher) { val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_CTRL_LEFT)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = trigger, @@ -2775,7 +2747,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = firstTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = secondTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -2804,18 +2776,18 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = homeTrigger, actionList = listOf(TEST_ACTION)), ) val consumedHomeDown = - inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_DOWN, FAKE_KEYBOARD_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_KEYBOARD_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_DOWN, FAKE_INTERNAL_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_INTERNAL_DEVICE) advanceUntilIdle() - inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_UP, FAKE_KEYBOARD_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, FAKE_KEYBOARD_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_UP, FAKE_INTERNAL_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, FAKE_INTERNAL_DEVICE) assertThat(consumedHomeDown, `is`(true)) @@ -2828,7 +2800,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = recentsTrigger, actionList = listOf(TEST_ACTION)), ) @@ -2836,14 +2808,14 @@ class KeyMapControllerTest { inputKeyEvent( KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_DOWN, - FAKE_KEYBOARD_DEVICE + FAKE_INTERNAL_DEVICE ) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_KEYBOARD_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_INTERNAL_DEVICE) advanceUntilIdle() - inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_UP, FAKE_KEYBOARD_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, FAKE_KEYBOARD_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_UP, FAKE_INTERNAL_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, FAKE_INTERNAL_DEVICE) assertThat(consumedRecentsDown, `is`(true)) } @@ -2857,7 +2829,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -2898,7 +2870,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -2931,7 +2903,7 @@ class KeyMapControllerTest { repeat = true, ) - keyMapListFlow.value = listOf(KeyMap(0, trigger = trigger, actionList = listOf(action))) + loadKeyMaps(KeyMap(0, trigger = trigger, actionList = listOf(action))) when (trigger.mode) { is TriggerMode.Parallel -> mockParallelTrigger(trigger, delay = 2000L) @@ -2978,8 +2950,7 @@ class KeyMapControllerTest { trigger: Trigger, ) = runTest(testDispatcher) { // given - keyMapListFlow.value = - listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) + loadKeyMaps(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when (trigger.keys[1] as KeyEventTriggerKey).let { @@ -3028,8 +2999,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) - keyMapListFlow.value = - listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) + loadKeyMaps(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { @@ -3067,8 +3037,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP, clickType = ClickType.LONG_PRESS), ) - keyMapListFlow.value = - listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) + loadKeyMaps(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { @@ -3115,7 +3084,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = longPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -3141,7 +3110,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -3172,7 +3141,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -3205,7 +3174,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = singleKeyTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = parallelTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -3232,7 +3201,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -3253,7 +3222,7 @@ class KeyMapControllerTest { runTest(testDispatcher) { val actionList = listOf(TEST_ACTION, TEST_ACTION_2) // GIVEN - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = actionList), ) @@ -3306,7 +3275,7 @@ class KeyMapControllerTest { fun invalidInput_downNotConsumed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN var consumedCount = 0 @@ -3333,7 +3302,7 @@ class KeyMapControllerTest { @TestCaseName("{0}") fun validInput_downConsumed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) var consumedCount = 0 @@ -3358,7 +3327,7 @@ class KeyMapControllerTest { @TestCaseName("{0}") fun validInput_doNotConsumeFlag_doNotConsumeDown(description: String, keyMap: KeyMap) = runTest(testDispatcher) { - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) var consumedCount = 0 @@ -4136,7 +4105,7 @@ class KeyMapControllerTest { @TestCaseName("{0}") fun validInput_actionPerformed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) if (keyMap.trigger.mode is TriggerMode.Parallel) { // WHEN @@ -4222,7 +4191,7 @@ class KeyMapControllerTest { private fun inputKeyEvent( keyCode: Int, action: Int, - device: InputDeviceInfo = FAKE_KEYBOARD_DEVICE, + device: InputDeviceInfo = FAKE_INTERNAL_DEVICE, metaState: Int? = null, scanCode: Int = 0, repeatCount: Int = 0, @@ -4259,7 +4228,7 @@ class KeyMapControllerTest { if (delay != null) { delay(delay) } else { - when ((trigger.mode as TriggerMode.Parallel).clickType) { + when (trigger.mode.clickType) { ClickType.SHORT_PRESS -> delay(50) ClickType.LONG_PRESS -> delay(LONG_PRESS_DELAY + 100L) ClickType.DOUBLE_PRESS -> {} From 1be08f41735af999d70b01222eb48b6062379db0 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 7 Aug 2025 22:32:41 +0100 Subject: [PATCH 065/215] #1394 fix KeyEventTriggerDevice tests --- .../base/trigger/KeyEventTriggerDevice.kt | 11 +++++---- .../base/trigger/TriggerKeyDeviceTest.kt | 23 ++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt index 2bb4b456b8..2f3c7ee069 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt @@ -30,11 +30,14 @@ sealed class KeyEventTriggerDevice() : Comparable { } fun isSameDevice(other: KeyEventTriggerDevice): Boolean { - if (other is External && this is External) { - return other.descriptor == this.descriptor - } else { + if (this is Any || other is Any) { return true } - } + if (this is External && other is External) { + return this.descriptor == other.descriptor + } else { + return this is Internal && other is Internal + } + } } \ No newline at end of file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt index 2e4deebe38..6e9e673d4e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt @@ -37,6 +37,27 @@ class TriggerKeyDeviceTest { fun `internal device is the same as an internal device`() { val device1 = KeyEventTriggerDevice.Internal val device2 = KeyEventTriggerDevice.Internal - assertThat(device1.isSameDevice(device2), `is`(false)) + assertThat(device1.isSameDevice(device2), `is`(true)) + } + + @Test + fun `any device is the same as an internal device`() { + val device1 = KeyEventTriggerDevice.Any + val device2 = KeyEventTriggerDevice.Internal + assertThat(device1.isSameDevice(device2), `is`(true)) + } + + @Test + fun `any device is the same as any device`() { + val device1 = KeyEventTriggerDevice.Any + val device2 = KeyEventTriggerDevice.Any + assertThat(device1.isSameDevice(device2), `is`(true)) + } + + @Test + fun `any device is the same as an external device`() { + val device1 = KeyEventTriggerDevice.Any + val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 0") + assertThat(device1.isSameDevice(device2), `is`(true)) } } \ No newline at end of file From 5edf0629d4e73175879db32596b00d969e6cd221 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 7 Aug 2025 22:45:58 +0100 Subject: [PATCH 066/215] #1394 libevdev_jni: do not use EPOLLWAKEUP --- sysbridge/src/main/cpp/libevdev_jni.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 1f95db335a..537760bb32 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -303,7 +303,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo return; } - epollFd = epoll_create1(EPOLL_CLOEXEC | EPOLLWAKEUP); +epollFd = epoll_create1(EPOLL_CLOEXEC); if (epollFd == -1) { LOGE("Failed to create epoll fd: %s", strerror(errno)); return; From ac8d5cdcdab8c6ff606a30fba64224ad2fb523f4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 7 Aug 2025 22:48:06 +0100 Subject: [PATCH 067/215] #1394 KeyMapDetectionController: register InputEventHub client first --- .../io/github/sds100/keymapper/base/input/InputEventHub.kt | 2 +- .../base/keymaps/detection/KeyMapDetectionController.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index fe5e39c6ad..9a2df7bba8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -226,7 +226,7 @@ class InputEventHubImpl @Inject constructor( override fun setGrabbedEvdevDevices(clientId: String, deviceDescriptors: List) { if (!clients.containsKey(clientId)) { - throw IllegalArgumentException("This client is not registered!") + throw IllegalArgumentException("This client $clientId is not registered when trying to grab devices!") } clients[clientId] = diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt index 215f42315a..5251491222 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt @@ -35,6 +35,9 @@ class KeyMapDetectionController( pauseKeyMapsUseCase.isPaused.stateIn(coroutineScope, SharingStarted.Eagerly, true) init { + // Must first register before collecting anything that may call reset() + inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) + coroutineScope.launch { detectUseCase.allKeyMapList.collectLatest { keyMapList -> algorithm.reset() @@ -49,8 +52,6 @@ class KeyMapDetectionController( } } } - - inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) } override fun onInputEvent( From 4af7d56400a24d9675bb6ade120b54eca736e284 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 7 Aug 2025 23:22:31 +0100 Subject: [PATCH 068/215] #1394 show pro mode keys in the user interface --- .../base/keymaps/KeyMapListItemCreator.kt | 1 + .../trigger/BaseConfigTriggerViewModel.kt | 24 ++- .../base/trigger/TriggerKeyListItem.kt | 26 ++- .../utils/ui/compose/icons/ProModeIcon.kt | 150 ++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt index c6c6ecae72..27ee133bfa 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt @@ -319,6 +319,7 @@ class KeyMapListItemCreator( val parts = buildList { + add("PRO") add(deviceName) if (!key.consumeEvent) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 2719744426..537bf88911 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -757,7 +757,19 @@ abstract class BaseConfigTriggerViewModel( } } - is EvdevTriggerKey -> TODO() + is EvdevTriggerKey -> EvdevEvent( + id = key.uid, + keyName = InputEventStrings.keyCodeToString(key.keyCode), + deviceName = key.deviceName, + clickType = clickType, + extraInfo = if (!key.consumeEvent) { + getString(R.string.flag_dont_override_default_action) + } else { + null + }, + linkType = linkType, + error = error, + ) } } } @@ -877,6 +889,16 @@ sealed class TriggerKeyListItemModel { override val error: TriggerError?, ) : TriggerKeyListItemModel() + data class EvdevEvent( + override val id: String, + override val linkType: LinkType, + val keyName: String, + val deviceName: String, + override val clickType: ClickType, + val extraInfo: String?, + override val error: TriggerError?, + ) : TriggerKeyListItemModel() + data class Assistant( override val id: String, override val linkType: LinkType, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index f3e26528e6..79c3aef869 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -46,6 +46,8 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.ui.LinkType import io.github.sds100.keymapper.base.utils.ui.compose.DragDropState +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon @Composable fun TriggerKeyListItem( @@ -112,6 +114,7 @@ fun TriggerKeyListItem( is TriggerKeyListItemModel.Assistant -> Icons.Outlined.Assistant is TriggerKeyListItemModel.FloatingButton -> Icons.Outlined.BubbleChart is TriggerKeyListItemModel.FingerprintGesture -> Icons.Outlined.Fingerprint + is TriggerKeyListItemModel.EvdevEvent -> KeyMapperIcons.ProModeIcon else -> null } @@ -138,6 +141,7 @@ fun TriggerKeyListItem( ) is TriggerKeyListItemModel.KeyEvent -> model.keyName + is TriggerKeyListItemModel.EvdevEvent -> "${model.keyName} (${model.deviceName})" is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource(R.string.trigger_error_floating_button_deleted_title) @@ -160,6 +164,7 @@ fun TriggerKeyListItem( val tertiaryText = when (model) { is TriggerKeyListItemModel.KeyEvent -> model.extraInfo + is TriggerKeyListItemModel.EvdevEvent -> model.extraInfo is TriggerKeyListItemModel.FloatingButton -> model.layoutName else -> null @@ -322,7 +327,7 @@ private fun ErrorTextColumn( @Preview @Composable -private fun KeyCodePreview() { +private fun KeyEventPreview() { TriggerKeyListItem( model = TriggerKeyListItemModel.KeyEvent( id = "id", @@ -338,6 +343,25 @@ private fun KeyCodePreview() { ) } +@Preview +@Composable +private fun EvdevEventPreview() { + TriggerKeyListItem( + model = TriggerKeyListItemModel.EvdevEvent( + id = "id", + keyName = "Volume Up", + deviceName = "Gpio-keys", + clickType = ClickType.SHORT_PRESS, + extraInfo = "Do not consume", + linkType = LinkType.ARROW, + error = null, + ), + isDragging = false, + isReorderingEnabled = true, + index = 0, + ) +} + @Preview @Composable private fun NoDragPreview() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt new file mode 100644 index 0000000000..f67b37bc8c --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt @@ -0,0 +1,150 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.ProModeIcon: ImageVector + get() { + if (_ProMode != null) { + return _ProMode!! + } + _ProMode = ImageVector.Builder( + name = "ProMode", + defaultWidth = 32.dp, + defaultHeight = 32.dp, + viewportWidth = 32f, + viewportHeight = 32f + ).apply { + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, -0f) + lineTo(32f, -0f) + lineTo(32f, 32f) + lineTo(-0f, 32f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + } + ) { + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(4f, 11f) + verticalLineToRelative(10f) + horizontalLineToRelative(2f) + verticalLineToRelative(-4f) + horizontalLineToRelative(2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, -2f) + verticalLineTo(13f) + arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 8f, 11f) + horizontalLineTo(4f) + moveToRelative(2f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineTo(6f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(13f, 11f) + verticalLineToRelative(10f) + horizontalLineToRelative(2f) + verticalLineToRelative(-4f) + horizontalLineToRelative(0.8f) + lineToRelative(1.2f, 4f) + horizontalLineToRelative(2f) + lineTo(17.76f, 16.85f) + curveTo(18.5f, 16.55f, 19f, 15.84f, 19f, 15f) + verticalLineToRelative(-2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, -2f) + horizontalLineToRelative(-4f) + moveToRelative(2f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineToRelative(-2f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(24f, 11f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, 2f) + verticalLineToRelative(6f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, 2f) + horizontalLineToRelative(2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, -2f) + verticalLineToRelative(-6f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, -2f) + horizontalLineToRelative(-2f) + moveToRelative(0f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(6f) + horizontalLineToRelative(-2f) + close() + } + }.build() + + return _ProMode!! + } + +@Suppress("ObjectPropertyName") +private var _ProMode: ImageVector? = null From f9117d2cd62e097190d45528889dc8e2aab1ed69 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 00:00:34 +0100 Subject: [PATCH 069/215] #1394 bunch of fixes for the input event pipeline --- .../keymapper/base/input/InputEventHub.kt | 17 ++++- .../base/keymaps/KeyMapListViewModel.kt | 3 - .../base/keymaps/detection/KeyMapAlgorithm.kt | 7 +-- .../detection/KeyMapDetectionController.kt | 21 ++++++- .../RerouteKeyEventsController.kt | 42 ++++++++++++- .../RerouteKeyEventsUseCase.kt | 6 +- .../BaseAccessibilityServiceController.kt | 63 ++++--------------- .../keymapper/sysbridge/IEvdevCallback.aidl | 1 + .../keymapper/sysbridge/BnEvdevCallback.h | 3 + .../keymapper/sysbridge/BpEvdevCallback.h | 1 + .../keymapper/sysbridge/IEvdevCallback.cpp | 54 +++++++++++++++- .../keymapper/sysbridge/IEvdevCallback.h | 6 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 4 ++ .../sysbridge/service/SystemBridge.kt | 1 + .../system/devices/AndroidDevicesAdapter.kt | 2 +- 15 files changed, 157 insertions(+), 74 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 9a2df7bba8..d86a495f19 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -91,6 +91,11 @@ class InputEventHubImpl @Inject constructor( } } + override fun onEvdevEventLoopStarted() { + Timber.i("On evdev event loop started") + invalidateGrabbedEvdevDevices() + } + override fun onEvdevEvent( deviceId: Int, timeSec: Long, @@ -142,10 +147,14 @@ class InputEventHubImpl @Inject constructor( // Only send events from evdev devices to the client if they grabbed it if (clientContext.grabbedEvdevDevices.contains(deviceDescriptor)) { - consume = consume || clientContext.callback.onInputEvent(event, detectionSource) + // Lazy evaluation may not execute this if its inlined? + val result = clientContext.callback.onInputEvent(event, detectionSource) + consume = consume || result } } else { - consume = consume || clientContext.callback.onInputEvent(event, detectionSource) + // Lazy evaluation may not execute this if its inlined? + val result = clientContext.callback.onInputEvent(event, detectionSource) + consume = consume || result } } @@ -254,6 +263,10 @@ class InputEventHubImpl @Inject constructor( private fun invalidateGrabbedEvdevDevices() { val descriptors: Set = clients.values.flatMap { it.grabbedEvdevDevices }.toSet() + if (systemBridge == null) { + return + } + try { systemBridge?.ungrabAllEvdevDevices() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt index 7f3749effe..90c49ecea1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt @@ -360,9 +360,6 @@ class KeyMapListViewModel( onAutomaticBackupResult(result) } } - - // TODO REMOVE - onNewKeyMapClick() } private fun buildSelectingAppBarState( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index 5a2773d5e0..249a0891de 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -333,10 +333,6 @@ class KeyMapAlgorithm( } } - triggers.flatMap { trigger -> - trigger.keys.filterIsInstance() - }.toList() - this.triggers = triggers.toTypedArray() this.triggerActions = triggerActions.toTypedArray() this.triggerConstraints = triggerConstraints.toTypedArray() @@ -407,7 +403,8 @@ class KeyMapAlgorithm( private var doublePressTimeoutTimes = longArrayOf() private var actionMap: SparseArrayCompat = SparseArrayCompat() - private var triggers: Array = emptyArray() + var triggers: Array = emptyArray() + private set /** * The events to detect for each sequence trigger. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt index 5251491222..91ee0cc885 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt @@ -7,14 +7,16 @@ import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey +import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import timber.log.Timber class KeyMapDetectionController( private val coroutineScope: CoroutineScope, @@ -39,16 +41,18 @@ class KeyMapDetectionController( inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) coroutineScope.launch { - detectUseCase.allKeyMapList.collectLatest { keyMapList -> + detectUseCase.allKeyMapList.collect { keyMapList -> algorithm.reset() algorithm.loadKeyMaps(keyMapList) + // Only grab the triggers that are actually being listened to by the algorithm + grabEvdevDevicesForTriggers(algorithm.triggers) } } coroutineScope.launch { isPaused.collect { isPaused -> if (isPaused) { - reset() + algorithm.reset() } } } @@ -78,6 +82,17 @@ class KeyMapDetectionController( inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) } + private fun grabEvdevDevicesForTriggers(triggers: Array) { + val evdevDevices = triggers + .flatMap { trigger -> trigger.keys.filterIsInstance() } + .map { it.deviceDescriptor } + .distinct() + .toList() + + Timber.i("Grab evdev devices for key map detection: ${evdevDevices.joinToString()}") + inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, evdevDevices) + } + private fun reset() { algorithm.reset() inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList()) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt index adb49b4a78..0e4b6e774f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt @@ -4,9 +4,13 @@ import android.view.KeyEvent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHub +import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import kotlinx.coroutines.CoroutineScope @@ -25,7 +29,13 @@ class RerouteKeyEventsController @AssistedInject constructor( private val coroutineScope: CoroutineScope, private val keyMapperImeMessenger: ImeInputEventInjector, private val useCaseFactory: RerouteKeyEventsUseCaseImpl.Factory, -) { + private val inputEventHub: InputEventHub +) : InputEventHubCallback { + + companion object { + private const val INPUT_EVENT_HUB_ID = "reroute_key_events" + } + @AssistedFactory interface Factory { fun create( @@ -43,8 +53,34 @@ class RerouteKeyEventsController @AssistedInject constructor( */ private var repeatJob: Job? = null - fun onKeyEvent(event: KMKeyEvent): Boolean { - if (!useCase.shouldRerouteKeyEvent(event.device?.descriptor)) { + init { + coroutineScope.launch { + useCase.isReroutingEnabled.collect { isEnabled -> + if (isEnabled) { + inputEventHub.registerClient( + INPUT_EVENT_HUB_ID, + this@RerouteKeyEventsController + ) + } else { + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + } + } + } + } + + fun teardown() { + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + } + + override fun onInputEvent( + event: KMInputEvent, + detectionSource: InputEventDetectionSource + ): Boolean { + if (event !is KMKeyEvent) { + return false + } + + if (!useCase.shouldRerouteKeyEvent(event.device.descriptor)) { return false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt index 83e7af7e95..048ff244e6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt @@ -12,6 +12,7 @@ import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking @@ -35,7 +36,7 @@ class RerouteKeyEventsUseCaseImpl @AssistedInject constructor( ): RerouteKeyEventsUseCaseImpl } - private val rerouteKeyEvents = + override val isReroutingEnabled: Flow = preferenceRepository.get(Keys.rerouteKeyEvents).map { it ?: false } private val devicesToRerouteKeyEvents = @@ -53,7 +54,7 @@ class RerouteKeyEventsUseCaseImpl @AssistedInject constructor( return false } - return rerouteKeyEvents.firstBlocking() && + return isReroutingEnabled.firstBlocking() && imeHelper.isCompatibleImeChosen() && ( descriptor != null && @@ -71,6 +72,7 @@ class RerouteKeyEventsUseCaseImpl @AssistedInject constructor( } interface RerouteKeyEventsUseCase { + val isReroutingEnabled: Flow fun shouldRerouteKeyEvent(descriptor: String?): Boolean fun inputKeyEvent(keyModel: InputKeyModel) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index b129c728c1..29c103794c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -104,10 +104,10 @@ abstract class BaseAccessibilityServiceController( detectConstraintsUseCase, ) - // TODO - val rerouteKeyEventsController = rerouteKeyEventsControllerFactory.create( - service.lifecycleScope, - ) + val rerouteKeyEventsController: RerouteKeyEventsController = + rerouteKeyEventsControllerFactory.create( + service.lifecycleScope, + ) val accessibilityNodeRecorder = accessibilityNodeRecorderFactory.create(service) @@ -115,10 +115,6 @@ abstract class BaseAccessibilityServiceController( pauseKeyMapsUseCase.isPaused .stateIn(service.lifecycleScope, SharingStarted.Eagerly, false) - private val screenOffTriggersEnabled: StateFlow = - detectKeyMapsUseCase.detectScreenOffTriggers - .stateIn(service.lifecycleScope, SharingStarted.Eagerly, false) - private val changeImeOnInputFocusFlow: StateFlow = settingsRepository .get(Keys.changeImeOnInputFocus) @@ -357,6 +353,7 @@ abstract class BaseAccessibilityServiceController( keyMapDetectionController.teardown() keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_ACCESSIBILITY_SERVICE) accessibilityNodeRecorder.teardown() + rerouteKeyEventsController.teardown() } open fun onConfigurationChanged(newConfig: Configuration) { @@ -367,33 +364,8 @@ abstract class BaseAccessibilityServiceController( detectionSource: InputEventDetectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ): Boolean { return inputEventHub.onInputEvent(event, detectionSource) - -// val detailedLogInfo = event.toString() -// -// // TODO move paused check to KeyMapController -// if (isPaused.value) { -// when (event.action) { -// KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") -// KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") -// } -// } else { -// try { -// var consume: Boolean -// -// consume = keyMapController.onKeyEvent(uniqueEvent) -// -// if (!consume) { -// consume = rerouteKeyEventsController.onKeyEvent(uniqueEvent) -// } -// -// return consume -// } catch (e: Exception) { -// Timber.e(e) -// } -// } } - // TODO handle somewhere else fun onKeyEventFromIme(event: KMKeyEvent): Boolean { /* Issue #850 @@ -403,33 +375,20 @@ abstract class BaseAccessibilityServiceController( before returning the UP key event. */ if (event.action == KeyEvent.ACTION_UP && (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP || event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) { - onKeyEvent( + inputEventHub.onInputEvent( event.copy(action = KeyEvent.ACTION_DOWN), detectionSource = InputEventDetectionSource.INPUT_METHOD, ) } - return onKeyEvent( - event, - detectionSource = InputEventDetectionSource.INPUT_METHOD, - ) + return inputEventHub.onInputEvent(event, InputEventDetectionSource.INPUT_METHOD) } fun onMotionEventFromIme(event: KMGamePadEvent): Boolean { - // TODO keymapcontroller will observe inputeventhub and check if a trigger is being recorded -// if (isPaused.value || record) { -// return false -// } -// -// try { -// val consume = keyMapController.onMotionEvent(event) -// -// return consume -// } catch (e: Exception) { -// Timber.e(e) -// return false -// } - return false + return inputEventHub.onInputEvent( + event, + detectionSource = InputEventDetectionSource.INPUT_METHOD, + ) } open fun onAccessibilityEvent(event: AccessibilityEvent) { diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl index 23c05af450..364d1c9386 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.sysbridge; interface IEvdevCallback { + void onEvdevEventLoopStarted(); void onEvdevEvent(int deviceId, long timeSec, long timeUsec, int type, int code, int value, int androidCode); } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index da05ac7c42..e22fdf87a4 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -34,6 +34,9 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { explicit IEvdevCallbackDelegator(const std::shared_ptr &impl) : _impl(impl) { } + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override { + return _impl->onEvdevEventLoopStarted(); + } ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override { return _impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode); } diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index eab4c785dc..c3ca3004de 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -19,6 +19,7 @@ class BpEvdevCallback : public ::ndk::BpCInterface { explicit BpEvdevCallback(const ::ndk::SpAIBinder& binder); virtual ~BpEvdevCallback(); + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override; }; } // namespace sysbridge diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index 7c47dd968a..818c81a077 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -20,7 +20,17 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback binder_status_t _aidl_ret_status = STATUS_UNKNOWN_TRANSACTION; std::shared_ptr _aidl_impl = std::static_pointer_cast(::ndk::ICInterface::asInterface(_aidl_binder)); switch (_aidl_code) { - case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/): { + case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEventLoopStarted*/): { + + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEventLoopStarted(); + _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); + if (_aidl_ret_status != STATUS_OK) break; + + if (!AStatus_isOk(_aidl_status.get())) break; + + break; + } + case (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/): { int32_t in_deviceId; int64_t in_timeSec; int64_t in_timeUsec; @@ -67,6 +77,40 @@ static AIBinder_Class* _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallba BpEvdevCallback::BpEvdevCallback(const ::ndk::SpAIBinder& binder) : BpCInterface(binder) {} BpEvdevCallback::~BpEvdevCallback() {} + ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 0 /*onEvdevEventLoopStarted*/), + _aidl_in.getR(), + _aidl_out.getR(), + 0 +#ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL +#endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEventLoopStarted(); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; + } ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) { binder_status_t _aidl_ret_status = STATUS_OK; ::ndk::ScopedAStatus _aidl_status; @@ -99,7 +143,7 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t _aidl_ret_status = AIBinder_transact( asBinder().get(), - (FIRST_CALL_TRANSACTION + 0 /*onEvdevEvent*/), + (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/), _aidl_in.getR(), _aidl_out.getR(), 0 @@ -181,6 +225,12 @@ const std::shared_ptr& IEvdevCallback::getDefaultImpl() { return IEvdevCallback::default_impl; } std::shared_ptr IEvdevCallback::default_impl = nullptr; + + ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEventLoopStarted() { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; + } ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_deviceId*/, int64_t /*in_timeSec*/, int64_t /*in_timeUsec*/, int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/, int32_t /*in_androidCode*/) { ::ndk::ScopedAStatus _aidl_status; _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index b93b675b85..1abc3034ee 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -29,19 +29,23 @@ class IEvdevCallback : public ::ndk::ICInterface { IEvdevCallback(); virtual ~IEvdevCallback(); - static constexpr uint32_t TRANSACTION_onEvdevEvent = FIRST_CALL_TRANSACTION + 0; + static constexpr uint32_t TRANSACTION_onEvdevEventLoopStarted = FIRST_CALL_TRANSACTION + 0; + static constexpr uint32_t TRANSACTION_onEvdevEvent = FIRST_CALL_TRANSACTION + 1; static std::shared_ptr fromBinder(const ::ndk::SpAIBinder& binder); static binder_status_t writeToParcel(AParcel* parcel, const std::shared_ptr& instance); static binder_status_t readFromParcel(const AParcel* parcel, std::shared_ptr* instance); static bool setDefaultImpl(const std::shared_ptr& impl); static const std::shared_ptr& getDefaultImpl(); + + virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; virtual ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) = 0; private: static std::shared_ptr default_impl; }; class IEvdevCallbackDefault : public IEvdevCallback { public: + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 537760bb32..5252659d56 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -335,6 +335,10 @@ epollFd = epoll_create1(EPOLL_CLOEXEC); LOGI("Start evdev event loop"); +callback-> + +onEvdevEventLoopStarted(); + while (running) { int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 6c509a97c4..7cc42bef48 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -244,6 +244,7 @@ internal class SystemBridge : ISystemBridge.Stub() { synchronized(evdevCallbackLock) { evdevCallback?.asBinder()?.unlinkToDeath(evdevCallbackDeathRecipient, 0) evdevCallback = null + stopEvdevEventLoop() } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index 0104c51756..40faf6321b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -156,7 +156,7 @@ class AndroidDevicesAdapter @Inject constructor( .values .joinToString() - Timber.i("Input device: $id ${device.name} Vendor=${device.vendorId} Product=${device.productId} Sources=$supportedSources") + Timber.d("Input device: $id ${device.name} Vendor=${device.vendorId} Product=${device.productId} Descriptor=${device.descriptor} Sources=$supportedSources") devices.add(InputDeviceUtils.createInputDeviceInfo(device)) } From 57682ad8d18d5e165e8099ef13c45d3cb03bff35 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 00:38:22 +0100 Subject: [PATCH 070/215] #1394 key map detection with evdev works --- .../base/keymaps/detection/KeyMapAlgorithm.kt | 442 +++++++++--------- .../keymapper/base/trigger/EvdevTriggerKey.kt | 6 +- .../base/trigger/KeyCodeTriggerKey.kt | 13 + .../base/trigger/KeyEventTriggerKey.kt | 6 +- .../base/keymaps/KeyMapAlgorithmTest.kt | 189 +++++++- .../keymapper/sysbridge/BnEvdevCallback.h | 6 +- .../keymapper/sysbridge/BpEvdevCallback.h | 2 +- .../keymapper/sysbridge/IEvdevCallback.cpp | 97 ++-- .../keymapper/sysbridge/IEvdevCallback.h | 9 +- 9 files changed, 478 insertions(+), 292 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index 249a0891de..353db4c25d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -18,6 +18,7 @@ import io.github.sds100.keymapper.base.trigger.AssistantTriggerType import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey +import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger @@ -64,6 +65,191 @@ class KeyMapAlgorithm( trigger.mode is TriggerMode.Parallel } + private var detectKeyMaps: Boolean = false + private var detectInternalEvents: Boolean = false + private var detectExternalEvents: Boolean = false + private var detectSequenceLongPresses: Boolean = false + private var detectSequenceDoublePresses: Boolean = false + + /** + * All sequence events that have the long press click type. + */ + private var longPressSequenceTriggerKeys: Array = arrayOf() + + /** + * All double press keys and the index of their corresponding trigger. first is the event and second is + * the trigger index. + */ + private var doublePressTriggerKeys: Array = arrayOf() + + /** + * order matches with [doublePressTriggerKeys] + */ + private var doublePressEventStates: IntArray = intArrayOf() + + /** + * The user has an amount of time to double press a key before it is registered as a double press. + * The order matches with [doublePressTriggerKeys]. This array stores the time when the corresponding trigger will + * timeout. If the key isn't waiting to timeout, the value is -1. + */ + private var doublePressTimeoutTimes = longArrayOf() + + private var actionMap: SparseArrayCompat = SparseArrayCompat() + var triggers: Array = emptyArray() + private set + + /** + * The events to detect for each sequence trigger. + */ + private var sequenceTriggers: IntArray = intArrayOf() + + /** + * Sequence triggers timeout after the first key has been pressed. + * This map stores the time when the corresponding trigger will timeout. If the trigger in + * isn't waiting to timeout, the value is -1. + * The index of a trigger matches with the index in [triggers] + */ + private var sequenceTriggersTimeoutTimes: MutableMap = mutableMapOf() + + /** + * The indexes of triggers that overlap after the first element with each trigger in [sequenceTriggers] + */ + private var sequenceTriggersOverlappingSequenceTriggers: Array = arrayOf() + + private var sequenceTriggersOverlappingParallelTriggers: Array = arrayOf() + + /** + * An array of the index of the last matched event in each trigger. + */ + private var lastMatchedEventIndices: IntArray = intArrayOf() + + /** + * An array of the constraints for every trigger + */ + private var triggerConstraints: Array> = arrayOf() + + /** + * The events to detect for each parallel trigger. + */ + private var parallelTriggers: IntArray = intArrayOf() + + /** + * The actions to perform when each trigger is detected. The order matches with + * [triggers]. + */ + private var triggerActions: Array = arrayOf() + + /** + * Stores whether each event in each parallel trigger need to be released after being held down. + * The index of a trigger matches with the index in [triggers] + */ + private var parallelTriggerEventsAwaitingRelease: Array = emptyArray() + + /** + * Whether each parallel trigger is awaiting to be released after performing an action. + * This is only set to true if the trigger has been successfully triggered and *all* the keys + * have not been released. + * The index of a trigger matches with the index in [triggers] + */ + private var parallelTriggersAwaitingReleaseAfterBeingTriggered: BooleanArray = booleanArrayOf() + + private var parallelTriggerModifierKeyIndices: Array> = arrayOf() + + /** + * The indexes of triggers that overlap after the first element with each trigger in [parallelTriggers] + */ + private var parallelTriggersOverlappingParallelTriggers = arrayOf() + + private var modifierKeyEventActions: Boolean = false + private var notModifierKeyEventActions: Boolean = false + private var keyCodesToImitateUpAction: MutableSet = mutableSetOf() + private var metaStateFromActions: Int = 0 + private var metaStateFromKeyEvent: Int = 0 + + private val eventDownTimeMap: MutableMap = mutableMapOf() + + /** + * This solves issue #1386. This stores the jobs that will wait until the sequence trigger + * times out and check whether the overlapping sequence trigger was indeed triggered. + */ + private val performActionsAfterSequenceTriggerTimeout: MutableMap = mutableMapOf() + + /** + * The indexes of parallel triggers that didn't have their actions performed because there is a matching trigger but + * for a long-press. These actions should only be performed if the long-press fails, otherwise when the user + * holds down the trigger keys for the long-press trigger, actions from both triggers will be performed. + */ + private val performActionsOnFailedLongPress: MutableSet = mutableSetOf() + + /** + * The indexes of parallel triggers that didn't have their actions performed because there is a matching trigger but + * for a double-press. These actions should only be performed if the double-press fails, otherwise each time the user + * presses the keys for the double press, actions from both triggers will be performed. + */ + private val performActionsOnFailedDoublePress: MutableSet = mutableSetOf() + + /** + * Maps jobs to perform an action after a long press to their corresponding parallel trigger index + */ + private val parallelTriggerLongPressJobs: SparseArrayCompat = SparseArrayCompat() + + /** + * Keys that are detected through an input method will potentially send multiple DOWN key events + * with incremented repeatCounts, such as DPAD buttons. These repeated DOWN key events must + * all be consumed and ignored because the UP key event is only sent once at the end. The action + * must not be executed for each repeat. The user may potentially have many hundreds + * of trigger keys so to reduce latency this set caches which keys + * will be affected by this behavior. + * + * NOTE: This only contains the trigger keys that are flagged to consume the key event. + */ + private var triggerKeysThatSendRepeatedKeyEvents: Set = emptySet() + + private var parallelTriggerActionPerformers: Map = + emptyMap() + private var sequenceTriggerActionPerformers: Map = + emptyMap() + + private val currentTime: Long + get() = useCase.currentTime + + private val defaultVibrateDuration: StateFlow = + useCase.defaultVibrateDuration.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.VIBRATION_DURATION.toLong(), + ) + + private val defaultSequenceTriggerTimeout: StateFlow = + useCase.defaultSequenceTriggerTimeout.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT.toLong(), + ) + + private val defaultLongPressDelay: StateFlow = + useCase.defaultLongPressDelay.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.LONG_PRESS_DELAY.toLong(), + ) + + private val defaultDoublePressDelay: StateFlow = + useCase.defaultDoublePressDelay.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.DOUBLE_PRESS_DELAY.toLong(), + ) + + private val forceVibrate: StateFlow = + useCase.forceVibrate.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.FORCE_VIBRATE, + ) + + private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() + fun loadKeyMaps(value: List) { actionMap.clear() @@ -83,7 +269,7 @@ class KeyMapAlgorithm( } else { detectKeyMaps = true - val longPressSequenceTriggerKeys = mutableListOf() + val longPressSequenceTriggerKeys = mutableListOf() val doublePressKeys = mutableListOf() @@ -118,7 +304,7 @@ class KeyMapAlgorithm( if (keyMap.trigger.mode == TriggerMode.Sequence && key.clickType == ClickType.LONG_PRESS && - key is KeyEventTriggerKey + key is KeyCodeTriggerKey ) { if (keyMap.trigger.keys.size > 1) { longPressSequenceTriggerKeys.add(key) @@ -147,6 +333,11 @@ class KeyMapAlgorithm( } } + is EvdevTriggerKey -> { + detectInternalEvents = true + detectExternalEvents = true + } + else -> {} } } @@ -326,8 +517,8 @@ class KeyMapAlgorithm( for (triggerIndex in parallelTriggers) { val trigger = triggers[triggerIndex] - trigger.keys.forEachIndexed { keyIndex, key -> - if (key is KeyEventTriggerKey && isModifierKey(key.keyCode)) { + for ((keyIndex, key) in trigger.keys.withIndex()) { + if (key is KeyCodeTriggerKey && isModifierKey(key.keyCode)) { parallelTriggerModifierKeyIndices.add(triggerIndex to keyIndex) } } @@ -373,191 +564,6 @@ class KeyMapAlgorithm( } } - private var detectKeyMaps: Boolean = false - private var detectInternalEvents: Boolean = false - private var detectExternalEvents: Boolean = false - private var detectSequenceLongPresses: Boolean = false - private var detectSequenceDoublePresses: Boolean = false - - /** - * All sequence events that have the long press click type. - */ - private var longPressSequenceTriggerKeys: Array = arrayOf() - - /** - * All double press keys and the index of their corresponding trigger. first is the event and second is - * the trigger index. - */ - private var doublePressTriggerKeys: Array = arrayOf() - - /** - * order matches with [doublePressTriggerKeys] - */ - private var doublePressEventStates: IntArray = intArrayOf() - - /** - * The user has an amount of time to double press a key before it is registered as a double press. - * The order matches with [doublePressTriggerKeys]. This array stores the time when the corresponding trigger will - * timeout. If the key isn't waiting to timeout, the value is -1. - */ - private var doublePressTimeoutTimes = longArrayOf() - - private var actionMap: SparseArrayCompat = SparseArrayCompat() - var triggers: Array = emptyArray() - private set - - /** - * The events to detect for each sequence trigger. - */ - private var sequenceTriggers: IntArray = intArrayOf() - - /** - * Sequence triggers timeout after the first key has been pressed. - * This map stores the time when the corresponding trigger will timeout. If the trigger in - * isn't waiting to timeout, the value is -1. - * The index of a trigger matches with the index in [triggers] - */ - private var sequenceTriggersTimeoutTimes: MutableMap = mutableMapOf() - - /** - * The indexes of triggers that overlap after the first element with each trigger in [sequenceTriggers] - */ - private var sequenceTriggersOverlappingSequenceTriggers: Array = arrayOf() - - private var sequenceTriggersOverlappingParallelTriggers: Array = arrayOf() - - /** - * An array of the index of the last matched event in each trigger. - */ - private var lastMatchedEventIndices: IntArray = intArrayOf() - - /** - * An array of the constraints for every trigger - */ - private var triggerConstraints: Array> = arrayOf() - - /** - * The events to detect for each parallel trigger. - */ - private var parallelTriggers: IntArray = intArrayOf() - - /** - * The actions to perform when each trigger is detected. The order matches with - * [triggers]. - */ - private var triggerActions: Array = arrayOf() - - /** - * Stores whether each event in each parallel trigger need to be released after being held down. - * The index of a trigger matches with the index in [triggers] - */ - private var parallelTriggerEventsAwaitingRelease: Array = emptyArray() - - /** - * Whether each parallel trigger is awaiting to be released after performing an action. - * This is only set to true if the trigger has been successfully triggered and *all* the keys - * have not been released. - * The index of a trigger matches with the index in [triggers] - */ - private var parallelTriggersAwaitingReleaseAfterBeingTriggered: BooleanArray = booleanArrayOf() - - private var parallelTriggerModifierKeyIndices: Array> = arrayOf() - - /** - * The indexes of triggers that overlap after the first element with each trigger in [parallelTriggers] - */ - private var parallelTriggersOverlappingParallelTriggers = arrayOf() - - private var modifierKeyEventActions: Boolean = false - private var notModifierKeyEventActions: Boolean = false - private var keyCodesToImitateUpAction: MutableSet = mutableSetOf() - private var metaStateFromActions: Int = 0 - private var metaStateFromKeyEvent: Int = 0 - - private val eventDownTimeMap: MutableMap = mutableMapOf() - - /** - * This solves issue #1386. This stores the jobs that will wait until the sequence trigger - * times out and check whether the overlapping sequence trigger was indeed triggered. - */ - private val performActionsAfterSequenceTriggerTimeout: MutableMap = mutableMapOf() - - /** - * The indexes of parallel triggers that didn't have their actions performed because there is a matching trigger but - * for a long-press. These actions should only be performed if the long-press fails, otherwise when the user - * holds down the trigger keys for the long-press trigger, actions from both triggers will be performed. - */ - private val performActionsOnFailedLongPress: MutableSet = mutableSetOf() - - /** - * The indexes of parallel triggers that didn't have their actions performed because there is a matching trigger but - * for a double-press. These actions should only be performed if the double-press fails, otherwise each time the user - * presses the keys for the double press, actions from both triggers will be performed. - */ - private val performActionsOnFailedDoublePress: MutableSet = mutableSetOf() - - /** - * Maps jobs to perform an action after a long press to their corresponding parallel trigger index - */ - private val parallelTriggerLongPressJobs: SparseArrayCompat = SparseArrayCompat() - - /** - * Keys that are detected through an input method will potentially send multiple DOWN key events - * with incremented repeatCounts, such as DPAD buttons. These repeated DOWN key events must - * all be consumed and ignored because the UP key event is only sent once at the end. The action - * must not be executed for each repeat. The user may potentially have many hundreds - * of trigger keys so to reduce latency this set caches which keys - * will be affected by this behavior. - * - * NOTE: This only contains the trigger keys that are flagged to consume the key event. - */ - private var triggerKeysThatSendRepeatedKeyEvents: Set = emptySet() - - private var parallelTriggerActionPerformers: Map = - emptyMap() - private var sequenceTriggerActionPerformers: Map = - emptyMap() - - private val currentTime: Long - get() = useCase.currentTime - - private val defaultVibrateDuration: StateFlow = - useCase.defaultVibrateDuration.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.VIBRATION_DURATION.toLong(), - ) - - private val defaultSequenceTriggerTimeout: StateFlow = - useCase.defaultSequenceTriggerTimeout.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT.toLong(), - ) - - private val defaultLongPressDelay: StateFlow = - useCase.defaultLongPressDelay.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.LONG_PRESS_DELAY.toLong(), - ) - - private val defaultDoublePressDelay: StateFlow = - useCase.defaultDoublePressDelay.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.DOUBLE_PRESS_DELAY.toLong(), - ) - - private val forceVibrate: StateFlow = - useCase.forceVibrate.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.FORCE_VIBRATE, - ) - - private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() - fun onMotionEvent(event: KMGamePadEvent): Boolean { if (!detectKeyMaps) return false @@ -606,7 +612,7 @@ class KeyMapAlgorithm( for ((triggerIndex, eventIndex) in parallelTriggerModifierKeyIndices) { val key = triggers[triggerIndex].keys[eventIndex] - if (key !is KeyEventTriggerKey) { + if (key !is KeyCodeTriggerKey) { continue } @@ -618,27 +624,16 @@ class KeyMapAlgorithm( val device = keyEvent.device - val event = if (device.isExternal) { - KeyCodeEvent( - keyCode = keyEvent.keyCode, - clickType = null, - descriptor = device.descriptor, - deviceId = device.id, - scanCode = keyEvent.scanCode, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source, - ) - } else { - KeyCodeEvent( - keyCode = keyEvent.keyCode, - clickType = null, - descriptor = null, - deviceId = device?.id ?: 0, - scanCode = keyEvent.scanCode, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source, - ) - } + val event = KeyCodeEvent( + keyCode = keyEvent.keyCode, + clickType = null, + descriptor = device.descriptor, + deviceId = device.id, + scanCode = keyEvent.scanCode, + repeatCount = keyEvent.repeatCount, + source = keyEvent.source, + isExternal = device.isExternal + ) when (keyEvent.action) { KeyEvent.ACTION_DOWN -> return onKeyDown(event) @@ -721,7 +716,7 @@ class KeyMapAlgorithm( consumeEvent = true } - key is KeyEventTriggerKey && event is KeyCodeEvent -> + key is KeyCodeTriggerKey && event is KeyCodeEvent -> if (key.keyCode == event.keyCode && key.consumeEvent) { consumeEvent = true } @@ -1140,7 +1135,12 @@ class KeyMapAlgorithm( if ((currentTime - downTime) >= longPressDelay(trigger)) { successfulLongPressTrigger = true } else if (detectSequenceLongPresses && - longPressSequenceTriggerKeys.any { it.matchesEvent(event.withLongPress) } + longPressSequenceTriggerKeys.any { key -> + when (key) { + is EvdevTriggerKey -> key.matchesEvent(event.withLongPress) + is KeyEventTriggerKey -> key.matchesEvent(event.withLongPress) + } + } ) { imitateDownUpKeyEvent = true } @@ -1654,18 +1654,15 @@ class KeyMapAlgorithm( return when (this.device) { KeyEventTriggerDevice.Any -> this.keyCode == event.keyCode && this.clickType == event.clickType is KeyEventTriggerDevice.External -> - this.keyCode == event.keyCode && - event.descriptor != null && - event.descriptor == this.device.descriptor && - this.clickType == event.clickType + event.isExternal && this.keyCode == event.keyCode && event.descriptor == this.device.descriptor && this.clickType == event.clickType KeyEventTriggerDevice.Internal -> - this.keyCode == event.keyCode && - event.descriptor == null && + !event.isExternal && + this.keyCode == event.keyCode && this.clickType == event.clickType } } else if (this is EvdevTriggerKey && event is KeyCodeEvent) { - return this.keyCode == event.keyCode && event.clickType == this.clickType && event.descriptor == this.deviceDescriptor + return this.keyCode == event.keyCode && this.clickType == event.clickType && this.deviceDescriptor == event.descriptor } else if (this is AssistantTriggerKey && event is AssistantEvent) { return if (this.type == AssistantTriggerType.ANY || event.type == AssistantTriggerType.ANY) { this.clickType == event.clickType @@ -1698,6 +1695,8 @@ class KeyMapAlgorithm( otherKey.device == KeyEventTriggerDevice.Internal && this.clickType == otherKey.clickType } + } else if (this is EvdevTriggerKey && otherKey is EvdevTriggerKey) { + return this.keyCode == otherKey.keyCode && this.clickType == otherKey.clickType && this.deviceDescriptor == otherKey.deviceDescriptor } else if (this is AssistantTriggerKey && otherKey is AssistantTriggerKey) { return this.type == otherKey.type && this.clickType == otherKey.clickType } else if (this is FloatingButtonKey && otherKey is FloatingButtonKey) { @@ -1778,8 +1777,9 @@ class KeyMapAlgorithm( private data class KeyCodeEvent( val keyCode: Int, override val clickType: ClickType?, - val descriptor: String?, + val descriptor: String, val deviceId: Int, + val isExternal: Boolean, val scanCode: Int, val repeatCount: Int, val source: Int, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt index 1fdd315d53..a60905491f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt @@ -14,13 +14,13 @@ import java.util.UUID */ data class EvdevTriggerKey( override val uid: String = UUID.randomUUID().toString(), - val keyCode: Int, - val scanCode: Int, + override val keyCode: Int, + override val scanCode: Int, val deviceDescriptor: String, val deviceName: String, override val clickType: ClickType = ClickType.SHORT_PRESS, override val consumeEvent: Boolean = true, -) : TriggerKey() { +) : TriggerKey(), KeyCodeTriggerKey { override val allowedDoublePress: Boolean = true override val allowedLongPress: Boolean = true diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt new file mode 100644 index 0000000000..5e12f7616d --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt @@ -0,0 +1,13 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.keymaps.ClickType + +/** + * This is a type for trigger keys that are detected by key code. This is a different meaning to + * key *event*. + */ +sealed interface KeyCodeTriggerKey { + val keyCode: Int + val scanCode: Int? + val clickType: ClickType +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt index 6518845303..1d4fb07589 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -11,7 +11,7 @@ import java.util.UUID @Serializable data class KeyEventTriggerKey( override val uid: String = UUID.randomUUID().toString(), - val keyCode: Int, + override val keyCode: Int, val device: KeyEventTriggerDevice, override val clickType: ClickType, override val consumeEvent: Boolean = true, @@ -20,8 +20,8 @@ data class KeyEventTriggerKey( * do not send key events to the accessibility service. */ val requiresIme: Boolean = false, - val scanCode: Int? = null, -) : TriggerKey() { + override val scanCode: Int? = null, +) : TriggerKey(), KeyCodeTriggerKey { override val allowedLongPress: Boolean = true override val allowedDoublePress: Boolean = true diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 663e93acaf..6770b2dc88 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -17,7 +17,10 @@ import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.detection.KeyMapAlgorithm import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey +import io.github.sds100.keymapper.base.trigger.FloatingButtonKey import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger @@ -49,6 +52,7 @@ import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` +import org.junit.Assert.fail import org.junit.Before import org.junit.Rule import org.junit.Test @@ -203,6 +207,138 @@ class KeyMapAlgorithmTest { ) } + @Test + fun `Sequence trigger with multiple evdev keys is triggered`() = + runTest(testDispatcher) { + val trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = 900, + deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, + deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_B, + scanCode = 901, + deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, + deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_DOWN, FAKE_CONTROLLER_INPUT_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP, FAKE_CONTROLLER_INPUT_DEVICE) + + inputKeyEvent(KeyEvent.KEYCODE_B, KeyEvent.ACTION_DOWN, FAKE_CONTROLLER_INPUT_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_B, KeyEvent.ACTION_UP, FAKE_CONTROLLER_INPUT_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Parallel trigger with multiple evdev keys is triggered`() = + runTest(testDispatcher) { + val trigger = parallelTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = 900, + deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, + deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_B, + scanCode = 901, + deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, + deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_DOWN, FAKE_CONTROLLER_INPUT_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_B, KeyEvent.ACTION_DOWN, FAKE_CONTROLLER_INPUT_DEVICE) + + inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP, FAKE_CONTROLLER_INPUT_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_B, KeyEvent.ACTION_UP, FAKE_CONTROLLER_INPUT_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Evdev trigger is not triggered from events from other internal devices`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = 900, + deviceDescriptor = "gpio_keys", + deviceName = "GPIO", + ) + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + mockEvdevKeyInput(trigger.keys[0], FAKE_INTERNAL_DEVICE) + + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + } + + @Test + fun `Evdev trigger is not triggered from events from other external devices`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = 900, + deviceDescriptor = FAKE_KEYBOARD_TRIGGER_KEY_DEVICE.descriptor, + deviceName = FAKE_KEYBOARD_TRIGGER_KEY_DEVICE.name, + ) + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_INPUT_DEVICE) + + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + } + + @Test + fun `Short press trigger evdev trigger from external device`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = 900, + deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, + deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + ) + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_INPUT_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Short press trigger evdev trigger from internal device`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = 900, + deviceDescriptor = FAKE_INTERNAL_DEVICE.descriptor, + deviceName = FAKE_INTERNAL_DEVICE.name, + ) + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + mockEvdevKeyInput(trigger.keys[0], FAKE_INTERNAL_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + @Test fun `Do not perform if one group constraint set is not satisfied`() = runTest(testDispatcher) { val trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)) @@ -4129,33 +4265,72 @@ class KeyMapAlgorithmTest { return } - val deviceDescriptor = triggerKeyDeviceToInputDevice(key.device) + val inputDevice = triggerKeyDeviceToInputDevice(key.device) + val pressDuration: Long = delay ?: when (key.clickType) { + ClickType.LONG_PRESS -> LONG_PRESS_DELAY + 100L + else -> 50L + } + + inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) + + when (key.clickType) { + ClickType.SHORT_PRESS -> { + delay(pressDuration) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + } + + ClickType.LONG_PRESS -> { + delay(pressDuration) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + } + + ClickType.DOUBLE_PRESS -> { + delay(pressDuration) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + delay(pressDuration) + + inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) + delay(pressDuration) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + } + } + } + + private suspend fun mockEvdevKeyInput( + key: TriggerKey, + inputDevice: InputDeviceInfo, + delay: Long? = null, + ) { + if (key !is EvdevTriggerKey) { + return + } + val pressDuration: Long = delay ?: when (key.clickType) { ClickType.LONG_PRESS -> LONG_PRESS_DELAY + 100L else -> 50L } - inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) when (key.clickType) { ClickType.SHORT_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) } ClickType.LONG_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) } ClickType.DOUBLE_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) } } } diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index e22fdf87a4..1271154377 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -34,9 +34,9 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { explicit IEvdevCallbackDelegator(const std::shared_ptr &impl) : _impl(impl) { } - ::ndk::ScopedAStatus onEvdevEventLoopStarted() override { - return _impl->onEvdevEventLoopStarted(); - } + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override { + return _impl->onEvdevEventLoopStarted(); + } ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override { return _impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode); } diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index c3ca3004de..c52a90dd9f 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -19,7 +19,7 @@ class BpEvdevCallback : public ::ndk::BpCInterface { explicit BpEvdevCallback(const ::ndk::SpAIBinder& binder); virtual ~BpEvdevCallback(); - ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override; }; } // namespace sysbridge diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index 818c81a077..fe0c2b0f76 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -20,17 +20,17 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback binder_status_t _aidl_ret_status = STATUS_UNKNOWN_TRANSACTION; std::shared_ptr _aidl_impl = std::static_pointer_cast(::ndk::ICInterface::asInterface(_aidl_binder)); switch (_aidl_code) { - case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEventLoopStarted*/): { + case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEventLoopStarted*/): { - ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEventLoopStarted(); - _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); - if (_aidl_ret_status != STATUS_OK) break; + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEventLoopStarted(); + _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); + if (_aidl_ret_status != STATUS_OK) break; - if (!AStatus_isOk(_aidl_status.get())) break; + if (!AStatus_isOk(_aidl_status.get())) break; - break; - } - case (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/): { + break; + } + case (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/): { int32_t in_deviceId; int64_t in_timeSec; int64_t in_timeUsec; @@ -77,40 +77,40 @@ static AIBinder_Class* _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallba BpEvdevCallback::BpEvdevCallback(const ::ndk::SpAIBinder& binder) : BpCInterface(binder) {} BpEvdevCallback::~BpEvdevCallback() {} - ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { - binder_status_t _aidl_ret_status = STATUS_OK; - ::ndk::ScopedAStatus _aidl_status; - ::ndk::ScopedAParcel _aidl_in; - ::ndk::ScopedAParcel _aidl_out; - - _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = AIBinder_transact( - asBinder().get(), - (FIRST_CALL_TRANSACTION + 0 /*onEvdevEventLoopStarted*/), - _aidl_in.getR(), - _aidl_out.getR(), - 0 -#ifdef BINDER_STABILITY_SUPPORT - | FLAG_PRIVATE_LOCAL -#endif // BINDER_STABILITY_SUPPORT - ); - if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { - _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEventLoopStarted(); - goto _aidl_status_return; - } - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; - _aidl_error: - _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); - _aidl_status_return: - return _aidl_status; - } +::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 0 /*onEvdevEventLoopStarted*/), + _aidl_in.getR(), + _aidl_out.getR(), + 0 + #ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL + #endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEventLoopStarted(); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; +} ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) { binder_status_t _aidl_ret_status = STATUS_OK; ::ndk::ScopedAStatus _aidl_status; @@ -143,7 +143,7 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t _aidl_ret_status = AIBinder_transact( asBinder().get(), - (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/), + (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/), _aidl_in.getR(), _aidl_out.getR(), 0 @@ -225,12 +225,11 @@ const std::shared_ptr& IEvdevCallback::getDefaultImpl() { return IEvdevCallback::default_impl; } std::shared_ptr IEvdevCallback::default_impl = nullptr; - - ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEventLoopStarted() { - ::ndk::ScopedAStatus _aidl_status; - _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); - return _aidl_status; - } +::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEventLoopStarted() { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; +} ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_deviceId*/, int64_t /*in_timeSec*/, int64_t /*in_timeUsec*/, int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/, int32_t /*in_androidCode*/) { ::ndk::ScopedAStatus _aidl_status; _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index 1abc3034ee..c231b06037 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -29,23 +29,22 @@ class IEvdevCallback : public ::ndk::ICInterface { IEvdevCallback(); virtual ~IEvdevCallback(); - static constexpr uint32_t TRANSACTION_onEvdevEventLoopStarted = FIRST_CALL_TRANSACTION + 0; - static constexpr uint32_t TRANSACTION_onEvdevEvent = FIRST_CALL_TRANSACTION + 1; + static constexpr uint32_t TRANSACTION_onEvdevEventLoopStarted = FIRST_CALL_TRANSACTION + 0; + static constexpr uint32_t TRANSACTION_onEvdevEvent = FIRST_CALL_TRANSACTION + 1; static std::shared_ptr fromBinder(const ::ndk::SpAIBinder& binder); static binder_status_t writeToParcel(AParcel* parcel, const std::shared_ptr& instance); static binder_status_t readFromParcel(const AParcel* parcel, std::shared_ptr* instance); static bool setDefaultImpl(const std::shared_ptr& impl); static const std::shared_ptr& getDefaultImpl(); - - virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; + virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; virtual ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) = 0; private: static std::shared_ptr default_impl; }; class IEvdevCallbackDefault : public IEvdevCallback { public: - ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; From 6e707901bbb82b3c045275f928265f107de213df Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 02:19:16 +0100 Subject: [PATCH 071/215] #1394 evdev key maps work and passthrough keys if they aren't remapped --- CHANGELOG.md | 9 + .../AccessibilityServiceController.kt | 14 +- .../keymapper/base/BaseSingletonHiltModule.kt | 6 + .../keymapper/base/actions/ActionData.kt | 1 + .../base/actions/PerformActionsUseCase.kt | 86 ++-- .../base/input/InjectKeyEventModel.kt | 29 ++ .../keymapper/base/input/InputEventHub.kt | 76 ++-- .../base/keymaps/SimpleMappingController.kt | 20 +- .../keymaps/detection/DetectKeyMapsUseCase.kt | 47 +- .../base/keymaps/detection/KeyMapAlgorithm.kt | 51 ++- .../detection/KeyMapDetectionController.kt | 22 +- .../ParallelTriggerActionPerformer.kt | 26 +- .../SequenceTriggerActionPerformer.kt | 4 +- .../RerouteKeyEventsController.kt | 72 ++- .../RerouteKeyEventsUseCase.kt | 37 +- .../accessibility/BaseAccessibilityService.kt | 14 +- .../BaseAccessibilityServiceController.kt | 14 +- .../accessibility/IAccessibilityService.kt | 8 +- .../inputmethod/ImeInputEventInjector.kt | 128 +----- .../base/system/navigation/OpenMenuHelper.kt | 40 +- .../base/trigger/RecordTriggerController.kt | 3 +- .../base/actions/PerformActionsUseCaseTest.kt | 412 +++++++++--------- .../base/keymaps/KeyMapAlgorithmTest.kt | 103 +++-- ...{InputEventType.kt => InputEventAction.kt} | 2 +- .../src/main/cpp/android/input/InputDevice.h | 8 +- .../main/cpp/android/input/KeyLayoutMap.cpp | 4 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 323 ++++++++------ .../system/inputevents/InputEventInjector.kt | 30 -- .../system/inputevents/KMKeyEvent.kt | 6 +- .../system/inputmethod/InputKeyModel.kt | 15 - .../system/permissions/Permission.kt | 2 + .../shizuku/ShizukuInputEventInjector.kt | 58 --- 32 files changed, 795 insertions(+), 875 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt rename common/src/main/java/io/github/sds100/keymapper/common/utils/{InputEventType.kt => InputEventAction.kt} (73%) delete mode 100644 system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt delete mode 100644 system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt delete mode 100644 system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c5c144dd..d69afd0a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) + +#### TO BE RELEASED + +## Removed + +- The key event relay service is now also used on all Android versions below Android 14. The + broadcast receiver method is no longer used. + ## [3.2.0](https://github.com/sds100/KeyMapper/releases/tag/v3.2.0) #### TO BE RELEASED diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 8832c80ada..9a0fb138fc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -12,11 +12,10 @@ import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImp import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeRecorder import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController +import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController -import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper -import io.github.sds100.keymapper.system.root.SuAdapter class AccessibilityServiceController @AssistedInject constructor( @Assisted @@ -28,12 +27,11 @@ class AccessibilityServiceController @AssistedInject constructor( detectConstraintsUseCaseFactory: DetectConstraintsUseCaseImpl.Factory, fingerprintGesturesSupported: FingerprintGesturesSupportedUseCase, pauseKeyMapsUseCase: PauseKeyMapsUseCase, - devicesAdapter: DevicesAdapter, - suAdapter: SuAdapter, settingsRepository: PreferenceRepository, systemBridgeSetupController: SystemBridgeSetupController, keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, - inputEventHub: InputEventHub + inputEventHub: InputEventHub, + recordTriggerController: RecordTriggerController ) : BaseAccessibilityServiceController( service = service, rerouteKeyEventsControllerFactory = rerouteKeyEventsControllerFactory, @@ -43,13 +41,11 @@ class AccessibilityServiceController @AssistedInject constructor( detectConstraintsUseCaseFactory = detectConstraintsUseCaseFactory, fingerprintGesturesSupported = fingerprintGesturesSupported, pauseKeyMapsUseCase = pauseKeyMapsUseCase, - devicesAdapter = devicesAdapter, - suAdapter = suAdapter, settingsRepository = settingsRepository, systemBridgeSetupController = systemBridgeSetupController, keyEventRelayServiceWrapper = keyEventRelayServiceWrapper, - inputEventHub = inputEventHub - + inputEventHub = inputEventHub, + recordTriggerController = recordTriggerController ) { @AssistedFactory interface Factory { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt index 76e14e299a..f3904c84b3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt @@ -24,6 +24,8 @@ import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCaseImpl +import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsUseCase +import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsUseCaseImpl import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCaseImpl @@ -152,4 +154,8 @@ abstract class BaseSingletonHiltModule { @Binds @Singleton abstract fun imeInputEvenInjector(impl: ImeInputEventInjectorImpl): ImeInputEventInjector + + @Binds + @Singleton + abstract fun rerouteKeyEventsUseCase(impl: RerouteKeyEventsUseCaseImpl): RerouteKeyEventsUseCase } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt index 3923f4d70b..514367a755 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt @@ -48,6 +48,7 @@ sealed class ActionData : Comparable { data class InputKeyEvent( val keyCode: Int, val metaState: Int = 0, + // TODO remove this option val useShell: Boolean = false, val device: Device? = null, ) : ActionData() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 367dd23464..3c486169af 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -10,6 +10,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.sound.SoundsManager +import io.github.sds100.keymapper.base.input.InjectKeyEventModel +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeAction import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeModel import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService @@ -17,7 +19,7 @@ import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.base.system.navigation.OpenMenuHelper import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Orientation @@ -45,7 +47,6 @@ import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.files.FileAdapter import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.intents.IntentAdapter import io.github.sds100.keymapper.system.intents.IntentTarget @@ -55,30 +56,22 @@ import io.github.sds100.keymapper.system.network.NetworkAdapter import io.github.sds100.keymapper.system.nfc.NfcAdapter import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapter import io.github.sds100.keymapper.system.notifications.NotificationServiceEvent -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.ToastAdapter 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.shizuku.ShizukuInputEventInjector import io.github.sds100.keymapper.system.url.OpenUrlAdapter import io.github.sds100.keymapper.system.volume.RingerMode import io.github.sds100.keymapper.system.volume.VolumeAdapter import io.github.sds100.keymapper.system.volume.VolumeStream -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import timber.log.Timber class PerformActionsUseCaseImpl @AssistedInject constructor( - private val appCoroutineScope: CoroutineScope, @Assisted private val service: IAccessibilityService, private val inputMethodAdapter: InputMethodAdapter, @@ -105,10 +98,10 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( private val openUrlAdapter: OpenUrlAdapter, private val resourceProvider: ResourceProvider, private val soundsManager: SoundsManager, - private val permissionAdapter: PermissionAdapter, private val notificationReceiverAdapter: NotificationReceiverAdapter, private val ringtoneAdapter: RingtoneAdapter, private val settingsRepository: PreferenceRepository, + private val inputEventHub: InputEventHub ) : PerformActionsUseCase { @AssistedFactory @@ -118,28 +111,16 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( ): PerformActionsUseCaseImpl } - private val shizukuInputEventInjector: ShizukuInputEventInjector = ShizukuInputEventInjector() - private val openMenuHelper by lazy { OpenMenuHelper( - suAdapter, service, - shizukuInputEventInjector, - permissionAdapter, - appCoroutineScope, + inputEventHub, ) } - /** - * Cache this so we aren't checking every time a key event must be inputted. - */ - private val inputKeyEventsWithShizuku: StateFlow = - permissionAdapter.isGrantedFlow(Permission.SHIZUKU) - .stateIn(appCoroutineScope, SharingStarted.Eagerly, false) - override suspend fun perform( action: ActionData, - inputEventType: InputEventType, + inputEventAction: InputEventAction, keyMetaState: Int, ) { /** @@ -170,27 +151,27 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( else -> InputDevice.SOURCE_KEYBOARD } - val model = InputKeyModel( + val firstInputAction = if (inputEventAction == InputEventAction.UP) { + KeyEvent.ACTION_UP + } else { + KeyEvent.ACTION_DOWN + } + + val model = InjectKeyEventModel( keyCode = action.keyCode, - inputType = inputEventType, + action = firstInputAction, metaState = keyMetaState.withFlag(action.metaState), deviceId = deviceId, source = source, + repeatCount = 0, + scanCode = 0 ) - result = when { - inputKeyEventsWithShizuku.value -> { - shizukuInputEventInjector.inputKeyEvent(model) - Success(Unit) - } - - action.useShell -> suAdapter.execute("input keyevent ${model.keyCode}") - - else -> { - keyMapperImeMessenger.inputKeyEvent(model) - - Success(Unit) - } + if (inputEventAction == InputEventAction.DOWN_UP) { + result = + injectKeyEvent(model).then { injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } + } else { + result = injectKeyEvent(model) } } @@ -333,7 +314,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.TapScreen -> { - result = service.tapScreen(action.x, action.y, inputEventType) + result = service.tapScreen(action.x, action.y, inputEventAction) } is ActionData.SwipeScreen -> { @@ -344,7 +325,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( action.yEnd, action.fingerCount, action.duration, - inputEventType, + inputEventAction, ) } @@ -356,7 +337,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( action.pinchType, action.fingerCount, action.duration, - inputEventType, + inputEventAction, ) } @@ -879,15 +860,28 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } when (result) { - is Success -> Timber.d("Performed action $action, input event type: $inputEventType, key meta state: $keyMetaState") + is Success -> Timber.d("Performed action $action, input event type: $inputEventAction, key meta state: $keyMetaState") is KMError -> Timber.d( - "Failed to perform action $action, reason: ${result.getFullMessage(resourceProvider)}, action: $action, input event type: $inputEventType, key meta state: $keyMetaState", + "Failed to perform action $action, reason: ${result.getFullMessage(resourceProvider)}, action: $action, input event type: $inputEventAction, key meta state: $keyMetaState", ) } result.showErrorMessageOnFail() } + private fun injectKeyEvent(model: InjectKeyEventModel): KMResult { + return when { + inputEventHub.isSystemBridgeConnected() -> { + return inputEventHub.injectKeyEvent(model) + } + + else -> { + keyMapperImeMessenger.inputKeyEvent(model) + Success(Unit) + } + } + } + override fun getErrorSnapshot(): ActionErrorSnapshot { return getActionErrorUseCase.actionErrorSnapshot.firstBlocking() } @@ -1020,7 +1014,7 @@ interface PerformActionsUseCase { suspend fun perform( action: ActionData, - inputEventType: InputEventType = InputEventType.DOWN_UP, + inputEventAction: InputEventAction = InputEventAction.DOWN_UP, keyMetaState: Int = 0, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt new file mode 100644 index 0000000000..59094965a9 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt @@ -0,0 +1,29 @@ +package io.github.sds100.keymapper.base.input + +import android.os.SystemClock +import android.view.KeyEvent + +data class InjectKeyEventModel( + val keyCode: Int, + val action: Int, + val metaState: Int, + val deviceId: Int, + val scanCode: Int, + val source: Int, + val repeatCount: Int = 0 +) { + fun toAndroidKeyEvent(): KeyEvent { + val eventTime = SystemClock.uptimeMillis() + return KeyEvent( + eventTime, + eventTime, + action, + keyCode, + repeatCount, + metaState, + deviceId, + scanCode, + source + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index d86a495f19..6835974153 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -5,7 +5,6 @@ import android.os.RemoteException import android.view.KeyEvent import io.github.sds100.keymapper.base.BuildConfig import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector -import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success @@ -24,7 +23,6 @@ import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -91,6 +89,10 @@ class InputEventHubImpl @Inject constructor( } } + override fun isSystemBridgeConnected(): Boolean { + return systemBridge != null + } + override fun onEvdevEventLoopStarted() { Timber.i("On evdev event loop started") invalidateGrabbedEvdevDevices() @@ -114,7 +116,25 @@ class InputEventHubImpl @Inject constructor( val keyEvent: KMKeyEvent? = evdevKeyEventTracker.toKeyEvent(evdevEvent) if (keyEvent != null) { - onInputEvent(keyEvent, InputEventDetectionSource.EVDEV) + val consumed = onInputEvent(keyEvent, InputEventDetectionSource.EVDEV) + + // Passthrough the key event if it is not consumed. + if (!consumed) { + if (logInputEventsEnabled.value) { + Timber.d("Passthrough key event from evdev: ${keyEvent.keyCode}") + } + injectKeyEvent( + InjectKeyEventModel( + keyCode = keyEvent.keyCode, + action = keyEvent.action, + metaState = keyEvent.metaState, + deviceId = keyEvent.deviceId, + scanCode = keyEvent.scanCode, + repeatCount = keyEvent.repeatCount, + source = keyEvent.source + ) + ) + } } else { onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) } @@ -244,22 +264,6 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedEvdevDevices() } - override suspend fun injectEvent(event: KMInputEvent): KMResult { - when (event) { - is KMGamePadEvent -> { - throw IllegalArgumentException("KMGamePadEvents can not be injected. Must use an evdev event instead.") - } - - is KMKeyEvent -> { - return injectKeyEvent(event) - } - - is KMEvdevEvent -> { - return injectEvdevEvent(event) - } - } - } - private fun invalidateGrabbedEvdevDevices() { val descriptors: Set = clients.values.flatMap { it.grabbedEvdevDevices }.toSet() @@ -284,7 +288,7 @@ class InputEventHubImpl @Inject constructor( } } - private fun injectEvdevEvent(event: KMEvdevEvent): KMResult { + override fun injectEvdevEvent(event: KMEvdevEvent): KMResult { val systemBridge = this.systemBridge if (systemBridge == null) { @@ -303,34 +307,16 @@ class InputEventHubImpl @Inject constructor( } } - private suspend fun injectKeyEvent(event: KMKeyEvent): KMResult { + override fun injectKeyEvent(event: InjectKeyEventModel): KMResult { val systemBridge = this.systemBridge if (systemBridge == null) { - // TODO InputKeyModel will be removed - val action = if (event.action == KeyEvent.ACTION_DOWN) { - InputEventType.DOWN - } else { - InputEventType.UP - } - - val model = InputKeyModel( - keyCode = event.keyCode, - inputType = action, - metaState = event.metaState, - deviceId = event.device?.id ?: -1, - scanCode = event.scanCode, - repeat = event.repeatCount, - source = event.source - ) - - imeInputEventInjector.inputKeyEvent(model) - + imeInputEventInjector.inputKeyEvent(event) return Success(true) } else { try { return systemBridge.injectInputEvent( - event.toKeyEvent(), + event.toAndroidKeyEvent(), INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH ).success() } catch (e: RemoteException) { @@ -368,10 +354,12 @@ interface InputEventHub { fun setGrabbedEvdevDevices(clientId: String, deviceDescriptors: List) /** - * Inject an input event. This may either use the key event relay service or the system + * Inject a key event. This may either use the key event relay service or the system * bridge depending on the permissions granted to Key Mapper. */ - suspend fun injectEvent(event: KMInputEvent): KMResult + fun injectKeyEvent(event: InjectKeyEventModel): KMResult + + fun injectEvdevEvent(event: KMEvdevEvent): KMResult /** * Send an input event to the connected clients. @@ -379,4 +367,6 @@ interface InputEventHub { * @return whether the input event was consumed by a client. */ fun onInputEvent(event: KMInputEvent, detectionSource: InputEventDetectionSource): Boolean + + fun isSystemBridgeConnected(): Boolean } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt index c0bb27f2ef..aa15212ed8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt @@ -6,7 +6,7 @@ import io.github.sds100.keymapper.base.actions.RepeatMode import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.constraints.isSatisfied import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.data.PreferenceDefaults import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -97,9 +97,9 @@ abstract class SimpleMappingController( val alreadyBeingHeldDown = actionsBeingHeldDown.any { action.uid == it.uid } val keyEventAction = when { - action.holdDown && !alreadyBeingHeldDown -> InputEventType.DOWN - alreadyBeingHeldDown -> InputEventType.UP - else -> InputEventType.DOWN_UP + action.holdDown && !alreadyBeingHeldDown -> InputEventAction.DOWN + alreadyBeingHeldDown -> InputEventAction.UP + else -> InputEventAction.DOWN_UP } when { @@ -129,10 +129,10 @@ abstract class SimpleMappingController( private suspend fun performAction( action: Action, - inputEventType: InputEventType = InputEventType.DOWN_UP, + inputEventAction: InputEventAction = InputEventAction.DOWN_UP, ) { repeat(action.multiplier ?: 1) { - performActionsUseCase.perform(action.data, inputEventType) + performActionsUseCase.perform(action.data, inputEventAction) } } @@ -149,15 +149,15 @@ abstract class SimpleMappingController( while (continueRepeating) { val keyEventAction = when { - holdDown -> InputEventType.DOWN - else -> InputEventType.DOWN_UP + holdDown -> InputEventAction.DOWN + else -> InputEventAction.DOWN_UP } performAction(action, keyEventAction) if (holdDown) { delay(holdDownDuration) - performAction(action, InputEventType.UP) + performAction(action, InputEventAction.UP) } repeatCount++ @@ -186,7 +186,7 @@ abstract class SimpleMappingController( coroutineScope.launch { for (it in actionsBeingHeldDown) { - performAction(it, InputEventType.UP) + performAction(it, InputEventAction.UP) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt index 2ce1bb6528..b7d26cb061 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt @@ -11,14 +11,14 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.groups.Group import io.github.sds100.keymapper.base.groups.GroupEntityMapper +import io.github.sds100.keymapper.base.input.InjectKeyEventModel +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.keymaps.KeyMapEntityMapper import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.base.system.navigation.OpenMenuHelper import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.data.Keys @@ -27,12 +27,9 @@ import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.popup.ToastAdapter import io.github.sds100.keymapper.system.root.SuAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuInputEventInjector import io.github.sds100.keymapper.system.vibrator.VibratorAdapter import io.github.sds100.keymapper.system.volume.VolumeAdapter import kotlinx.coroutines.CoroutineScope @@ -41,12 +38,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import timber.log.Timber class DetectKeyMapsUseCaseImpl @AssistedInject constructor( - private val imeInputEventInjector: ImeInputEventInjector, @Assisted private val accessibilityService: IAccessibilityService, private val keyMapRepository: KeyMapRepository, @@ -61,6 +55,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( private val vibrator: VibratorAdapter, @Assisted private val coroutineScope: CoroutineScope, + private val inputEventHub: InputEventHub ) : DetectKeyMapsUseCase { @AssistedFactory @@ -170,14 +165,9 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( override val currentTime: Long get() = SystemClock.elapsedRealtime() - private val shizukuInputEventInjector = ShizukuInputEventInjector() - private val openMenuHelper = OpenMenuHelper( - suAdapter, accessibilityService, - shizukuInputEventInjector, - permissionAdapter, - coroutineScope, + inputEventHub, ) override val forceVibrate: Flow = @@ -200,25 +190,22 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( keyCode: Int, metaState: Int, deviceId: Int, - inputEventType: InputEventType, + action: Int, scanCode: Int, source: Int, ) { - val model = InputKeyModel( - keyCode, - inputEventType, - metaState, - deviceId, - scanCode, + val model = InjectKeyEventModel( + keyCode = keyCode, + action = action, + metaState = metaState, + deviceId = deviceId, + scanCode = scanCode, source = source, ) - if (permissionAdapter.isGranted(Permission.SHIZUKU)) { - Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)} with Shizuku, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") - - coroutineScope.launch { - shizukuInputEventInjector.inputKeyEvent(model) - } + if (inputEventHub.isSystemBridgeConnected()) { + Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)} with system bridge, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") + inputEventHub.injectKeyEvent(model) } else { Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)}, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") @@ -235,9 +222,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( KeyEvent.KEYCODE_MENU -> openMenuHelper.openMenu() - else -> runBlocking { - imeInputEventInjector.inputKeyEvent(model) - } + else -> inputEventHub.injectKeyEvent(model) } } } @@ -265,7 +250,7 @@ interface DetectKeyMapsUseCase { keyCode: Int, metaState: Int = 0, deviceId: Int = 0, - inputEventType: InputEventType = InputEventType.DOWN_UP, + action: Int, scanCode: Int = 0, source: Int = InputDevice.SOURCE_UNKNOWN, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index 353db4c25d..6eac573abf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -24,7 +24,6 @@ import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey import io.github.sds100.keymapper.base.trigger.TriggerMode -import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.PreferenceDefaults @@ -917,7 +916,7 @@ class KeyMapAlgorithm( keyCode = event.keyCode, metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), deviceId = event.deviceId, - inputEventType = InputEventType.DOWN, + action = KeyEvent.ACTION_DOWN, scanCode = event.scanCode, source = event.source, ) @@ -1409,7 +1408,14 @@ class KeyMapAlgorithm( if (event is KeyCodeEvent) { useCase.imitateButtonPress( event.keyCode, - inputEventType = InputEventType.DOWN_UP, + action = KeyEvent.ACTION_DOWN, + scanCode = event.scanCode, + source = event.source, + ) + + useCase.imitateButtonPress( + event.keyCode, + action = KeyEvent.ACTION_UP, scanCode = event.scanCode, source = event.source, ) @@ -1424,21 +1430,34 @@ class KeyMapAlgorithm( !mappedToDoublePress && event is KeyCodeEvent ) { - val keyEventAction = if (imitateUpKeyEvent) { - InputEventType.UP + if (imitateUpKeyEvent) { + useCase.imitateButtonPress( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_UP, + scanCode = event.scanCode, + source = event.source, + ) } else { - InputEventType.DOWN_UP + useCase.imitateButtonPress( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_DOWN, + scanCode = event.scanCode, + source = event.source, + ) + useCase.imitateButtonPress( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_UP, + scanCode = event.scanCode, + source = event.source, + ) } - useCase.imitateButtonPress( - keyCode = event.keyCode, - metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), - deviceId = event.deviceId, - inputEventType = keyEventAction, - scanCode = event.scanCode, - source = event.source, - ) - keyCodesToImitateUpAction.remove(event.keyCode) } @@ -1577,7 +1596,7 @@ class KeyMapAlgorithm( keyCode = keyCode, metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), deviceId = deviceId, - inputEventType = InputEventType.DOWN, + action = KeyEvent.ACTION_DOWN, scanCode = scanCode, source = source, ) // use down action because this is what Android does diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt index 91ee0cc885..fc9eb48f7f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt @@ -7,7 +7,10 @@ import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.base.trigger.AssistantTriggerType import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey +import io.github.sds100.keymapper.base.trigger.RecordTriggerController +import io.github.sds100.keymapper.base.trigger.RecordTriggerState import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent @@ -24,7 +27,8 @@ class KeyMapDetectionController( private val performActionsUseCase: PerformActionsUseCase, private val detectConstraints: DetectConstraintsUseCase, private val inputEventHub: InputEventHub, - private val pauseKeyMapsUseCase: PauseKeyMapsUseCase + private val pauseKeyMapsUseCase: PauseKeyMapsUseCase, + private val recordTriggerController: RecordTriggerController ) : InputEventHubCallback { companion object { private const val INPUT_EVENT_HUB_ID = "key_map_controller" @@ -66,6 +70,10 @@ class KeyMapDetectionController( return false } + if (recordTriggerController.state.value is RecordTriggerState.CountingDown) { + return false + } + if (event is KMKeyEvent) { return algorithm.onKeyEvent(event) } else { @@ -77,6 +85,18 @@ class KeyMapDetectionController( algorithm.onFingerprintGesture(type) } + fun onFloatingButtonDown(button: String) { + algorithm.onFloatingButtonDown(button) + } + + fun onFloatingButtonUp(button: String) { + algorithm.onFloatingButtonUp(button) + } + + fun onAssistantEvent(event: AssistantTriggerType) { + algorithm.onAssistantEvent(event) + } + fun teardown() { reset() inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt index dcdac06267..9f3a4584fa 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt @@ -4,7 +4,7 @@ import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.actions.RepeatMode -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.system.inputevents.InputEventUtils import kotlinx.coroutines.CoroutineScope @@ -75,13 +75,13 @@ class ParallelTriggerActionPerformer( actionIsHeldDown[actionIndex] = true } - val actionInputEventType = when { - performUpAction -> InputEventType.UP - action.holdDown -> InputEventType.DOWN - else -> InputEventType.DOWN_UP + val actionInputEventAction = when { + performUpAction -> InputEventAction.UP + action.holdDown -> InputEventAction.DOWN + else -> InputEventAction.DOWN_UP } - performAction(action, actionInputEventType, metaState) + performAction(action, actionInputEventAction, metaState) if (action.repeat && action.holdDown) { delay(action.holdDownDuration?.toLong() ?: defaultHoldDownDuration.value) @@ -124,13 +124,13 @@ class ParallelTriggerActionPerformer( while (isActive && continueRepeating) { if (action.holdDown) { - performAction(action, InputEventType.DOWN, metaState) + performAction(action, InputEventAction.DOWN, metaState) delay( action.holdDownDuration?.toLong() ?: defaultHoldDownDuration.value, ) - performAction(action, InputEventType.UP, metaState) + performAction(action, InputEventAction.UP, metaState) } else { - performAction(action, InputEventType.DOWN_UP, metaState) + performAction(action, InputEventAction.DOWN_UP, metaState) } repeatCount++ @@ -159,7 +159,7 @@ class ParallelTriggerActionPerformer( if (actionIsHeldDown[actionIndex]) { actionIsHeldDown[actionIndex] = false - performAction(action, InputEventType.UP, metaState) + performAction(action, InputEventAction.UP, metaState) } } } @@ -173,7 +173,7 @@ class ParallelTriggerActionPerformer( coroutineScope.launch { for ((index, isHeldDown) in actionIsHeldDown.withIndex()) { if (isHeldDown) { - performAction(actionList[index], inputEventType = InputEventType.UP, 0) + performAction(actionList[index], inputEventAction = InputEventAction.UP, 0) } } } @@ -190,11 +190,11 @@ class ParallelTriggerActionPerformer( private suspend fun performAction( action: Action, - inputEventType: InputEventType, + inputEventAction: InputEventAction, metaState: Int, ) { repeat(action.multiplier ?: 1) { - useCase.perform(action.data, inputEventType, metaState) + useCase.perform(action.data, inputEventAction, metaState) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt index a040f7f860..92227fec03 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt @@ -2,7 +2,7 @@ package io.github.sds100.keymapper.base.keymaps.detection import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.PerformActionsUseCase -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -37,7 +37,7 @@ class SequenceTriggerActionPerformer( private suspend fun performAction(action: Action, metaState: Int) { repeat(action.multiplier ?: 1) { - useCase.perform(action.data, InputEventType.DOWN_UP, metaState) + useCase.perform(action.data, InputEventAction.DOWN_UP, metaState) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt index 0e4b6e774f..178d96e19d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt @@ -4,15 +4,13 @@ import android.view.KeyEvent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.github.sds100.keymapper.base.input.InjectKeyEventModel import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector -import io.github.sds100.keymapper.common.utils.InputDeviceInfo -import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -24,11 +22,13 @@ import kotlinx.coroutines.launch * on Android 11. There was a bug in the system where enabling an accessibility service * would reset the device ID of key events to -1. */ +// TODO remove this feature because it is extra maintenance for a bug that only exists on a small amount of devices. +// TODO update changelog and website, remove strings. class RerouteKeyEventsController @AssistedInject constructor( @Assisted private val coroutineScope: CoroutineScope, private val keyMapperImeMessenger: ImeInputEventInjector, - private val useCaseFactory: RerouteKeyEventsUseCaseImpl.Factory, + private val useCase: RerouteKeyEventsUseCase, private val inputEventHub: InputEventHub ) : InputEventHubCallback { @@ -43,8 +43,6 @@ class RerouteKeyEventsController @AssistedInject constructor( ): RerouteKeyEventsController } - private val useCase = useCaseFactory.create(keyMapperImeMessenger) - /** * The job of the key that should be repeating. This should be a down key event for the last * key that has been pressed down. @@ -85,20 +83,8 @@ class RerouteKeyEventsController @AssistedInject constructor( } return when (event.action) { - KeyEvent.ACTION_DOWN -> onKeyDown( - event.keyCode, - event.device, - event.metaState, - event.scanCode, - ) - - KeyEvent.ACTION_UP -> onKeyUp( - event.keyCode, - event.device, - event.metaState, - event.scanCode, - ) - + KeyEvent.ACTION_DOWN -> onKeyDown(event) + KeyEvent.ACTION_UP -> onKeyUp(event) else -> false } } @@ -107,21 +93,19 @@ class RerouteKeyEventsController @AssistedInject constructor( * @return whether to consume the key event. */ private fun onKeyDown( - keyCode: Int, - device: InputDeviceInfo?, - metaState: Int, - scanCode: Int = 0, + event: KMKeyEvent ): Boolean { - val inputKeyModel = InputKeyModel( - keyCode = keyCode, - inputType = InputEventType.DOWN, - metaState = metaState, - deviceId = device?.id ?: 0, - scanCode = scanCode, - repeat = 0, + val injectModel = InjectKeyEventModel( + keyCode = event.keyCode, + action = KeyEvent.ACTION_DOWN, + metaState = event.metaState, + deviceId = event.deviceId, + scanCode = event.scanCode, + repeatCount = event.repeatCount, + source = event.source ) - useCase.inputKeyEvent(inputKeyModel) + useCase.inputKeyEvent(injectModel) repeatJob?.cancel() @@ -131,7 +115,7 @@ class RerouteKeyEventsController @AssistedInject constructor( var repeatCount = 1 while (isActive) { - useCase.inputKeyEvent(inputKeyModel.copy(repeat = repeatCount)) + useCase.inputKeyEvent(injectModel.copy(repeatCount = repeatCount)) delay(50) repeatCount++ } @@ -140,21 +124,17 @@ class RerouteKeyEventsController @AssistedInject constructor( return true } - private fun onKeyUp( - keyCode: Int, - device: InputDeviceInfo?, - metaState: Int, - scanCode: Int = 0, - ): Boolean { + private fun onKeyUp(event: KMKeyEvent): Boolean { repeatJob?.cancel() - val inputKeyModel = InputKeyModel( - keyCode = keyCode, - inputType = InputEventType.UP, - metaState = metaState, - deviceId = device?.id ?: 0, - scanCode = scanCode, - repeat = 0, + val inputKeyModel = InjectKeyEventModel( + keyCode = event.keyCode, + action = KeyEvent.ACTION_UP, + metaState = event.metaState, + deviceId = event.deviceId, + scanCode = event.scanCode, + repeatCount = event.repeatCount, + source = event.source ) useCase.inputKeyEvent(inputKeyModel) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt index 048ff244e6..7e12942192 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt @@ -1,41 +1,33 @@ package io.github.sds100.keymapper.base.reroutekeyevents import android.os.Build -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import io.github.sds100.keymapper.base.input.InjectKeyEventModel import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking +import javax.inject.Inject +import javax.inject.Singleton /** * This is used for the feature created in issue #618 to fix the device IDs of key events * on Android 11. There was a bug in the system where enabling an accessibility service * would reset the device ID of key events to -1. */ -class RerouteKeyEventsUseCaseImpl @AssistedInject constructor( - @Assisted - private val keyMapperImeMessenger: ImeInputEventInjector, +@Singleton +class RerouteKeyEventsUseCaseImpl @Inject constructor( + // MUST use the input event injector instead of the InputManager to bypass the buggy code in Android. + private val imeInputEventInjector: ImeInputEventInjector, private val inputMethodAdapter: InputMethodAdapter, private val preferenceRepository: PreferenceRepository, private val buildConfigProvider: BuildConfigProvider, ) : RerouteKeyEventsUseCase { - @AssistedFactory - interface Factory { - fun create( - keyMapperImeMessenger: ImeInputEventInjector, - ): RerouteKeyEventsUseCaseImpl - } - override val isReroutingEnabled: Flow = preferenceRepository.get(Keys.rerouteKeyEvents).map { it ?: false } @@ -56,23 +48,16 @@ class RerouteKeyEventsUseCaseImpl @AssistedInject constructor( return isReroutingEnabled.firstBlocking() && imeHelper.isCompatibleImeChosen() && - ( - descriptor != null && - devicesToRerouteKeyEvents.firstBlocking() - .contains(descriptor) - ) + (descriptor != null && devicesToRerouteKeyEvents.firstBlocking().contains(descriptor)) } - override fun inputKeyEvent(keyModel: InputKeyModel) { - // It is safe to run the ime injector on the main thread. - runBlocking { - keyMapperImeMessenger.inputKeyEvent(keyModel) - } + override fun inputKeyEvent(keyEvent: InjectKeyEventModel) { + imeInputEventInjector.inputKeyEvent(keyEvent) } } interface RerouteKeyEventsUseCase { val isReroutingEnabled: Flow fun shouldRerouteKeyEvent(descriptor: String?): Boolean - fun inputKeyEvent(keyModel: InputKeyModel) + fun inputKeyEvent(keyEvent: InjectKeyEventModel) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index d31d24a081..6fab500a93 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -23,7 +23,7 @@ import androidx.savedstate.SavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.input.InputEventDetectionSource -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.MathUtils @@ -244,7 +244,7 @@ abstract class BaseAccessibilityService : override fun onKeyEvent(event: KeyEvent?): Boolean { event ?: return super.onKeyEvent(event) - val kmKeyEvent = KMKeyEvent.fromKeyEvent(event) ?: return false + val kmKeyEvent = KMKeyEvent.fromAndroidKeyEvent(event) ?: return false return getController()?.onKeyEvent( kmKeyEvent, @@ -284,7 +284,7 @@ abstract class BaseAccessibilityService : } } - override fun tapScreen(x: Int, y: Int, inputEventType: InputEventType): KMResult<*> { + override fun tapScreen(x: Int, y: Int, inputEventAction: InputEventAction): KMResult<*> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val duration = 1L // ms @@ -294,7 +294,7 @@ abstract class BaseAccessibilityService : val strokeDescription = when { - inputEventType == InputEventType.DOWN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> + inputEventAction == InputEventAction.DOWN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> StrokeDescription( path, 0, @@ -302,7 +302,7 @@ abstract class BaseAccessibilityService : true, ) - inputEventType == InputEventType.UP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> + inputEventAction == InputEventAction.UP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> StrokeDescription( path, 59999, @@ -338,7 +338,7 @@ abstract class BaseAccessibilityService : yEnd: Int, fingerCount: Int, duration: Int, - inputEventType: InputEventType, + inputEventAction: InputEventAction, ): KMResult<*> { // virtual distance between fingers on multitouch gestures val fingerGestureDistance = 10L @@ -440,7 +440,7 @@ abstract class BaseAccessibilityService : pinchType: PinchScreenType, fingerCount: Int, duration: Int, - inputEventType: InputEventType, + inputEventAction: InputEventAction, ): KMResult<*> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (fingerCount >= GestureDescription.getMaxStrokeCount()) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 29c103794c..6fa5d322b4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -22,6 +22,7 @@ import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImp import io.github.sds100.keymapper.base.keymaps.detection.KeyMapDetectionController import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController +import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.minusFlag @@ -31,11 +32,9 @@ import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper -import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -64,15 +63,13 @@ abstract class BaseAccessibilityServiceController( private val detectConstraintsUseCaseFactory: DetectConstraintsUseCaseImpl.Factory, private val fingerprintGesturesSupported: FingerprintGesturesSupportedUseCase, private val pauseKeyMapsUseCase: PauseKeyMapsUseCase, - private val devicesAdapter: DevicesAdapter, - private val suAdapter: SuAdapter, private val settingsRepository: PreferenceRepository, private val systemBridgeSetupController: SystemBridgeSetupController, private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, - private val inputEventHub: InputEventHub + private val inputEventHub: InputEventHub, + private val recordTriggerController: RecordTriggerController ) { companion object { - private const val DEFAULT_NOTIFICATION_TIMEOUT = 200L private const val CALLBACK_ID_ACCESSIBILITY_SERVICE = "accessibility_service" } @@ -94,7 +91,8 @@ abstract class BaseAccessibilityServiceController( performActionsUseCase, detectConstraintsUseCase, inputEventHub, - pauseKeyMapsUseCase + pauseKeyMapsUseCase, + recordTriggerController ) val triggerKeyMapFromOtherAppsController = TriggerKeyMapFromOtherAppsController( @@ -174,7 +172,7 @@ abstract class BaseAccessibilityServiceController( override fun onKeyEvent(event: KeyEvent?): Boolean { event ?: return false - val kmKeyEvent = KMKeyEvent.fromKeyEvent(event) ?: return false + val kmKeyEvent = KMKeyEvent.fromAndroidKeyEvent(event) ?: return false return onKeyEventFromIme(kmKeyEvent) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt index c62a1c8b05..3021d7f6a0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt @@ -2,7 +2,7 @@ package io.github.sds100.keymapper.base.system.accessibility import android.os.Build import androidx.annotation.RequiresApi -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.PinchScreenType import kotlinx.coroutines.flow.Flow @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow interface IAccessibilityService { fun doGlobalAction(action: Int): KMResult<*> - fun tapScreen(x: Int, y: Int, inputEventType: InputEventType): KMResult<*> + fun tapScreen(x: Int, y: Int, inputEventAction: InputEventAction): KMResult<*> fun swipeScreen( xStart: Int, @@ -19,7 +19,7 @@ interface IAccessibilityService { yEnd: Int, fingerCount: Int, duration: Int, - inputEventType: InputEventType, + inputEventAction: InputEventAction, ): KMResult<*> fun pinchScreen( @@ -29,7 +29,7 @@ interface IAccessibilityService { pinchType: PinchScreenType, fingerCount: Int, duration: Int, - inputEventType: InputEventType, + inputEventAction: InputEventAction, ): KMResult<*> val isFingerprintGestureDetectionAvailable: Boolean diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt index 6ebe31de6e..97d4506bb9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt @@ -1,16 +1,9 @@ package io.github.sds100.keymapper.base.system.inputmethod -import android.content.Context -import android.content.Intent -import android.os.Build import android.os.SystemClock import android.view.KeyCharacterMap import android.view.KeyEvent -import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.common.utils.InputEventType -import io.github.sds100.keymapper.system.inputevents.InputEventInjector -import io.github.sds100.keymapper.system.inputevents.createKeyEvent -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel +import io.github.sds100.keymapper.base.input.InjectKeyEventModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import timber.log.Timber @@ -23,33 +16,15 @@ import javax.inject.Singleton */ @Singleton class ImeInputEventInjectorImpl @Inject constructor( - @ApplicationContext private val ctx: Context, private val keyEventRelayService: KeyEventRelayServiceWrapper, private val inputMethodAdapter: InputMethodAdapter, ) : ImeInputEventInjector { - companion object { - // DON'T CHANGE THESE!!! - private const val KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN_UP = - "io.github.sds100.keymapper.inputmethod.ACTION_INPUT_DOWN_UP" - private const val KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN = - "io.github.sds100.keymapper.inputmethod.ACTION_INPUT_DOWN" - private const val KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_UP = - "io.github.sds100.keymapper.inputmethod.ACTION_INPUT_UP" - private const val KEY_MAPPER_INPUT_METHOD_ACTION_TEXT = - "io.github.sds100.keymapper.inputmethod.ACTION_INPUT_TEXT" - - private const val KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT = - "io.github.sds100.keymapper.inputmethod.EXTRA_KEY_EVENT" - private const val KEY_MAPPER_INPUT_METHOD_EXTRA_TEXT = - "io.github.sds100.keymapper.inputmethod.EXTRA_TEXT" - private const val CALLBACK_ID_INPUT_METHOD = "input_method" } - // TODO replace with a method that accepts KMKeyEvent - override suspend fun inputKeyEvent(model: InputKeyModel) { - Timber.d("Inject key event with input method ${KeyEvent.keyCodeToString(model.keyCode)}, $model") + override fun inputKeyEvent(event: InjectKeyEventModel) { + Timber.d("Inject key event with input method $event") val imePackageName = inputMethodAdapter.chosenIme.value?.packageName @@ -58,79 +33,11 @@ class ImeInputEventInjectorImpl @Inject constructor( return } - // Only use the new key event relay service on Android 14+ because - // it introduced a 1 second delay for broadcasts to context-registered - // receivers. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - inputKeyEventRelayService(model, imePackageName) - } else { - inputKeyEventBroadcast(model, imePackageName) - } - } - - private fun inputKeyEventBroadcast(model: InputKeyModel, imePackageName: String) { - val intentAction = when (model.inputType) { - InputEventType.DOWN -> KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN - InputEventType.DOWN_UP -> KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN_UP - InputEventType.UP -> KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_UP - } - - Intent(intentAction).apply { - setPackage(imePackageName) - - val action = when (model.inputType) { - InputEventType.DOWN, InputEventType.DOWN_UP -> KeyEvent.ACTION_DOWN - InputEventType.UP -> KeyEvent.ACTION_UP - } - - val eventTime = SystemClock.uptimeMillis() - - val keyEvent = createKeyEvent(eventTime, action, model) - - putExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT, keyEvent) - - ctx.sendBroadcast(this) - } - } - - 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) - keyEventRelayService.sendKeyEvent( - downKeyEvent, - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) - - val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) - keyEventRelayService.sendKeyEvent( - upKeyEvent, - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) - } - - InputEventType.DOWN -> { - val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) - keyEventRelayService.sendKeyEvent( - downKeyEvent, - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) - } - - InputEventType.UP -> { - val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) - keyEventRelayService.sendKeyEvent( - upKeyEvent, - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) - } - } + keyEventRelayService.sendKeyEvent( + event.toAndroidKeyEvent(), + imePackageName, + CALLBACK_ID_INPUT_METHOD, + ) } override fun inputText(text: String) { @@ -143,25 +50,11 @@ class ImeInputEventInjectorImpl @Inject constructor( return } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - inputTextRelayService(text, imePackageName) - } else { - inputTextBroadcast(text, imePackageName) - } - } - - private fun inputTextBroadcast(text: String, imePackageName: String) { - Intent(KEY_MAPPER_INPUT_METHOD_ACTION_TEXT).apply { - setPackage(imePackageName) - - putExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_TEXT, text) - ctx.sendBroadcast(this) - } + inputTextRelayService(text, imePackageName) } private fun inputTextRelayService(text: String, imePackageName: String) { // taken from android.view.inputmethod.BaseInputConnection.sendCurrentText() - if (text.isEmpty()) { return } @@ -207,6 +100,7 @@ class ImeInputEventInjectorImpl @Inject constructor( } } -interface ImeInputEventInjector : InputEventInjector { +interface ImeInputEventInjector { fun inputText(text: String) + fun inputKeyEvent(event: InjectKeyEventModel) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt index 3d0d14e61c..401c85948d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt @@ -1,27 +1,19 @@ package io.github.sds100.keymapper.base.system.navigation +import android.view.InputDevice import android.view.KeyEvent import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import io.github.sds100.keymapper.base.input.InjectKeyEventModel +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeAction import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService -import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.success -import io.github.sds100.keymapper.system.inputevents.InputEventInjector -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import io.github.sds100.keymapper.system.permissions.Permission -import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.root.SuAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import io.github.sds100.keymapper.common.utils.then class OpenMenuHelper( - private val suAdapter: SuAdapter, private val accessibilityService: IAccessibilityService, - private val shizukuInputEventInjector: InputEventInjector, - private val permissionAdapter: PermissionAdapter, - private val coroutineScope: CoroutineScope, + private val inputEventHub: InputEventHub, ) { companion object { @@ -30,22 +22,24 @@ class OpenMenuHelper( fun openMenu(): KMResult<*> { when { - permissionAdapter.isGranted(Permission.SHIZUKU) -> { - val inputKeyModel = InputKeyModel( + inputEventHub.isSystemBridgeConnected() -> { + val downEvent = InjectKeyEventModel( keyCode = KeyEvent.KEYCODE_MENU, - inputType = InputEventType.DOWN_UP, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + scanCode = 0, + deviceId = -1, + repeatCount = 0, + source = InputDevice.SOURCE_UNKNOWN, ) - coroutineScope.launch { - shizukuInputEventInjector.inputKeyEvent(inputKeyModel) - } + val upEvent = downEvent.copy(action = KeyEvent.ACTION_UP) - return success() + return inputEventHub.injectKeyEvent(downEvent).then { + inputEventHub.injectKeyEvent(upEvent) + } } - suAdapter.isRooted.firstBlocking() -> - return suAdapter.execute("input keyevent ${KeyEvent.KEYCODE_MENU}\n") - else -> { accessibilityService.performActionOnNode({ it.contentDescription == OVERFLOW_MENU_CONTENT_DESCRIPTION }) { AccessibilityNodeAction( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 49b12d1d7b..0a91f09a95 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -210,7 +211,7 @@ class RecordTriggerControllerImpl @Inject constructor( } interface RecordTriggerController { - val state: Flow + val state: StateFlow val onRecordKey: Flow /** diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index 7270772a6b..b74c50e16e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -2,17 +2,16 @@ package io.github.sds100.keymapper.base.actions import android.view.InputDevice import android.view.KeyEvent +import io.github.sds100.keymapper.base.input.InjectKeyEventModel import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.base.system.devices.FakeDevicesAdapter import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.utils.InputDeviceInfo -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.popup.ToastAdapter import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -50,13 +49,10 @@ class PerformActionsUseCaseTest { mockToastAdapter = mock() useCase = PerformActionsUseCaseImpl( - testScope, service = mockAccessibilityService, inputMethodAdapter = mock(), fileAdapter = mock(), - suAdapter = mock { - on { isRooted }.then { MutableStateFlow(false) } - }, + suAdapter = mock {}, shell = mock(), intentAdapter = mock(), getActionErrorUseCase = mock(), @@ -79,9 +75,9 @@ class PerformActionsUseCaseTest { resourceProvider = mock(), settingsRepository = mock(), soundsManager = mock(), - permissionAdapter = mock(), notificationReceiverAdapter = mock(), ringtoneAdapter = mock(), + inputEventHub = mock() ) } @@ -89,244 +85,270 @@ class PerformActionsUseCaseTest { * issue #771 */ @Test - fun `dont show accessibility service not found error for open menu action`() = runTest(testDispatcher) { - // GIVEN - val action = ActionData.OpenMenu - - whenever( - mockAccessibilityService.performActionOnNode( - any(), - any(), - ), - ).doReturn(KMError.FailedToFindAccessibilityNode) - - // WHEN - useCase.perform(action) - - // THEN - verify(mockToastAdapter, never()).show(anyOrNull()) - } + fun `dont show accessibility service not found error for open menu action`() = + runTest(testDispatcher) { + // GIVEN + val action = ActionData.OpenMenu + + whenever( + mockAccessibilityService.performActionOnNode( + any(), + any(), + ), + ).doReturn(KMError.FailedToFindAccessibilityNode) + + // WHEN + useCase.perform(action) + + // THEN + verify(mockToastAdapter, never()).show(anyOrNull()) + } /** * issue #772 */ @Test - fun `set the device id of key event actions to a connected game controller if is a game pad key code`() = runTest(testDispatcher) { - // GIVEN - val fakeGamePad = InputDeviceInfo( - descriptor = "game_pad", - name = "Game pad", - id = 1, - isExternal = true, - isGameController = true, - sources = InputDevice.SOURCE_GAMEPAD - ) - - fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad)) - - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = null, - ) + fun `set the device id of key event actions to a connected game controller if is a game pad key code`() = + runTest(testDispatcher) { + // GIVEN + val fakeGamePad = InputDeviceInfo( + descriptor = "game_pad", + name = "Game pad", + id = 1, + isExternal = true, + isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD + ) + + fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad)) + + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = null, + ) + + // WHEN + useCase.perform(action) + + // THEN + val expectedDownEvent = InjectKeyEventModel( + + keyCode = KeyEvent.KEYCODE_BUTTON_A, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = fakeGamePad.id, + scanCode = 0, + repeatCount = 0, + source = InputDevice.SOURCE_GAMEPAD, + ) - // WHEN - useCase.perform(action) - - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = fakeGamePad.id, - scanCode = 0, - repeat = 0, - source = InputDevice.SOURCE_GAMEPAD, - ) + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) - } + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + } /** * issue #772 */ @Test - fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() = runTest(testDispatcher) { - // GIVEN - fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList()) + fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() = + runTest(testDispatcher) { + // GIVEN + fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList()) - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = null, - ) + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = null, + ) - // WHEN - useCase.perform(action) - - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = 0, - scanCode = 0, - repeat = 0, - source = InputDevice.SOURCE_GAMEPAD, - ) + // WHEN + useCase.perform(action) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) - } + // THEN + val expectedDownEvent = InjectKeyEventModel( + + keyCode = KeyEvent.KEYCODE_BUTTON_A, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = 0, + scanCode = 0, + repeatCount = 0, + source = InputDevice.SOURCE_GAMEPAD, + ) + + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) + + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + } /** * issue #772 */ @Test - fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() = runTest(testDispatcher) { - // GIVEN - val fakeGamePad = InputDeviceInfo( - descriptor = "game_pad", - name = "Game pad", - id = 1, - isExternal = true, - isGameController = true, - sources = InputDevice.SOURCE_GAMEPAD - ) + fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() = + runTest(testDispatcher) { + // GIVEN + val fakeGamePad = InputDeviceInfo( + descriptor = "game_pad", + name = "Game pad", + id = 1, + isExternal = true, + isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD + ) + + val fakeKeyboard = InputDeviceInfo( + descriptor = "keyboard", + name = "Keyboard", + id = 2, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD + ) + + fakeDevicesAdapter.connectedInputDevices.value = + State.Data(listOf(fakeGamePad, fakeKeyboard)) + + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = ActionData.InputKeyEvent.Device( + descriptor = "keyboard", + name = "Keyboard", + ), + ) - val fakeKeyboard = InputDeviceInfo( - descriptor = "keyboard", - name = "Keyboard", - id = 2, - isExternal = true, - isGameController = false, - sources = InputDevice.SOURCE_GAMEPAD - ) + // WHEN + useCase.perform(action) - fakeDevicesAdapter.connectedInputDevices.value = - State.Data(listOf(fakeGamePad, fakeKeyboard)) + // THEN + val expectedDownEvent = InjectKeyEventModel( - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = ActionData.InputKeyEvent.Device( - descriptor = "keyboard", - name = "Keyboard", - ), - ) + keyCode = KeyEvent.KEYCODE_BUTTON_A, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = fakeKeyboard.id, + scanCode = 0, + repeatCount = 0, + source = InputDevice.SOURCE_GAMEPAD, + ) - // WHEN - useCase.perform(action) - - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = fakeKeyboard.id, - scanCode = 0, - repeat = 0, - source = InputDevice.SOURCE_GAMEPAD, - ) + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) - } + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + } /** * issue #637 */ @Test - fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() = runTest(testDispatcher) { - // GIVEN - val descriptor = "fake_device_descriptor" - - val action = ActionData.InputKeyEvent( - keyCode = 1, - metaState = 0, - useShell = false, - device = ActionData.InputKeyEvent.Device( - descriptor = descriptor, - name = "fake_name_2", - ), - ) - - fakeDevicesAdapter.connectedInputDevices.value = State.Data( - listOf( - InputDeviceInfo( - descriptor = descriptor, - name = "fake_name_1", - id = 10, - isExternal = true, - isGameController = false, - sources = InputDevice.SOURCE_GAMEPAD - ), + fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() = + runTest(testDispatcher) { + // GIVEN + val descriptor = "fake_device_descriptor" - InputDeviceInfo( + val action = ActionData.InputKeyEvent( + keyCode = 1, + metaState = 0, + useShell = false, + device = ActionData.InputKeyEvent.Device( descriptor = descriptor, name = "fake_name_2", - id = 11, - isExternal = true, - isGameController = false, - sources = InputDevice.SOURCE_GAMEPAD ), - ), - ) + ) + + fakeDevicesAdapter.connectedInputDevices.value = State.Data( + listOf( + InputDeviceInfo( + descriptor = descriptor, + name = "fake_name_1", + id = 10, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD + ), + + InputDeviceInfo( + descriptor = descriptor, + name = "fake_name_2", + id = 11, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD + ), + ), + ) + + // none of the devices support the key code + fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false } - // none of the devices support the key code - fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false } + // WHEN + useCase.perform(action, inputEventAction = InputEventAction.DOWN_UP, keyMetaState = 0) - // WHEN - useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) + // THEN + val expectedDownEvent = InjectKeyEventModel( - // THEN - verify(mockImeInputEventInjector, times(1)).inputKeyEvent( - InputKeyModel( keyCode = 1, - inputType = InputEventType.DOWN_UP, + action = KeyEvent.ACTION_DOWN, metaState = 0, deviceId = 11, scanCode = 0, - repeat = 0, + repeatCount = 0, source = InputDevice.SOURCE_KEYBOARD, - ), - ) - } + ) + + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) + + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + } @Test - fun `perform key event action with no device name, ensure action is still performed with correct device id`() = runTest(testDispatcher) { - // GIVEN - val descriptor = "fake_device_descriptor" - - val action = ActionData.InputKeyEvent( - keyCode = 1, - metaState = 0, - useShell = false, - device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""), - ) + fun `perform key event action with no device name, ensure action is still performed with correct device id`() = + runTest(testDispatcher) { + // GIVEN + val descriptor = "fake_device_descriptor" - fakeDevicesAdapter.connectedInputDevices.value = State.Data( - listOf( - InputDeviceInfo( - descriptor = descriptor, - name = "fake_name", - id = 10, - isExternal = true, - isGameController = false, - sources = InputDevice.SOURCE_GAMEPAD + val action = ActionData.InputKeyEvent( + keyCode = 1, + metaState = 0, + useShell = false, + device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""), + ) + + fakeDevicesAdapter.connectedInputDevices.value = State.Data( + listOf( + InputDeviceInfo( + descriptor = descriptor, + name = "fake_name", + id = 10, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD + ), ), - ), - ) + ) + + // WHEN + useCase.perform(action, inputEventAction = InputEventAction.DOWN_UP, keyMetaState = 0) - // WHEN - useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) + // THEN + val expectedDownEvent = InjectKeyEventModel( - // THEN - verify(mockImeInputEventInjector, times(1)).inputKeyEvent( - InputKeyModel( keyCode = 1, - inputType = InputEventType.DOWN_UP, + action = KeyEvent.ACTION_DOWN, metaState = 0, deviceId = 10, scanCode = 0, - repeat = 0, + repeatCount = 0, source = InputDevice.SOURCE_KEYBOARD, - ), - ) - } + ) + + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) + + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 6770b2dc88..887dad011b 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -17,10 +17,8 @@ import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.detection.KeyMapAlgorithm import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey -import io.github.sds100.keymapper.base.trigger.FloatingButtonKey import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger @@ -32,7 +30,7 @@ import io.github.sds100.keymapper.base.utils.sequenceTrigger import io.github.sds100.keymapper.base.utils.singleKeyTrigger import io.github.sds100.keymapper.base.utils.triggerKey import io.github.sds100.keymapper.common.utils.InputDeviceInfo -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.system.camera.CameraLens @@ -52,7 +50,6 @@ import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` -import org.junit.Assert.fail import org.junit.Before import org.junit.Rule import org.junit.Test @@ -985,11 +982,11 @@ class KeyMapAlgorithmTest { inOrder(performActionsUseCase) { inputMotionEvent(axisHatX = -1.0f) - verify(performActionsUseCase, times(1)).perform(action.data, InputEventType.DOWN) + verify(performActionsUseCase, times(1)).perform(action.data, InputEventAction.DOWN) delay(1000) // Hold down the DPAD button for 1 second. inputMotionEvent(axisHatX = 0.0f) - verify(performActionsUseCase, times(1)).perform(action.data, InputEventType.UP) + verify(performActionsUseCase, times(1)).perform(action.data, InputEventAction.UP) } } @@ -1341,12 +1338,11 @@ class KeyMapAlgorithmTest { ) // If no triggers are detected - mockTriggerKeyInput(shorterTrigger.keys[0], 100L) verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) verify(performActionsUseCase, never()).perform(TEST_ACTION.data) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(2)).imitateButtonPress( any(), any(), any(), @@ -2044,7 +2040,14 @@ class KeyMapAlgorithmTest { inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP) // THEN - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress(keyCode = 1) + verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + keyCode = 1, + action = KeyEvent.ACTION_DOWN + ) + verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + keyCode = 1, + action = KeyEvent.ACTION_UP + ) verifyNoMoreInteractions() // verify nothing happens and no key events are consumed when the 2nd key in the trigger is pressed @@ -2053,8 +2056,22 @@ class KeyMapAlgorithmTest { assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(false)) // THEN - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 1) - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 2) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + keyCode = 1, + action = KeyEvent.ACTION_DOWN + ) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + keyCode = 1, + action = KeyEvent.ACTION_UP + ) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + keyCode = 2, + action = KeyEvent.ACTION_DOWN + ) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + keyCode = 2, + action = KeyEvent.ACTION_UP + ) verify(performActionsUseCase, never()).perform(action = TEST_ACTION.data) // verify the action is performed and no keys are imitated when triggering the key map @@ -2105,8 +2122,22 @@ class KeyMapAlgorithmTest { inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP) // THEN - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 1) - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 2) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + keyCode = 1, + action = KeyEvent.ACTION_DOWN + ) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + keyCode = 1, + action = KeyEvent.ACTION_UP + ) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + keyCode = 2, + action = KeyEvent.ACTION_DOWN + ) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + keyCode = 2, + action = KeyEvent.ACTION_UP + ) } /** @@ -2568,7 +2599,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.DOWN, + InputEventAction.DOWN, metaState, ) @@ -2576,13 +2607,13 @@ class KeyMapAlgorithmTest { KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, - InputEventType.DOWN, + KeyEvent.ACTION_DOWN, scanCode = 33, ) verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.UP, + InputEventAction.UP, 0, ) @@ -2590,7 +2621,7 @@ class KeyMapAlgorithmTest { KeyEvent.KEYCODE_E, 0, FAKE_KEYBOARD_DEVICE_ID, - InputEventType.UP, + KeyEvent.ACTION_UP, scanCode = 33, ) @@ -2637,7 +2668,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.DOWN, + InputEventAction.DOWN, metaState, ) @@ -2645,7 +2676,7 @@ class KeyMapAlgorithmTest { KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, - InputEventType.DOWN, + KeyEvent.ACTION_DOWN, scanCode = 33, ) @@ -2653,13 +2684,13 @@ class KeyMapAlgorithmTest { KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, - InputEventType.UP, + KeyEvent.ACTION_UP, scanCode = 33, ) verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.UP, + InputEventAction.UP, 0, ) @@ -2783,7 +2814,7 @@ class KeyMapAlgorithmTest { // THEN verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.DOWN, + InputEventAction.DOWN, ) // WHEN @@ -2791,7 +2822,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.UP, + InputEventAction.UP, ) } @@ -3230,10 +3261,16 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // then - verify( - detectKeyMapsUseCase, - times(1), - ).imitateButtonPress(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) + verify(detectKeyMapsUseCase, times(1)) + .imitateButtonPress( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + action = KeyEvent.ACTION_DOWN + ) + verify(detectKeyMapsUseCase, times(1)) + .imitateButtonPress( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + action = KeyEvent.ACTION_UP + ) } @Test @@ -3346,10 +3383,14 @@ class KeyMapAlgorithmTest { delay = 100L, ) - verify( - detectKeyMapsUseCase, - times(1), - ).imitateButtonPress(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) + verify(detectKeyMapsUseCase, times(1)) + .imitateButtonPress( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + action = KeyEvent.ACTION_DOWN + ) + + verify(detectKeyMapsUseCase, times(1)) + .imitateButtonPress(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, action = KeyEvent.ACTION_UP) } @Test diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventType.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventAction.kt similarity index 73% rename from common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventType.kt rename to common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventAction.kt index e3805172d5..0eab15976d 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventType.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventAction.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.common.utils -enum class InputEventType { +enum class InputEventAction { DOWN_UP, DOWN, UP, diff --git a/sysbridge/src/main/cpp/android/input/InputDevice.h b/sysbridge/src/main/cpp/android/input/InputDevice.h index cf23049775..c7af07cc20 100644 --- a/sysbridge/src/main/cpp/android/input/InputDevice.h +++ b/sysbridge/src/main/cpp/android/input/InputDevice.h @@ -39,10 +39,10 @@ namespace android { std::string name; std::string location; std::string uniqueId; - uint16_t bus; - uint16_t vendor; - uint16_t product; - uint16_t version; + int bus; + int vendor; + int product; + int version; // A composite input device descriptor string that uniquely identifies the device // even across reboots or reconnections. The value of this field is used by diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp index 9fe17d8447..bbf5a5437b 100644 --- a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -279,8 +279,8 @@ namespace android { std::optional keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.c_str()); if (!keyCode) { - LOGW("%s: Unknown key code label %s", mTokenizer->getLocation().c_str(), - keyCodeToken.c_str()); +// LOGW("%s: Unknown key code label %s", mTokenizer->getLocation().c_str(), +// keyCodeToken.c_str()); // Do not crash at this point because there may be more flags afterwards that need parsing. } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 5252659d56..83bbc2f885 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -37,7 +37,7 @@ enum CommandType { struct Command { CommandType type; - std::variant data; + std::variant data; }; struct DeviceContext { @@ -52,7 +52,7 @@ void ungrabDevice(jint device_id); static int epollFd = -1; static int commandEventFd = -1; -static std::queue commandQueue; +static std::queue commandQueue; static std::mutex commandMutex; // This maps the file descriptor of an evdev device to its context. @@ -110,9 +110,11 @@ static int findEvdevDevice( int devBus = libevdev_get_id_bustype(*outDev); if (name != devName || - devVendor != vendor || - devProduct != product || - devBus != bus) { + devVendor != vendor || + devProduct != product || + // The hidden device bus field was only added to InputDevice.java in Android 14. + // So only check it if it is a real value + (bus != -1 && devBus != bus)) { libevdev_free(*outDev); close(fd); @@ -127,8 +129,8 @@ static int findEvdevDevice( closedir(dir); LOGE("Input device not found with name: %s, bus: %d, vendor: %d, product: %d", name.c_str(), - bus, - vendor, product); + bus, + vendor, product); return -1; } @@ -149,7 +151,7 @@ convertJInputDeviceIdentifier(JNIEnv *env, jobject jInputDeviceIdentifier) { deviceIdentifier.product = env->GetIntField(jInputDeviceIdentifier, productFieldId); jfieldID nameFieldId = env->GetFieldID(inputDeviceIdentifierClass, "name", - "Ljava/lang/String;"); + "Ljava/lang/String;"); auto nameString = (jstring) env->GetObjectField(jInputDeviceIdentifier, nameFieldId); const char *nameChars = env->GetStringUTFChars(nameString, nullptr); @@ -165,19 +167,21 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { } extern "C" -JNIEXPORT jboolean JNICALL +JNIEXPORT jboolean + +JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNative(JNIEnv *env, - jobject thiz, - jobject jInputDeviceIdentifier) { + jobject thiz, + jobject jInputDeviceIdentifier) { jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); jfieldID idFieldId = env->GetFieldID(inputDeviceIdentifierClass, "id", "I"); android::InputDeviceIdentifier identifier = convertJInputDeviceIdentifier(env, - jInputDeviceIdentifier); + jInputDeviceIdentifier); int deviceId = env->GetIntField(jInputDeviceIdentifier, idFieldId); Command cmd = {GRAB, GrabData{deviceId, identifier}}; - std::lock_guard lock(commandMutex); + std::lock_guard lock(commandMutex); commandQueue.push(cmd); uint64_t val = 1; @@ -205,12 +209,12 @@ int onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); callback->onEvdevEvent(deviceId, - inputEvent.time.tv_sec, - inputEvent.time.tv_usec, - inputEvent.type, - inputEvent.code, - inputEvent.value, - outKeycode); + inputEvent.time.tv_sec, + inputEvent.time.tv_usec, + inputEvent.type, + inputEvent.code, + inputEvent.value, + outKeycode); } if (rc == 1 || rc == 0 || rc == -EAGAIN) { @@ -230,10 +234,10 @@ void handleCommand(const Command &cmd) { struct libevdev *dev = nullptr; int rc = findEvdevDevice(data.identifier.name, - data.identifier.bus, - data.identifier.vendor, - data.identifier.product, - &dev); + data.identifier.bus, + data.identifier.vendor, + data.identifier.product, + &dev); if (rc < 0) { LOGE("Failed to find device for grab command"); return; @@ -242,7 +246,7 @@ void handleCommand(const Command &cmd) { rc = libevdev_grab(dev, LIBEVDEV_GRAB); if (rc < 0) { LOGE("Failed to grab evdev device %s: %s", - libevdev_get_name(dev), strerror(-rc)); + libevdev_get_name(dev), strerror(-rc)); libevdev_free(dev); return; } @@ -273,13 +277,13 @@ void handleCommand(const Command &cmd) { return; } - std::lock_guard lock(evdevDevicesMutex); + std::lock_guard lock(evdevDevicesMutex); evdevDevices->insert_or_assign(evdevFd, context); } else if (cmd.type == UNGRAB) { const UngrabData &data = std::get(cmd.data); - std::lock_guard lock(evdevDevicesMutex); + std::lock_guard lock(evdevDevicesMutex); for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { if (it->second.deviceId == data.deviceId) { int fd = it->first; @@ -295,90 +299,124 @@ void handleCommand(const Command &cmd) { extern "C" JNIEXPORT void JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, - jobject thiz, - jobject jCallbackBinder) { - if (epollFd != -1 || commandEventFd != -1) { - LOGE("The evdev event loop has already started."); - return; - } +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv +*env, +jobject thiz, + jobject +jCallbackBinder) { +if (epollFd != -1 || commandEventFd != -1) { +LOGE("The evdev event loop has already started."); +return; +} epollFd = epoll_create1(EPOLL_CLOEXEC); - if (epollFd == -1) { - LOGE("Failed to create epoll fd: %s", strerror(errno)); - return; - } +if (epollFd == -1) { +LOGE("Failed to create epoll fd: %s", +strerror(errno) +); +return; +} - commandEventFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); - if (commandEventFd == -1) { - LOGE("Failed to create command eventfd: %s", strerror(errno)); - close(epollFd); - return; - } +commandEventFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); +if (commandEventFd == -1) { +LOGE("Failed to create command eventfd: %s", +strerror(errno) +); +close(epollFd); +return; +} - struct epoll_event event{}; - event.events = EPOLLIN; - event.data.fd = commandEventFd; - if (epoll_ctl(epollFd, EPOLL_CTL_ADD, commandEventFd, &event) == -1) { - LOGE("Failed to add command eventfd to epoll: %s", strerror(errno)); - close(epollFd); - close(commandEventFd); - return; - } +struct epoll_event event{}; +event. +events = EPOLLIN; +event.data. +fd = commandEventFd; +if ( +epoll_ctl(epollFd, EPOLL_CTL_ADD, commandEventFd, &event +) == -1) { +LOGE("Failed to add command eventfd to epoll: %s", +strerror(errno) +); +close(epollFd); +close(commandEventFd); +return; +} - AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); - const ::ndk::SpAIBinder spBinder(callbackAIBinder); - std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); +AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); +const ::ndk::SpAIBinder spBinder(callbackAIBinder); +std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); - struct epoll_event events[MAX_EPOLL_EVENTS]; - bool running = true; +struct epoll_event events[MAX_EPOLL_EVENTS]; +bool running = true; - LOGI("Start evdev event loop"); +LOGI("Start evdev event loop"); callback-> onEvdevEventLoopStarted(); - while (running) { - int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); - - for (int i = 0; i < n; ++i) { - if (events[i].data.fd == commandEventFd) { - uint64_t val; - ssize_t s = read(commandEventFd, &val, sizeof(val)); - if (s < 0) { - LOGE("Error reading from command event fd: %s", strerror(errno)); - } - - std::lock_guard lock(commandMutex); - while (!commandQueue.empty()) { - Command cmd = commandQueue.front(); - commandQueue.pop(); - if (cmd.type == STOP) { - running = false; - break; - } - handleCommand(cmd); - } - } else { - std::lock_guard lock(evdevDevicesMutex); - DeviceContext *dc = &evdevDevices->at(events[i].data.fd); - onEpollEvent(dc, callback.get()); - } - } - } +while (running) { +int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); + +for ( +int i = 0; +i lock(evdevDevicesMutex); +std::lock_guard lock(commandMutex); +while (!commandQueue. - for (auto const &[fd, dc]: *evdevDevices) { - libevdev_grab(dc.evdev, LIBEVDEV_UNGRAB); - libevdev_free(dc.evdev); - } +empty() + +) { +Command cmd = commandQueue.front(); +commandQueue. + +pop(); + +if (cmd.type == STOP) { +running = false; +break; +} +handleCommand(cmd); +} +} else { +std::lock_guard lock(evdevDevicesMutex); +DeviceContext *dc = &evdevDevices->at(events[i].data.fd); +onEpollEvent(dc, callback +. + +get() + +); +} +} +} + +// Cleanup +std::lock_guard lock(evdevDevicesMutex); + +for (auto const &[fd, dc]: *evdevDevices) { +libevdev_grab(dc +.evdev, LIBEVDEV_UNGRAB); +libevdev_free(dc +.evdev); +} + +evdevDevices-> + +clear(); - evdevDevices->clear(); - close(commandEventFd); - close(epollFd); +close(commandEventFd); +close(epollFd); } void ungrabDevice(int deviceId) { @@ -388,7 +426,7 @@ void ungrabDevice(int deviceId) { cmd.type = UNGRAB; cmd.data = UngrabData{deviceId}; - std::lock_guard lock(commandMutex); + std::lock_guard lock(commandMutex); commandQueue.push(cmd); // Notify the event loop @@ -401,56 +439,75 @@ void ungrabDevice(int deviceId) { extern "C" JNIEXPORT void JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, - jobject thiz, - jint device_id) { - ungrabDevice(device_id); +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv +*env, +jobject thiz, + jint +device_id) { +ungrabDevice(device_id); } extern "C" JNIEXPORT void JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv *env, - jobject thiz) { - Command cmd = {STOP}; - - std::lock_guard lock(commandMutex); - commandQueue.push(cmd); - - // Notify the event loop - uint64_t val = 1; - ssize_t written = write(commandEventFd, &val, sizeof(val)); - if (written < 0) { - LOGE("Failed to write to commandEventFd: %s", strerror(errno)); - } +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv +*env, +jobject thiz +) { +Command cmd = {STOP}; + +std::lock_guard lock(commandMutex); +commandQueue. +push(cmd); + +// Notify the event loop +uint64_t val = 1; +ssize_t written = write(commandEventFd, &val, sizeof(val)); +if (written < 0) { +LOGE("Failed to write to commandEventFd: %s", +strerror(errno) +); +} } extern "C" -JNIEXPORT jboolean JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNative(JNIEnv *env, - jobject thiz, - jint device_id, - jint type, - jint code, - jint value) { - // TODO: implement writeEvdevEvent() +JNIEXPORT jboolean +JNICALL + Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNative(JNIEnv * env, + jobject +thiz, +jint device_id, + jint +type, +jint code, + jint +value) { +// TODO: implement writeEvdevEvent() } extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDevicesNative( - JNIEnv *env, - jobject thiz) { - std::vector deviceIds; + JNIEnv +*env, +jobject thiz +) { +std::vector deviceIds; - { - std::lock_guard evdevLock(evdevDevicesMutex); +{ +std::lock_guard evdevLock(evdevDevicesMutex); - for (auto pair: *evdevDevices) { - deviceIds.push_back(pair.second.deviceId); - } - } +for ( +auto pair +: *evdevDevices) { +deviceIds. +push_back(pair +.second.deviceId); +} +} - for (int id: deviceIds) { - ungrabDevice(id); - } +for ( +int id +: deviceIds) { +ungrabDevice(id); +} } \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt deleted file mode 100644 index c6005860c7..0000000000 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt +++ /dev/null @@ -1,30 +0,0 @@ -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) -} - -/** - * Create a KeyEvent instance that can be injected into the Android system. - */ -fun createKeyEvent( - 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, - ) -} \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 516d3bcf84..92f9b38519 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -20,7 +20,7 @@ data class KMKeyEvent( ) : KMInputEvent { companion object { - fun fromKeyEvent(keyEvent: KeyEvent): KMKeyEvent? { + fun fromAndroidKeyEvent(keyEvent: KeyEvent): KMKeyEvent? { val device = keyEvent.device ?: return null return KMKeyEvent( @@ -38,7 +38,7 @@ data class KMKeyEvent( override val deviceId: Int = device.id - fun toKeyEvent(): KeyEvent { + fun toAndroidKeyEvent(): KeyEvent { return KeyEvent( eventTime, eventTime, @@ -46,7 +46,7 @@ data class KMKeyEvent( keyCode, repeatCount, metaState, - device?.id ?: -1, + device.id, scanCode, source ) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt deleted file mode 100644 index 49a8fb937f..0000000000 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.sds100.keymapper.system.inputmethod - -import android.view.InputDevice -import io.github.sds100.keymapper.common.utils.InputEventType - -// TODO delete -data class InputKeyModel( - val keyCode: Int, - val inputType: InputEventType = InputEventType.DOWN_UP, - val metaState: Int = 0, - val deviceId: Int = 0, - val scanCode: Int = 0, - val repeat: Int = 0, - val source: Int = InputDevice.SOURCE_UNKNOWN, -) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt index f552ff3c8d..6db7203434 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt @@ -12,6 +12,8 @@ enum class Permission { CALL_PHONE, ROOT, IGNORE_BATTERY_OPTIMISATION, + + // TODO remove. Replace with System Bridge. SHIZUKU, ACCESS_FINE_LOCATION, ANSWER_PHONE_CALL, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt deleted file mode 100644 index 9679b8727c..0000000000 --- a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt +++ /dev/null @@ -1,58 +0,0 @@ -package io.github.sds100.keymapper.system.shizuku - -import android.annotation.SuppressLint -import android.content.Context -import android.hardware.input.IInputManager -import android.os.SystemClock -import android.view.KeyEvent -import io.github.sds100.keymapper.common.utils.InputEventType -import io.github.sds100.keymapper.system.inputevents.InputEventInjector -import io.github.sds100.keymapper.system.inputevents.createKeyEvent -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import rikka.shizuku.ShizukuBinderWrapper -import rikka.shizuku.SystemServiceHelper -import timber.log.Timber - -@SuppressLint("PrivateApi") -class ShizukuInputEventInjector : InputEventInjector { - - companion object { - // private const val INJECT_INPUT_EVENT_MODE_ASYNC = 0 - - private const val INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2 - } - - private val iInputManager: IInputManager by lazy { - val binder = - ShizukuBinderWrapper(SystemServiceHelper.getSystemService(Context.INPUT_SERVICE)) - IInputManager.Stub.asInterface(binder) - } - - override suspend fun inputKeyEvent(model: InputKeyModel) { - Timber.d("Inject input event with Shizuku ${KeyEvent.keyCodeToString(model.keyCode)}, $model") - - val action = when (model.inputType) { - InputEventType.DOWN, InputEventType.DOWN_UP -> KeyEvent.ACTION_DOWN - InputEventType.UP -> KeyEvent.ACTION_UP - } - - val eventTime = SystemClock.uptimeMillis() - - val keyEvent = createKeyEvent(eventTime, action, model) - - withContext(Dispatchers.IO) { - // MUST wait for the application to finish processing the event before sending the next one. - // Otherwise, rapidly repeating input events will go in a big queue and all inputs - // into the application will be delayed or overloaded. - iInputManager.injectInputEvent(keyEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH) - - if (model.inputType == InputEventType.DOWN_UP) { - val upEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_UP) - - iInputManager.injectInputEvent(upEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH) - } - } - } -} From 8fb2d0960b00821133a60b2d041b2764f20e7908 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 02:52:47 +0100 Subject: [PATCH 072/215] #1394 bump version to 4.0.0 --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 358135df61..7a4085f7e6 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=3.2.0 +VERSION_NAME=4.0.0 VERSION_CODE=128 VERSION_NUM=0 \ No newline at end of file From 6fc59204a4fbb8ee7a499e4ceca766ad33f0e70c Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 12:47:12 +0100 Subject: [PATCH 073/215] #1394 pass correct package name to sysbridge --- sysbridge/src/main/cpp/starter.cpp | 31 +++++++------------ .../sysbridge/service/SystemBridge.kt | 16 +++++++--- .../keymapper/sysbridge/starter/Starter.kt | 3 +- sysbridge/src/main/res/raw/start.sh | 3 +- .../java/android/ddm/DdmHandleAppName.java | 6 +++- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/sysbridge/src/main/cpp/starter.cpp b/sysbridge/src/main/cpp/starter.cpp index 025d3582a7..7bbad5f747 100644 --- a/sysbridge/src/main/cpp/starter.cpp +++ b/sysbridge/src/main/cpp/starter.cpp @@ -30,8 +30,6 @@ #define EXIT_FATAL_KILL 9 #define EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX 10 -// TODO take package name as argument -#define PACKAGE_NAME "io.github.sds100.keymapper.debug" #define SERVER_NAME "keymapper_sysbridge" #define SERVER_CLASS_PATH "io.github.sds100.keymapper.sysbridge.service.SystemBridge" @@ -46,7 +44,7 @@ #endif static void run_server(const char *apk_path, const char *lib_path, const char *main_class, - const char *process_name) { + const char *process_name, const char *package_name) { if (setenv("CLASSPATH", apk_path, true)) { LOGE("can't set CLASSPATH\n"); exit(EXIT_FATAL_SET_CLASSPATH); @@ -97,6 +95,7 @@ v_current = (uintptr_t) v + v_size - sizeof(char *); \ ARG_PUSH(argv, "/system/bin/app_process") ARG_PUSH_FMT(argv, "-Djava.class.path=%s", apk_path) ARG_PUSH_FMT(argv, "-Dkeymapper_sysbridge.library.path=%s", lib_path) + ARG_PUSH_FMT(argv, "-Dkeymapper_sysbridge.package=%s", package_name) ARG_PUSH_DEBUG_VM_PARAMS(argv) ARG_PUSH(argv, "/system/bin") ARG_PUSH_FMT(argv, "--nice-name=%s", process_name) @@ -112,11 +111,11 @@ v_current = (uintptr_t) v + v_size - sizeof(char *); \ } static void start_server(const char *apk_path, const char *lib_path, const char *main_class, - const char *process_name) { + const char *process_name, const char *package_name) { if (daemon(false, false) == 0) { LOGD("child"); - run_server(apk_path, lib_path, main_class, process_name); + run_server(apk_path, lib_path, main_class, process_name, package_name); } else { perrorf("fatal: can't fork\n"); exit(EXIT_FATAL_FORK); @@ -171,6 +170,7 @@ char *context = nullptr; int starter_main(int argc, char *argv[]) { char *apk_path = nullptr; char *lib_path = nullptr; + char *package_name = 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. @@ -179,10 +179,14 @@ int starter_main(int argc, char *argv[]) { apk_path = argv[i] + 6; } else if (strncmp(argv[i], "--lib=", 6) == 0) { lib_path = argv[i] + 6; + } else if (strncmp(argv[i], "--package=", 10) == 0) { + package_name = argv[i] + 10; } } printf("info: apk path = %s\n", apk_path); + printf("info: lib path = %s\n", lib_path); + printf("info: package name = %s\n", package_name); int uid = getuid(); if (uid != 0 && uid != 2000) { @@ -195,7 +199,7 @@ int starter_main(int argc, char *argv[]) { if (uid == 0) { chown("/data/local/tmp/keymapper_sysbridge_starter", 2000, 2000); se::setfilecon("/data/local/tmp/keymapper_sysbridge_starter", - "u:object_r:shell_data_file:s0"); + "u:object_r:shell_data_file:s0"); switch_cgroup(); int sdkLevel = 0; @@ -268,19 +272,6 @@ int starter_main(int argc, char *argv[]) { 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); @@ -301,7 +292,7 @@ int starter_main(int argc, char *argv[]) { printf("info: starting server...\n"); fflush(stdout); LOGD("start_server"); - start_server(apk_path, lib_path, SERVER_CLASS_PATH, SERVER_NAME); + start_server(apk_path, lib_path, SERVER_CLASS_PATH, SERVER_NAME, package_name); exit(EXIT_SUCCESS); } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 7cc42bef48..d5483cfb4e 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -36,6 +36,8 @@ import kotlin.system.exitProcess internal class SystemBridge : ISystemBridge.Stub() { // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. + // TODO every minute ping key mapper and if no response then stop the process. + // TODO if no response when sending to the callback, stop the process. // TODO return error code and map this to a SystemBridgeError in key mapper external fun grabEvdevDeviceNative( @@ -50,11 +52,13 @@ internal class SystemBridge : ISystemBridge.Stub() { external fun stopEvdevEventLoop() companion object { - private const val TAG: String = "SystemBridge" + private const val TAG: String = "KeyMapperSystemBridge" + private val packageName: String? = System.getProperty("keymapper_sysbridge.package") @JvmStatic fun main(args: Array) { - DdmHandleAppName.setAppName("keymapper_sysbridge", 0) + Log.i(TAG, "Sysbridge package name = $packageName") + DdmHandleAppName.setAppName("keymapper_sysbridge", packageName, 0) @Suppress("DEPRECATION") Looper.prepareMainLooper() SystemBridge() @@ -170,8 +174,11 @@ internal class SystemBridge : ISystemBridge.Stub() { } init { + val libraryPath = System.getProperty("keymapper_sysbridge.library.path") @SuppressLint("UnsafeDynamicallyLoadedCode") - System.load("${System.getProperty("keymapper_sysbridge.library.path")}/libevdev.so") + System.load("$libraryPath/libevdev.so") + + Log.i(TAG, "SystemBridge started") waitSystemService("package") @@ -202,8 +209,7 @@ internal class SystemBridge : ISystemBridge.Stub() { mainHandler.post { for (userId in UserManagerApis.getUserIdsNoThrow()) { - // TODO use correct package name - sendBinderToApp(this, "io.github.sds100.keymapper.debug", userId) + sendBinderToApp(this, packageName, userId) } } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt index 0596b00df9..fb34f48df4 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt @@ -48,8 +48,9 @@ internal object Starter { val sh = writeScript(context, File(dir, "start.sh"), starter) val apkPath = context.applicationInfo.sourceDir val libPath = context.applicationInfo.nativeLibraryDir + val packageName = context.applicationInfo.packageName - commandInternal[1] = "sh $sh --apk=$apkPath --lib=$libPath" + commandInternal[1] = "sh $sh --apk=$apkPath --lib=$libPath --package=$packageName" logd(commandInternal[1]!!) } diff --git a/sysbridge/src/main/res/raw/start.sh b/sysbridge/src/main/res/raw/start.sh index 9d58884e07..0f70844b0a 100644 --- a/sysbridge/src/main/res/raw/start.sh +++ b/sysbridge/src/main/res/raw/start.sh @@ -39,7 +39,8 @@ fi if [ -f $STARTER_PATH ]; then echo "info: exec $STARTER_PATH" - $STARTER_PATH "$1" "$2" + # Pass apk path, library path, and package name + $STARTER_PATH "$1" "$2" "$3" result=$? if [ ${result} -ne 0 ]; then echo "info: keymapper_sysbridge_starter exit with non-zero value $result" diff --git a/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java b/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java index d1b7c37864..c9594237c0 100644 --- a/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java +++ b/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java @@ -2,7 +2,11 @@ public class DdmHandleAppName { - public static void setAppName(String name, int userId) { + public static void setAppName(String appName, int userId) { + throw new RuntimeException("STUB"); + } + + public static void setAppName(String appName, String pkgName, int userId) { throw new RuntimeException("STUB"); } } From 59be6c94682b482589ca0f1bd00e784279897bb1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 12:52:34 +0100 Subject: [PATCH 074/215] #1394 libevdev_jni.cpp: reformat --- sysbridge/src/main/cpp/libevdev_jni.cpp | 335 ++++++++++-------------- 1 file changed, 140 insertions(+), 195 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 83bbc2f885..d1f0613a28 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -37,7 +37,7 @@ enum CommandType { struct Command { CommandType type; - std::variant data; + std::variant data; }; struct DeviceContext { @@ -52,7 +52,7 @@ void ungrabDevice(jint device_id); static int epollFd = -1; static int commandEventFd = -1; -static std::queue commandQueue; +static std::queue commandQueue; static std::mutex commandMutex; // This maps the file descriptor of an evdev device to its context. @@ -113,8 +113,8 @@ static int findEvdevDevice( devVendor != vendor || devProduct != product || // The hidden device bus field was only added to InputDevice.java in Android 14. - // So only check it if it is a real value - (bus != -1 && devBus != bus)) { + // So only check it if it is a real value + (bus != -1 && devBus != bus)) { libevdev_free(*outDev); close(fd); @@ -129,8 +129,8 @@ static int findEvdevDevice( closedir(dir); LOGE("Input device not found with name: %s, bus: %d, vendor: %d, product: %d", name.c_str(), - bus, - vendor, product); + bus, + vendor, product); return -1; } @@ -151,7 +151,7 @@ convertJInputDeviceIdentifier(JNIEnv *env, jobject jInputDeviceIdentifier) { deviceIdentifier.product = env->GetIntField(jInputDeviceIdentifier, productFieldId); jfieldID nameFieldId = env->GetFieldID(inputDeviceIdentifierClass, "name", - "Ljava/lang/String;"); + "Ljava/lang/String;"); auto nameString = (jstring) env->GetObjectField(jInputDeviceIdentifier, nameFieldId); const char *nameChars = env->GetStringUTFChars(nameString, nullptr); @@ -167,21 +167,19 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { } extern "C" -JNIEXPORT jboolean - -JNICALL +JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNative(JNIEnv *env, - jobject thiz, - jobject jInputDeviceIdentifier) { + jobject thiz, + jobject jInputDeviceIdentifier) { jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); jfieldID idFieldId = env->GetFieldID(inputDeviceIdentifierClass, "id", "I"); android::InputDeviceIdentifier identifier = convertJInputDeviceIdentifier(env, - jInputDeviceIdentifier); + jInputDeviceIdentifier); int deviceId = env->GetIntField(jInputDeviceIdentifier, idFieldId); Command cmd = {GRAB, GrabData{deviceId, identifier}}; - std::lock_guard lock(commandMutex); + std::lock_guard lock(commandMutex); commandQueue.push(cmd); uint64_t val = 1; @@ -209,12 +207,12 @@ int onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); callback->onEvdevEvent(deviceId, - inputEvent.time.tv_sec, - inputEvent.time.tv_usec, - inputEvent.type, - inputEvent.code, - inputEvent.value, - outKeycode); + inputEvent.time.tv_sec, + inputEvent.time.tv_usec, + inputEvent.type, + inputEvent.code, + inputEvent.value, + outKeycode); } if (rc == 1 || rc == 0 || rc == -EAGAIN) { @@ -234,10 +232,10 @@ void handleCommand(const Command &cmd) { struct libevdev *dev = nullptr; int rc = findEvdevDevice(data.identifier.name, - data.identifier.bus, - data.identifier.vendor, - data.identifier.product, - &dev); + data.identifier.bus, + data.identifier.vendor, + data.identifier.product, + &dev); if (rc < 0) { LOGE("Failed to find device for grab command"); return; @@ -246,7 +244,7 @@ void handleCommand(const Command &cmd) { rc = libevdev_grab(dev, LIBEVDEV_GRAB); if (rc < 0) { LOGE("Failed to grab evdev device %s: %s", - libevdev_get_name(dev), strerror(-rc)); + libevdev_get_name(dev), strerror(-rc)); libevdev_free(dev); return; } @@ -277,13 +275,13 @@ void handleCommand(const Command &cmd) { return; } - std::lock_guard lock(evdevDevicesMutex); + std::lock_guard lock(evdevDevicesMutex); evdevDevices->insert_or_assign(evdevFd, context); } else if (cmd.type == UNGRAB) { const UngrabData &data = std::get(cmd.data); - std::lock_guard lock(evdevDevicesMutex); + std::lock_guard lock(evdevDevicesMutex); for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { if (it->second.deviceId == data.deviceId) { int fd = it->first; @@ -299,124 +297,90 @@ void handleCommand(const Command &cmd) { extern "C" JNIEXPORT void JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv -*env, -jobject thiz, - jobject -jCallbackBinder) { -if (epollFd != -1 || commandEventFd != -1) { -LOGE("The evdev event loop has already started."); -return; -} - -epollFd = epoll_create1(EPOLL_CLOEXEC); -if (epollFd == -1) { -LOGE("Failed to create epoll fd: %s", -strerror(errno) -); -return; -} - -commandEventFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); -if (commandEventFd == -1) { -LOGE("Failed to create command eventfd: %s", -strerror(errno) -); -close(epollFd); -return; -} - -struct epoll_event event{}; -event. -events = EPOLLIN; -event.data. -fd = commandEventFd; -if ( -epoll_ctl(epollFd, EPOLL_CTL_ADD, commandEventFd, &event -) == -1) { -LOGE("Failed to add command eventfd to epoll: %s", -strerror(errno) -); -close(epollFd); -close(commandEventFd); -return; -} - -AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); -const ::ndk::SpAIBinder spBinder(callbackAIBinder); -std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); - -struct epoll_event events[MAX_EPOLL_EVENTS]; -bool running = true; - -LOGI("Start evdev event loop"); - -callback-> - -onEvdevEventLoopStarted(); - -while (running) { -int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); - -for ( -int i = 0; -i lock(commandMutex); -while (!commandQueue. - -empty() - -) { -Command cmd = commandQueue.front(); -commandQueue. - -pop(); - -if (cmd.type == STOP) { -running = false; -break; -} -handleCommand(cmd); -} -} else { -std::lock_guard lock(evdevDevicesMutex); -DeviceContext *dc = &evdevDevices->at(events[i].data.fd); -onEpollEvent(dc, callback -. +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, + jobject thiz, + jobject jCallbackBinder) { + if (epollFd != -1 || commandEventFd != -1) { + LOGE("The evdev event loop has already started."); + return; + } -get() + epollFd = epoll_create1(EPOLL_CLOEXEC); + if (epollFd == -1) { + LOGE("Failed to create epoll fd: %s", strerror(errno)); + return; + } -); -} -} -} + commandEventFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (commandEventFd == -1) { + LOGE("Failed to create command eventfd: %s", strerror(errno)); + close(epollFd); + return; + } -// Cleanup -std::lock_guard lock(evdevDevicesMutex); + struct epoll_event event{}; + event.events = EPOLLIN; + event.data.fd = commandEventFd; + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, commandEventFd, &event) == -1) { + LOGE("Failed to add command eventfd to epoll: %s", strerror(errno)); + close(epollFd); + close(commandEventFd); + return; + } -for (auto const &[fd, dc]: *evdevDevices) { -libevdev_grab(dc -.evdev, LIBEVDEV_UNGRAB); -libevdev_free(dc -.evdev); -} + AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); + const ::ndk::SpAIBinder spBinder(callbackAIBinder); + std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); + + struct epoll_event events[MAX_EPOLL_EVENTS]; + bool running = true; + + LOGI("Start evdev event loop"); + + callback-> + + onEvdevEventLoopStarted(); + + while (running) { + int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); + + for (int i = 0; i < n; ++i) { + if (events[i].data.fd == commandEventFd) { + uint64_t val; + ssize_t s = read(commandEventFd, &val, sizeof(val)); + if (s < 0) { + LOGE("Error reading from command event fd: %s", strerror(errno)); + } + + std::lock_guard lock(commandMutex); + while (!commandQueue.empty()) { + Command cmd = commandQueue.front(); + commandQueue.pop(); + if (cmd.type == STOP) { + running = false; + break; + } + handleCommand(cmd); + } + } else { + std::lock_guard lock(evdevDevicesMutex); + DeviceContext *dc = &evdevDevices->at(events[i].data.fd); + onEpollEvent(dc, callback.get()); + } + } + } -evdevDevices-> + // Cleanup + std::lock_guard lock(evdevDevicesMutex); -clear(); + for (auto const &[fd, dc]: *evdevDevices) { + libevdev_grab(dc.evdev, LIBEVDEV_UNGRAB); + libevdev_free(dc.evdev); + } -close(commandEventFd); -close(epollFd); + evdevDevices->clear(); + close(commandEventFd); + close(epollFd); } void ungrabDevice(int deviceId) { @@ -426,7 +390,7 @@ void ungrabDevice(int deviceId) { cmd.type = UNGRAB; cmd.data = UngrabData{deviceId}; - std::lock_guard lock(commandMutex); + std::lock_guard lock(commandMutex); commandQueue.push(cmd); // Notify the event loop @@ -439,75 +403,56 @@ void ungrabDevice(int deviceId) { extern "C" JNIEXPORT void JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv -*env, -jobject thiz, - jint -device_id) { -ungrabDevice(device_id); +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, + jobject thiz, + jint device_id) { + ungrabDevice(device_id); } extern "C" JNIEXPORT void JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv -*env, -jobject thiz -) { -Command cmd = {STOP}; - -std::lock_guard lock(commandMutex); -commandQueue. -push(cmd); - -// Notify the event loop -uint64_t val = 1; -ssize_t written = write(commandEventFd, &val, sizeof(val)); -if (written < 0) { -LOGE("Failed to write to commandEventFd: %s", -strerror(errno) -); -} +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv *env, + jobject thiz) { + Command cmd = {STOP}; + + std::lock_guard lock(commandMutex); + commandQueue.push(cmd); + + // Notify the event loop + uint64_t val = 1; + ssize_t written = write(commandEventFd, &val, sizeof(val)); + if (written < 0) { + LOGE("Failed to write to commandEventFd: %s", strerror(errno)); + } } extern "C" -JNIEXPORT jboolean -JNICALL - Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNative(JNIEnv * env, - jobject -thiz, -jint device_id, - jint -type, -jint code, - jint -value) { -// TODO: implement writeEvdevEvent() +JNIEXPORT jboolean JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNative(JNIEnv *env, + jobject thiz, + jint device_id, + jint type, + jint code, + jint value) { + // TODO: implement writeEvdevEvent() } extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDevicesNative( - JNIEnv -*env, -jobject thiz -) { -std::vector deviceIds; + JNIEnv *env, + jobject thiz) { + std::vector deviceIds; -{ -std::lock_guard evdevLock(evdevDevicesMutex); + { + std::lock_guard evdevLock(evdevDevicesMutex); -for ( -auto pair -: *evdevDevices) { -deviceIds. -push_back(pair -.second.deviceId); -} -} + for (auto pair: *evdevDevices) { + deviceIds.push_back(pair.second.deviceId); + } + } -for ( -int id -: deviceIds) { -ungrabDevice(id); -} + for (int id: deviceIds) { + ungrabDevice(id); + } } \ No newline at end of file From 9f56d802b4d62dc3887a201c961fd723de6af9ad Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 13:27:06 +0100 Subject: [PATCH 075/215] #1394 create a event loop for injecting key events asynchronously --- .../base/actions/PerformActionsUseCase.kt | 19 +--- .../keymapper/base/input/InputEventHub.kt | 88 +++++++++++++++---- .../keymaps/detection/DetectKeyMapsUseCase.kt | 6 +- .../base/system/navigation/OpenMenuHelper.kt | 4 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 2 + .../sysbridge/service/SystemBridge.kt | 17 +--- .../java/android/ddm/DdmHandleAppName.java | 4 - 7 files changed, 81 insertions(+), 59 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 3c486169af..59d6fce1bf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -168,10 +168,10 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( ) if (inputEventAction == InputEventAction.DOWN_UP) { - result = - injectKeyEvent(model).then { injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } + result = inputEventHub.injectKeyEvent(model) + .then { inputEventHub.injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } } else { - result = injectKeyEvent(model) + result = inputEventHub.injectKeyEvent(model) } } @@ -869,19 +869,6 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( result.showErrorMessageOnFail() } - private fun injectKeyEvent(model: InjectKeyEventModel): KMResult { - return when { - inputEventHub.isSystemBridgeConnected() -> { - return inputEventHub.injectKeyEvent(model) - } - - else -> { - keyMapperImeMessenger.inputKeyEvent(model) - Success(Unit) - } - } - } - override fun getErrorSnapshot(): ActionErrorSnapshot { return getActionErrorUseCase.actionErrorSnapshot.firstBlocking() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 6835974153..0a215852ca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -24,10 +24,15 @@ import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -51,6 +56,9 @@ class InputEventHubImpl @Inject constructor( private var systemBridge: ISystemBridge? = null + // Event queue for processing key events asynchronously in order + private val keyEventQueue = Channel(capacity = 100) + private val systemBridgeConnection: SystemBridgeConnection = object : SystemBridgeConnection { override fun onServiceConnected(service: ISystemBridge) { Timber.i("InputEventHub connected to SystemBridge") @@ -87,6 +95,23 @@ class InputEventHubImpl @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { systemBridgeManager.registerConnection(systemBridgeConnection) } + + startKeyEventProcessingLoop() + } + + /** + * Starts a coroutine that processes key events from the queue in order on the IO thread.= + **/ + private fun startKeyEventProcessingLoop() { + coroutineScope.launch(Dispatchers.IO) { + for (event in keyEventQueue) { + try { + injectKeyEvent(event) + } catch (e: Exception) { + Timber.e(e, "Error processing key event: $event") + } + } + } } override fun isSystemBridgeConnected(): Boolean { @@ -123,17 +148,19 @@ class InputEventHubImpl @Inject constructor( if (logInputEventsEnabled.value) { Timber.d("Passthrough key event from evdev: ${keyEvent.keyCode}") } - injectKeyEvent( - InjectKeyEventModel( - keyCode = keyEvent.keyCode, - action = keyEvent.action, - metaState = keyEvent.metaState, - deviceId = keyEvent.deviceId, - scanCode = keyEvent.scanCode, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source + runBlocking { + injectKeyEvent( + InjectKeyEventModel( + keyCode = keyEvent.keyCode, + action = keyEvent.action, + metaState = keyEvent.metaState, + deviceId = keyEvent.deviceId, + scanCode = keyEvent.scanCode, + repeatCount = keyEvent.repeatCount, + source = keyEvent.source + ) ) - ) + } } } else { onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) @@ -222,11 +249,11 @@ class InputEventHubImpl @Inject constructor( is KMKeyEvent -> { when (event.action) { KeyEvent.ACTION_DOWN -> { - Timber.d("Key down: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.device?.id}, metaState=${event.metaState}, source=${event.source}") + Timber.d("Key down ${KeyEvent.keyCodeToString(event.keyCode)}: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.device?.id}, metaState=${event.metaState}, source=${event.source}") } KeyEvent.ACTION_UP -> { - Timber.d("Key up: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.device?.id}, metaState=${event.metaState}, source=${event.source}") + Timber.d("Key up ${KeyEvent.keyCodeToString(event.keyCode)}: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.device?.id}, metaState=${event.metaState}, source=${event.source}") } else -> { @@ -307,7 +334,7 @@ class InputEventHubImpl @Inject constructor( } } - override fun injectKeyEvent(event: InjectKeyEventModel): KMResult { + override suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult { val systemBridge = this.systemBridge if (systemBridge == null) { @@ -315,16 +342,32 @@ class InputEventHubImpl @Inject constructor( return Success(true) } else { try { - return systemBridge.injectInputEvent( - event.toAndroidKeyEvent(), - INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH - ).success() + Timber.d("Injecting input event ${event.keyCode} with system bridge") + return withContext(Dispatchers.IO) { + systemBridge.injectInputEvent( + event.toAndroidKeyEvent(), + INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH + ).success() + } } catch (e: RemoteException) { return KMError.Exception(e) } } } + override fun injectKeyEventAsync(event: InjectKeyEventModel): KMResult { + return try { + // Send the event to the queue for processing + if (keyEventQueue.trySend(event).isSuccess) { + Success(Unit) + } else { + KMError.Exception(Exception("Failed to queue key event")) + } + } catch (e: Exception) { + KMError.Exception(e) + } + } + private data class ClientContext( val callback: InputEventHubCallback, /** @@ -356,8 +399,17 @@ interface InputEventHub { /** * Inject a key event. This may either use the key event relay service or the system * bridge depending on the permissions granted to Key Mapper. + * + * Must be suspend so injecting to the systembridge can happen on another thread. + */ + suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult + + /** + * Some callers don't care about the result from injecting and it isn't critical + * for it to be synchronous so calls to this + * will be put in a queue and processed. */ - fun injectKeyEvent(event: InjectKeyEventModel): KMResult + fun injectKeyEventAsync(event: InjectKeyEventModel): KMResult fun injectEvdevEvent(event: KMEvdevEvent): KMResult diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt index b7d26cb061..0bb074039e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt @@ -27,7 +27,6 @@ import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.popup.ToastAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.vibrator.VibratorAdapter @@ -50,7 +49,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( private val suAdapter: SuAdapter, private val volumeAdapter: VolumeAdapter, private val toastAdapter: ToastAdapter, - private val permissionAdapter: PermissionAdapter, private val resourceProvider: ResourceProvider, private val vibrator: VibratorAdapter, @Assisted @@ -205,7 +203,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( if (inputEventHub.isSystemBridgeConnected()) { Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)} with system bridge, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") - inputEventHub.injectKeyEvent(model) + inputEventHub.injectKeyEventAsync(model) } else { Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)}, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") @@ -222,7 +220,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( KeyEvent.KEYCODE_MENU -> openMenuHelper.openMenu() - else -> inputEventHub.injectKeyEvent(model) + else -> inputEventHub.injectKeyEventAsync(model) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt index 401c85948d..58e4f2663d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt @@ -35,8 +35,8 @@ class OpenMenuHelper( val upEvent = downEvent.copy(action = KeyEvent.ACTION_UP) - return inputEventHub.injectKeyEvent(downEvent).then { - inputEventHub.injectKeyEvent(upEvent) + return inputEventHub.injectKeyEventAsync(downEvent).then { + inputEventHub.injectKeyEventAsync(upEvent) } } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index d1f0613a28..47dc5e558e 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -381,6 +381,8 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo evdevDevices->clear(); close(commandEventFd); close(epollFd); + + LOGI("Stopped evdev event loop"); } void ungrabDevice(int deviceId) { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index d5483cfb4e..209cb03acb 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -58,7 +58,7 @@ internal class SystemBridge : ISystemBridge.Stub() { @JvmStatic fun main(args: Array) { Log.i(TAG, "Sysbridge package name = $packageName") - DdmHandleAppName.setAppName("keymapper_sysbridge", packageName, 0) + DdmHandleAppName.setAppName("keymapper_sysbridge", 0) @Suppress("DEPRECATION") Looper.prepareMainLooper() SystemBridge() @@ -214,8 +214,6 @@ internal class SystemBridge : ISystemBridge.Stub() { } } - // TODO ungrab all evdev devices - // TODO ungrab all evdev devices if no key mapper app is bound to the service override fun destroy() { Log.i(TAG, "SystemBridge destroyed") @@ -258,18 +256,7 @@ internal class SystemBridge : ISystemBridge.Stub() { deviceId: Int, ): Boolean { // Can not filter touchscreens because the volume and power buttons in the emulator come through touchscreen devices. - -// val inputDevice = inputManager.getInputDevice(deviceId); -// -// if (inputDevice == null) { -// return false; -// } -// -// if (inputDevice.supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) { -// Log.e(TAG, "Key Mapper does not permit touchscreens to be grabbed") -// return false; -// } - + // Perhaps this will also happen on other real devices. return grabEvdevDeviceNative(buildInputDeviceIdentifier(deviceId)) } diff --git a/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java b/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java index c9594237c0..ac4009959f 100644 --- a/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java +++ b/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java @@ -5,8 +5,4 @@ public class DdmHandleAppName { public static void setAppName(String appName, int userId) { throw new RuntimeException("STUB"); } - - public static void setAppName(String appName, String pkgName, int userId) { - throw new RuntimeException("STUB"); - } } From e6164fdbd2cb26a1b5d10eecccf7d1477f8c559a Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 13:27:06 +0100 Subject: [PATCH 076/215] #1394 create a event loop for injecting key events asynchronously --- .../base/actions/PerformActionsUseCase.kt | 19 +--- .../keymapper/base/input/InputEventHub.kt | 90 +++++++++++++++---- .../keymaps/detection/DetectKeyMapsUseCase.kt | 6 +- .../base/system/navigation/OpenMenuHelper.kt | 4 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 2 + .../sysbridge/service/SystemBridge.kt | 17 +--- .../java/android/ddm/DdmHandleAppName.java | 4 - 7 files changed, 82 insertions(+), 60 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 3c486169af..59d6fce1bf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -168,10 +168,10 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( ) if (inputEventAction == InputEventAction.DOWN_UP) { - result = - injectKeyEvent(model).then { injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } + result = inputEventHub.injectKeyEvent(model) + .then { inputEventHub.injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } } else { - result = injectKeyEvent(model) + result = inputEventHub.injectKeyEvent(model) } } @@ -869,19 +869,6 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( result.showErrorMessageOnFail() } - private fun injectKeyEvent(model: InjectKeyEventModel): KMResult { - return when { - inputEventHub.isSystemBridgeConnected() -> { - return inputEventHub.injectKeyEvent(model) - } - - else -> { - keyMapperImeMessenger.inputKeyEvent(model) - Success(Unit) - } - } - } - override fun getErrorSnapshot(): ActionErrorSnapshot { return getActionErrorUseCase.actionErrorSnapshot.firstBlocking() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 6835974153..af69ee1478 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -24,10 +24,15 @@ import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -51,6 +56,9 @@ class InputEventHubImpl @Inject constructor( private var systemBridge: ISystemBridge? = null + // Event queue for processing key events asynchronously in order + private val keyEventQueue = Channel(capacity = 100) + private val systemBridgeConnection: SystemBridgeConnection = object : SystemBridgeConnection { override fun onServiceConnected(service: ISystemBridge) { Timber.i("InputEventHub connected to SystemBridge") @@ -87,6 +95,23 @@ class InputEventHubImpl @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { systemBridgeManager.registerConnection(systemBridgeConnection) } + + startKeyEventProcessingLoop() + } + + /** + * Starts a coroutine that processes key events from the queue in order on the IO thread.= + **/ + private fun startKeyEventProcessingLoop() { + coroutineScope.launch(Dispatchers.IO) { + for (event in keyEventQueue) { + try { + injectKeyEvent(event) + } catch (e: Exception) { + Timber.e(e, "Error processing key event: $event") + } + } + } } override fun isSystemBridgeConnected(): Boolean { @@ -123,17 +148,19 @@ class InputEventHubImpl @Inject constructor( if (logInputEventsEnabled.value) { Timber.d("Passthrough key event from evdev: ${keyEvent.keyCode}") } - injectKeyEvent( - InjectKeyEventModel( - keyCode = keyEvent.keyCode, - action = keyEvent.action, - metaState = keyEvent.metaState, - deviceId = keyEvent.deviceId, - scanCode = keyEvent.scanCode, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source + runBlocking { + injectKeyEvent( + InjectKeyEventModel( + keyCode = keyEvent.keyCode, + action = keyEvent.action, + metaState = keyEvent.metaState, + deviceId = keyEvent.deviceId, + scanCode = keyEvent.scanCode, + repeatCount = keyEvent.repeatCount, + source = keyEvent.source + ) ) - ) + } } } else { onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) @@ -215,18 +242,18 @@ class InputEventHubImpl @Inject constructor( is KMGamePadEvent -> { Timber.d( - "GamePad event: deviceId=${event.device?.id}, axisHatX=${event.axisHatX}, axisHatY=${event.axisHatY}" + "GamePad event: deviceId=${event.deviceId}, axisHatX=${event.axisHatX}, axisHatY=${event.axisHatY}" ) } is KMKeyEvent -> { when (event.action) { KeyEvent.ACTION_DOWN -> { - Timber.d("Key down: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.device?.id}, metaState=${event.metaState}, source=${event.source}") + Timber.d("Key down ${KeyEvent.keyCodeToString(event.keyCode)}: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.deviceId}, metaState=${event.metaState}, source=${event.source}") } KeyEvent.ACTION_UP -> { - Timber.d("Key up: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.device?.id}, metaState=${event.metaState}, source=${event.source}") + Timber.d("Key up ${KeyEvent.keyCodeToString(event.keyCode)}: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.deviceId}, metaState=${event.metaState}, source=${event.source}") } else -> { @@ -307,7 +334,7 @@ class InputEventHubImpl @Inject constructor( } } - override fun injectKeyEvent(event: InjectKeyEventModel): KMResult { + override suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult { val systemBridge = this.systemBridge if (systemBridge == null) { @@ -315,16 +342,32 @@ class InputEventHubImpl @Inject constructor( return Success(true) } else { try { - return systemBridge.injectInputEvent( - event.toAndroidKeyEvent(), - INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH - ).success() + Timber.d("Injecting input event ${event.keyCode} with system bridge") + return withContext(Dispatchers.IO) { + systemBridge.injectInputEvent( + event.toAndroidKeyEvent(), + INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH + ).success() + } } catch (e: RemoteException) { return KMError.Exception(e) } } } + override fun injectKeyEventAsync(event: InjectKeyEventModel): KMResult { + return try { + // Send the event to the queue for processing + if (keyEventQueue.trySend(event).isSuccess) { + Success(Unit) + } else { + KMError.Exception(Exception("Failed to queue key event")) + } + } catch (e: Exception) { + KMError.Exception(e) + } + } + private data class ClientContext( val callback: InputEventHubCallback, /** @@ -356,8 +399,17 @@ interface InputEventHub { /** * Inject a key event. This may either use the key event relay service or the system * bridge depending on the permissions granted to Key Mapper. + * + * Must be suspend so injecting to the systembridge can happen on another thread. + */ + suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult + + /** + * Some callers don't care about the result from injecting and it isn't critical + * for it to be synchronous so calls to this + * will be put in a queue and processed. */ - fun injectKeyEvent(event: InjectKeyEventModel): KMResult + fun injectKeyEventAsync(event: InjectKeyEventModel): KMResult fun injectEvdevEvent(event: KMEvdevEvent): KMResult diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt index b7d26cb061..0bb074039e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt @@ -27,7 +27,6 @@ import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.popup.ToastAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.vibrator.VibratorAdapter @@ -50,7 +49,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( private val suAdapter: SuAdapter, private val volumeAdapter: VolumeAdapter, private val toastAdapter: ToastAdapter, - private val permissionAdapter: PermissionAdapter, private val resourceProvider: ResourceProvider, private val vibrator: VibratorAdapter, @Assisted @@ -205,7 +203,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( if (inputEventHub.isSystemBridgeConnected()) { Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)} with system bridge, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") - inputEventHub.injectKeyEvent(model) + inputEventHub.injectKeyEventAsync(model) } else { Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)}, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") @@ -222,7 +220,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( KeyEvent.KEYCODE_MENU -> openMenuHelper.openMenu() - else -> inputEventHub.injectKeyEvent(model) + else -> inputEventHub.injectKeyEventAsync(model) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt index 401c85948d..58e4f2663d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt @@ -35,8 +35,8 @@ class OpenMenuHelper( val upEvent = downEvent.copy(action = KeyEvent.ACTION_UP) - return inputEventHub.injectKeyEvent(downEvent).then { - inputEventHub.injectKeyEvent(upEvent) + return inputEventHub.injectKeyEventAsync(downEvent).then { + inputEventHub.injectKeyEventAsync(upEvent) } } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index d1f0613a28..47dc5e558e 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -381,6 +381,8 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo evdevDevices->clear(); close(commandEventFd); close(epollFd); + + LOGI("Stopped evdev event loop"); } void ungrabDevice(int deviceId) { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index d5483cfb4e..209cb03acb 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -58,7 +58,7 @@ internal class SystemBridge : ISystemBridge.Stub() { @JvmStatic fun main(args: Array) { Log.i(TAG, "Sysbridge package name = $packageName") - DdmHandleAppName.setAppName("keymapper_sysbridge", packageName, 0) + DdmHandleAppName.setAppName("keymapper_sysbridge", 0) @Suppress("DEPRECATION") Looper.prepareMainLooper() SystemBridge() @@ -214,8 +214,6 @@ internal class SystemBridge : ISystemBridge.Stub() { } } - // TODO ungrab all evdev devices - // TODO ungrab all evdev devices if no key mapper app is bound to the service override fun destroy() { Log.i(TAG, "SystemBridge destroyed") @@ -258,18 +256,7 @@ internal class SystemBridge : ISystemBridge.Stub() { deviceId: Int, ): Boolean { // Can not filter touchscreens because the volume and power buttons in the emulator come through touchscreen devices. - -// val inputDevice = inputManager.getInputDevice(deviceId); -// -// if (inputDevice == null) { -// return false; -// } -// -// if (inputDevice.supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) { -// Log.e(TAG, "Key Mapper does not permit touchscreens to be grabbed") -// return false; -// } - + // Perhaps this will also happen on other real devices. return grabEvdevDeviceNative(buildInputDeviceIdentifier(deviceId)) } diff --git a/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java b/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java index c9594237c0..ac4009959f 100644 --- a/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java +++ b/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java @@ -5,8 +5,4 @@ public class DdmHandleAppName { public static void setAppName(String appName, int userId) { throw new RuntimeException("STUB"); } - - public static void setAppName(String appName, String pkgName, int userId) { - throw new RuntimeException("STUB"); - } } From 5b344311864b678b4329fe49c2fe4645f641ea25 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 14:22:20 +0100 Subject: [PATCH 077/215] #1394 bunch of tweaks to SystemBridge --- .../keymapper/base/input/InjectKeyEventModel.kt | 5 +++-- .../sds100/keymapper/base/input/InputEventHub.kt | 11 +++++++++-- sysbridge/src/main/cpp/libevdev_jni.cpp | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt index 59094965a9..3875fa5ab6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt @@ -12,7 +12,7 @@ data class InjectKeyEventModel( val source: Int, val repeatCount: Int = 0 ) { - fun toAndroidKeyEvent(): KeyEvent { + fun toAndroidKeyEvent(flags: Int = 0): KeyEvent { val eventTime = SystemClock.uptimeMillis() return KeyEvent( eventTime, @@ -23,7 +23,8 @@ data class InjectKeyEventModel( metaState, deviceId, scanCode, - source + flags, // flags + source, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index af69ee1478..e6b4b3ff2e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -342,10 +342,17 @@ class InputEventHubImpl @Inject constructor( return Success(true) } else { try { - Timber.d("Injecting input event ${event.keyCode} with system bridge") + val androidKeyEvent = event.toAndroidKeyEvent(flags = KeyEvent.FLAG_FROM_SYSTEM) + + if (logInputEventsEnabled.value) { + Timber.d("Injecting key event $androidKeyEvent with system bridge") + } + return withContext(Dispatchers.IO) { + // All injected events have their device id set to -1 (VIRTUAL_KEYBOARD_ID) + // in InputDispatcher.cpp injectInputEvent. systemBridge.injectInputEvent( - event.toAndroidKeyEvent(), + androidKeyEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH ).success() } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 47dc5e558e..5a53775014 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -87,7 +87,7 @@ static int findEvdevDevice( // Check if it's a character device (input device) struct stat st{}; - LOGD("Found input device: %s", fullPath); +// LOGD("Found input device: %s", fullPath); // Try to open the device to see if it's accessible int fd = open(fullPath, O_RDONLY); From 9993945a5d407d46997c596123aab8aada4fa90b Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 16:59:54 +0100 Subject: [PATCH 078/215] #1394 ignore touch screen key events --- .../base/input/EvdevKeyEventTracker.kt | 10 ++++++ .../base/trigger/RecordTriggerController.kt | 33 +++++++++++++++++-- .../keymapper/base/utils/InputEventStrings.kt | 2 +- .../sysbridge/service/SystemBridge.kt | 3 ++ .../system/inputevents/KMKeyEvent.kt | 14 -------- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt index 1a86d320ef..2443d4a85d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt @@ -14,6 +14,12 @@ class EvdevKeyEventTracker( private val inputDeviceCache: InputDeviceCache ) { + companion object { + private val IGNORED_CODES: Set = setOf( + 330 // BTN_TOUCH. This is sent when you sometimes tap the touch screen. + ) + } + fun toKeyEvent(event: KMEvdevEvent): KMKeyEvent? { if (!event.isKeyEvent) { return null @@ -23,6 +29,10 @@ class EvdevKeyEventTracker( return null } + if (IGNORED_CODES.contains(event.code)) { + return null + } + val action = when (event.value) { 0 -> KeyEvent.ACTION_UP 1 -> KeyEvent.ACTION_DOWN diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 0a91f09a95..a2b5bc7de6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -57,6 +57,11 @@ class RecordTriggerControllerImpl @Inject constructor( private val recordedKeys: MutableList = mutableListOf() override val onRecordKey: MutableSharedFlow = MutableSharedFlow() + /** + * The keys that are currently being held down. They are removed from the set when the up event + * is received and it is cleared when recording starts. + */ + private val downKeyEvents: MutableSet = mutableSetOf() private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() override fun onInputEvent( @@ -92,10 +97,30 @@ class RecordTriggerControllerImpl @Inject constructor( } is KMKeyEvent -> { + val matchingDownEvent: KMKeyEvent? = downKeyEvents.find { + it.keyCode == event.keyCode && + it.scanCode == event.scanCode && + it.deviceId == event.deviceId && + it.source == event.source + } + + // Must also remove old down events if a new down even is received. + if (matchingDownEvent != null) { + downKeyEvents.remove(matchingDownEvent) + } + if (event.action == KeyEvent.ACTION_DOWN) { - val recordedKey = - createRecordedKey(event, detectionSource) - onRecordKey(recordedKey) + downKeyEvents.add(event) + } else if (event.action == KeyEvent.ACTION_UP) { + + // Only record the key if there is a matching down event. + // Do not do this when recording motion events from the input method + // or Activity because they intentionally only input a down event. + if (matchingDownEvent != null) { + val recordedKey = createRecordedKey(event, detectionSource) + onRecordKey(recordedKey) + } + } return true } @@ -185,6 +210,7 @@ class RecordTriggerControllerImpl @Inject constructor( private fun recordTriggerJob(): Job = coroutineScope.launch(Dispatchers.Default) { recordedKeys.clear() dpadMotionEventTracker.reset() + downKeyEvents.clear() inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this@RecordTriggerControllerImpl) @@ -204,6 +230,7 @@ class RecordTriggerControllerImpl @Inject constructor( delay(1000) } + downKeyEvents.clear() dpadMotionEventTracker.reset() inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) state.update { RecordTriggerState.Completed(recordedKeys) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt index ce7fe9751a..54a34737a1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt @@ -362,7 +362,7 @@ object InputEventStrings { KeyEvent.KEYCODE_MACRO_3 to "Macro 3", KeyEvent.KEYCODE_MACRO_4 to "Macro 4", KeyEvent.KEYCODE_EMOJI_PICKER to "Emoji Picker", - KeyEvent.KEYCODE_SCREENSHOT to "Screenshot" + KeyEvent.KEYCODE_SCREENSHOT to "Screenshot", ) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 209cb03acb..2e21eb27ca 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -257,6 +257,9 @@ internal class SystemBridge : ISystemBridge.Stub() { ): Boolean { // Can not filter touchscreens because the volume and power buttons in the emulator come through touchscreen devices. // Perhaps this will also happen on other real devices. + + // TODO whenever grabbing a touchscreen device, duplicate the input device + return grabEvdevDeviceNative(buildInputDeviceIdentifier(deviceId)) } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 92f9b38519..246d52c348 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -37,18 +37,4 @@ data class KMKeyEvent( } override val deviceId: Int = device.id - - fun toAndroidKeyEvent(): KeyEvent { - return KeyEvent( - eventTime, - eventTime, - action, - keyCode, - repeatCount, - metaState, - device.id, - scanCode, - source - ) - } } From 8835dfb034199d542d5d41d710fe280ada257394 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 19:45:31 +0100 Subject: [PATCH 079/215] #1394 fix: loop over all pending evdev events so that mice work correctly --- .../sds100/keymapper/base/BaseMainActivity.kt | 7 + .../keymapper/base/input/InputEventHub.kt | 53 +++--- .../base/trigger/RecordTriggerController.kt | 5 +- .../keymapper/sysbridge/IEvdevCallback.aidl | 2 +- .../keymapper/sysbridge/BnEvdevCallback.h | 4 +- .../keymapper/sysbridge/BpEvdevCallback.h | 2 +- .../keymapper/sysbridge/IEvdevCallback.cpp | 15 +- .../keymapper/sysbridge/IEvdevCallback.h | 4 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 179 ++++++++++++------ .../sysbridge/service/SystemBridge.kt | 27 ++- .../system/devices/AndroidDevicesAdapter.kt | 21 +- 11 files changed, 211 insertions(+), 108 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index a85a600022..efd10f0769 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -34,6 +34,8 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter +import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl @@ -90,6 +92,9 @@ abstract class BaseMainActivity : AppCompatActivity() { @Inject lateinit var suAdapter: SuAdapterImpl + @Inject + lateinit var devicesAdapter: AndroidDevicesAdapter + private lateinit var requestPermissionDelegate: RequestPermissionDelegate private val currentNightMode: Int @@ -188,6 +193,8 @@ abstract class BaseMainActivity : AppCompatActivity() { ContextCompat.RECEIVER_EXPORTED, ) } + + devicesAdapter.logConnectedInputDevices() } override fun onResume() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index e6b4b3ff2e..ba7a457927 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.ConcurrentHashMap @@ -131,7 +130,7 @@ class InputEventHubImpl @Inject constructor( code: Int, value: Int, androidCode: Int - ) { + ): Boolean { val evdevEvent = KMEvdevEvent(deviceId, type, code, value, androidCode, timeSec, timeUsec) if (logInputEventsEnabled.value) { @@ -141,29 +140,29 @@ class InputEventHubImpl @Inject constructor( val keyEvent: KMKeyEvent? = evdevKeyEventTracker.toKeyEvent(evdevEvent) if (keyEvent != null) { - val consumed = onInputEvent(keyEvent, InputEventDetectionSource.EVDEV) + return onInputEvent(keyEvent, InputEventDetectionSource.EVDEV) // Passthrough the key event if it is not consumed. - if (!consumed) { - if (logInputEventsEnabled.value) { - Timber.d("Passthrough key event from evdev: ${keyEvent.keyCode}") - } - runBlocking { - injectKeyEvent( - InjectKeyEventModel( - keyCode = keyEvent.keyCode, - action = keyEvent.action, - metaState = keyEvent.metaState, - deviceId = keyEvent.deviceId, - scanCode = keyEvent.scanCode, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source - ) - ) - } - } +// if (!consumed) { +// if (logInputEventsEnabled.value) { +// Timber.d("Passthrough key event from evdev: ${keyEvent.keyCode}") +// } +// runBlocking { +// injectKeyEvent( +// InjectKeyEventModel( +// keyCode = keyEvent.keyCode, +// action = keyEvent.action, +// metaState = keyEvent.metaState, +// deviceId = keyEvent.deviceId, +// scanCode = keyEvent.scanCode, +// repeatCount = keyEvent.repeatCount, +// source = keyEvent.source +// ) +// ) +// } +// } } else { - onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) + return onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) } } @@ -299,7 +298,13 @@ class InputEventHubImpl @Inject constructor( } try { - systemBridge?.ungrabAllEvdevDevices() + val ungrabResult = systemBridge?.ungrabAllEvdevDevices() + Timber.i("Ungrabbed all evdev devices: $ungrabResult") + + if (ungrabResult != true) { + Timber.e("Failed to ungrab all evdev devices before grabbing.") + return + } val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() ?: return @@ -311,7 +316,7 @@ class InputEventHubImpl @Inject constructor( Timber.i("Grabbed evdev device ${device.name}: $grabResult") } } catch (_: RemoteException) { - Timber.e("Failed to grab device. Is the system bridge dead?") + Timber.e("Failed to invalidate grabbed device. Is the system bridge dead?") } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index a2b5bc7de6..0d69cc1172 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -144,12 +144,13 @@ class RecordTriggerControllerImpl @Inject constructor( } override suspend fun stopRecording(): KMResult<*> { + recordingTriggerJob?.cancel() + recordingTriggerJob = null + dpadMotionEventTracker.reset() inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) state.update { RecordTriggerState.Completed(recordedKeys) } - recordingTriggerJob?.cancel() - recordingTriggerJob = null return Success(Unit) } diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl index 364d1c9386..75dfa75bae 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl @@ -2,5 +2,5 @@ package io.github.sds100.keymapper.sysbridge; interface IEvdevCallback { void onEvdevEventLoopStarted(); - void onEvdevEvent(int deviceId, long timeSec, long timeUsec, int type, int code, int value, int androidCode); + boolean onEvdevEvent(int deviceId, long timeSec, long timeUsec, int type, int code, int value, int androidCode); } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index 1271154377..523b40c0ff 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -37,8 +37,8 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { ::ndk::ScopedAStatus onEvdevEventLoopStarted() override { return _impl->onEvdevEventLoopStarted(); } - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override { - return _impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode); + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override { + return _impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); } protected: private: diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index c52a90dd9f..463820b1ce 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -20,7 +20,7 @@ class BpEvdevCallback : public ::ndk::BpCInterface { virtual ~BpEvdevCallback(); ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override; + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; }; } // namespace sysbridge } // namespace keymapper diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index fe0c2b0f76..e62851aa64 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -38,6 +38,7 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback int32_t in_code; int32_t in_value; int32_t in_androidCode; + bool _aidl_return; _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_deviceId); if (_aidl_ret_status != STATUS_OK) break; @@ -60,12 +61,15 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_androidCode); if (_aidl_ret_status != STATUS_OK) break; - ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode); + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, &_aidl_return); _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); if (_aidl_ret_status != STATUS_OK) break; if (!AStatus_isOk(_aidl_status.get())) break; + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_out, _aidl_return); + if (_aidl_ret_status != STATUS_OK) break; + break; } } @@ -111,7 +115,7 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { _aidl_status_return: return _aidl_status; } -::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) { +::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) { binder_status_t _aidl_ret_status = STATUS_OK; ::ndk::ScopedAStatus _aidl_status; ::ndk::ScopedAParcel _aidl_in; @@ -152,7 +156,7 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t #endif // BINDER_STABILITY_SUPPORT ); if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { - _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode); + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); goto _aidl_status_return; } if (_aidl_ret_status != STATUS_OK) goto _aidl_error; @@ -161,6 +165,9 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t if (_aidl_ret_status != STATUS_OK) goto _aidl_error; if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_out.get(), _aidl_return); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_error: _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); _aidl_status_return: @@ -230,7 +237,7 @@ ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEventLoopStarted() { _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; } -::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_deviceId*/, int64_t /*in_timeSec*/, int64_t /*in_timeUsec*/, int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/, int32_t /*in_androidCode*/) { +::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_deviceId*/, int64_t /*in_timeSec*/, int64_t /*in_timeUsec*/, int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/, int32_t /*in_androidCode*/, bool* /*_aidl_return*/) { ::ndk::ScopedAStatus _aidl_status; _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index c231b06037..e3cc7dcf53 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -38,14 +38,14 @@ class IEvdevCallback : public ::ndk::ICInterface { static bool setDefaultImpl(const std::shared_ptr& impl); static const std::shared_ptr& getDefaultImpl(); virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; - virtual ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) = 0; + virtual ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) = 0; private: static std::shared_ptr default_impl; }; class IEvdevCallbackDefault : public IEvdevCallback { public: ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode) override; + ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; }; diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 5a53775014..d7369318a6 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -44,6 +44,7 @@ struct DeviceContext { int deviceId; struct android::InputDeviceIdentifier inputDeviceIdentifier; struct libevdev *evdev; + struct libevdev_uinput *uinputDev; struct android::KeyLayoutMap keyLayoutMap; }; @@ -84,13 +85,9 @@ static int findEvdevDevice( char fullPath[256]; snprintf(fullPath, sizeof(fullPath), "/dev/input/%s", entry->d_name); - // Check if it's a character device (input device) - struct stat st{}; - -// LOGD("Found input device: %s", fullPath); - - // Try to open the device to see if it's accessible - int fd = open(fullPath, O_RDONLY); + // MUST be NONBLOCK so that the loop reading the evdev events eventually returns + // due to an EAGAIN error. + int fd = open(fullPath, O_RDONLY | O_NONBLOCK); if (fd == -1) { continue; @@ -110,11 +107,11 @@ static int findEvdevDevice( int devBus = libevdev_get_id_bustype(*outDev); if (name != devName || - devVendor != vendor || - devProduct != product || - // The hidden device bus field was only added to InputDevice.java in Android 14. - // So only check it if it is a real value - (bus != -1 && devBus != bus)) { + devVendor != vendor || + devProduct != product || + // The hidden device bus field was only added to InputDevice.java in Android 14. + // So only check it if it is a real value + (bus != -1 && devBus != bus)) { libevdev_free(*outDev); close(fd); @@ -194,32 +191,43 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNa } -int onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { +void onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { struct input_event inputEvent{}; - // the number of ready file descriptors int rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); - if (rc == 0) { - int32_t outKeycode = -1; - uint32_t outFlags = -1; - int deviceId = deviceContext->deviceId; - deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); - - callback->onEvdevEvent(deviceId, - inputEvent.time.tv_sec, - inputEvent.time.tv_usec, - inputEvent.type, - inputEvent.code, - inputEvent.value, - outKeycode); - } + do { + if (rc == LIBEVDEV_READ_STATUS_SUCCESS) { // rc == 0 + int32_t outKeycode = -1; + uint32_t outFlags = -1; + int deviceId = deviceContext->deviceId; + deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); + + bool returnValue; + callback->onEvdevEvent(deviceId, + inputEvent.time.tv_sec, + inputEvent.time.tv_usec, + inputEvent.type, + inputEvent.code, + inputEvent.value, + outKeycode, + &returnValue); + + if (!returnValue) { + libevdev_uinput_write_event(deviceContext->uinputDev, + inputEvent.type, + inputEvent.code, + inputEvent.value); + } - if (rc == 1 || rc == 0 || rc == -EAGAIN) { - return 0; - } else { - return rc; - } + rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); + + } else if (rc == LIBEVDEV_READ_STATUS_SYNC) { + rc = libevdev_next_event(deviceContext->evdev, + LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC, + &inputEvent); + } + } while (rc != -EAGAIN); } // Set this to some upper limit. It is unlikely that Key Mapper will be polling @@ -230,6 +238,21 @@ void handleCommand(const Command &cmd) { if (cmd.type == GRAB) { const GrabData &data = std::get(cmd.data); + { + std::lock_guard lock(evdevDevicesMutex); + for (auto pair: *evdevDevices) { + DeviceContext context = pair.second; + if (context.inputDeviceIdentifier.name == data.identifier.name + && context.inputDeviceIdentifier.bus == data.identifier.bus + && context.inputDeviceIdentifier.vendor == data.identifier.vendor + && context.inputDeviceIdentifier.product == data.identifier.product) { + LOGW("Device %s is already grabbed. Maybe it is a virtual uinput device.", + data.identifier.name.c_str()); + return; + } + } + } + struct libevdev *dev = nullptr; int rc = findEvdevDevice(data.identifier.name, data.identifier.bus, @@ -259,16 +282,35 @@ void handleCommand(const Command &cmd) { return; } + struct libevdev_uinput *uinputDev = nullptr; + int uinputFd = open("/dev/uinput", O_RDWR); + if (uinputFd < 0) { + LOGE("Failed to open /dev/uinput to clone the device."); + return; + } + + rc = libevdev_uinput_create_from_device(dev, uinputFd, &uinputDev); + + if (rc < 0) { + LOGE("Failed to create uinput device from evdev device %s: %s", + libevdev_get_name(dev), strerror(-rc)); + close(uinputFd); + libevdev_free(dev); + return; + } + DeviceContext context{ data.deviceId, data.identifier, dev, + uinputDev, *klResult.value() }; struct epoll_event event{}; event.events = EPOLLIN; event.data.fd = evdevFd; + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, evdevFd, &event) == -1) { LOGE("Failed to add new device to epoll: %s", strerror(errno)); libevdev_free(dev); @@ -278,23 +320,35 @@ void handleCommand(const Command &cmd) { std::lock_guard lock(evdevDevicesMutex); evdevDevices->insert_or_assign(evdevFd, context); + LOGI("Grabbed device %d", context.deviceId); + } else if (cmd.type == UNGRAB) { + const UngrabData &data = std::get(cmd.data); std::lock_guard lock(evdevDevicesMutex); for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { + int deviceId = it->second.deviceId; if (it->second.deviceId == data.deviceId) { + + // Do this before freeing the evdev file descriptor + libevdev_uinput_destroy(it->second.uinputDev); + int fd = it->first; epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr); libevdev_grab(it->second.evdev, LIBEVDEV_UNGRAB); libevdev_free(it->second.evdev); evdevDevices->erase(it); + + LOGI("Ungrabbed device %d", deviceId); break; } } } } +// TODO create separate loops for handling evdev events and commands so that ungrabbing can work if one is still processing a stream a of evdev events. +// The systemBridge should launch these loops on separate threads. extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, @@ -321,6 +375,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo struct epoll_event event{}; event.events = EPOLLIN; event.data.fd = commandEventFd; + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, commandEventFd, &event) == -1) { LOGE("Failed to add command eventfd to epoll: %s", strerror(errno)); close(epollFd); @@ -337,9 +392,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo LOGI("Start evdev event loop"); - callback-> - - onEvdevEventLoopStarted(); + callback->onEvdevEventLoopStarted(); while (running) { int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); @@ -348,14 +401,22 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo if (events[i].data.fd == commandEventFd) { uint64_t val; ssize_t s = read(commandEventFd, &val, sizeof(val)); + if (s < 0) { LOGE("Error reading from command event fd: %s", strerror(errno)); } - std::lock_guard lock(commandMutex); - while (!commandQueue.empty()) { - Command cmd = commandQueue.front(); - commandQueue.pop(); + std::vector commandsToProcess; + { + std::lock_guard lock(commandMutex); + while (!commandQueue.empty()) { + commandsToProcess.push_back(commandQueue.front()); + commandQueue.pop(); + } + } + + // Process commands without holding the mutex + for (const auto &cmd: commandsToProcess) { if (cmd.type == STOP) { running = false; break; @@ -363,7 +424,6 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo handleCommand(cmd); } } else { - std::lock_guard lock(evdevDevicesMutex); DeviceContext *dc = &evdevDevices->at(events[i].data.fd); onEpollEvent(dc, callback.get()); } @@ -385,15 +445,19 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo LOGI("Stopped evdev event loop"); } -void ungrabDevice(int deviceId) { - LOGI("Ungrab device %d", deviceId); - +extern "C" +JNIEXPORT void JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, + jobject thiz, + jint deviceId) { Command cmd; cmd.type = UNGRAB; cmd.data = UngrabData{deviceId}; - std::lock_guard lock(commandMutex); - commandQueue.push(cmd); + { + std::lock_guard lock(commandMutex); + commandQueue.push(cmd); + } // Notify the event loop uint64_t val = 1; @@ -403,14 +467,6 @@ void ungrabDevice(int deviceId) { } } -extern "C" -JNIEXPORT void JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, - jobject thiz, - jint device_id) { - ungrabDevice(device_id); -} - extern "C" JNIEXPORT void JNICALL @@ -433,7 +489,7 @@ extern "C" JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNative(JNIEnv *env, jobject thiz, - jint device_id, + jint deviceId, jint type, jint code, jint value) { @@ -454,7 +510,18 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDev } } + std::lock_guard commandLock(commandMutex); for (int id: deviceIds) { - ungrabDevice(id); + Command cmd; + cmd.type = UNGRAB; + cmd.data = UngrabData{id}; + commandQueue.push(cmd); + } + + // Notify the event loop + uint64_t val = 1; + ssize_t written = write(commandEventFd, &val, sizeof(val)); + if (written < 0) { + LOGE("Failed to write to commandEventFd: %s", strerror(errno)); } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 2e21eb27ca..7988d0edf4 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -12,6 +12,7 @@ import android.os.IBinder import android.os.Looper import android.os.ServiceManager import android.util.Log +import android.view.InputDevice import android.view.InputEvent import io.github.sds100.keymapper.common.utils.getBluetoothAddress import io.github.sds100.keymapper.common.utils.getDeviceBus @@ -40,6 +41,7 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO if no response when sending to the callback, stop the process. // TODO return error code and map this to a SystemBridgeError in key mapper + external fun grabEvdevDeviceNative( deviceIdentifier: InputDeviceIdentifier ): Boolean @@ -252,6 +254,7 @@ internal class SystemBridge : ISystemBridge.Stub() { } } + // TODO passthrough a timeout that will automatically ungrab after that time. override fun grabEvdevDevice( deviceId: Int, ): Boolean { @@ -259,8 +262,16 @@ internal class SystemBridge : ISystemBridge.Stub() { // Perhaps this will also happen on other real devices. // TODO whenever grabbing a touchscreen device, duplicate the input device + val inputDevice = inputManager.getInputDevice(deviceId) ?: return false + + // Don't grab any virtual devices or udev devices + if (inputDevice.isVirtual) { + Log.i(TAG, "Not grabbing virtual device: $deviceId") + return false + } - return grabEvdevDeviceNative(buildInputDeviceIdentifier(deviceId)) + val deviceIdentifier = buildInputDeviceIdentifier(inputDevice) ?: return false + return grabEvdevDeviceNative(deviceIdentifier) } override fun ungrabEvdevDevice(deviceId: Int): Boolean { @@ -269,23 +280,21 @@ internal class SystemBridge : ISystemBridge.Stub() { } override fun ungrabAllEvdevDevices(): Boolean { + Log.i(TAG, "Start ungrab all evdev devices"); ungrabAllEvdevDevicesNative() return true } - private fun buildInputDeviceIdentifier(deviceId: Int): InputDeviceIdentifier { - val inputDevice = inputManager.getInputDevice(deviceId) - - val deviceIdentifier = InputDeviceIdentifier( - id = deviceId, - name = inputDevice.name, + private fun buildInputDeviceIdentifier(inputDevice: InputDevice): InputDeviceIdentifier? { + return InputDeviceIdentifier( + id = inputDevice.id, + name = inputDevice.name ?: return null, vendor = inputDevice.vendorId, product = inputDevice.productId, - descriptor = inputDevice.descriptor, + descriptor = inputDevice.descriptor ?: return null, bus = inputDevice.getDeviceBus(), bluetoothAddress = inputDevice.getBluetoothAddress() ) - return deviceIdentifier } override fun injectInputEvent(event: InputEvent?, mode: Int): Boolean { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index 40faf6321b..6476052799 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -123,6 +123,20 @@ class AndroidDevicesAdapter @Inject constructor( }.launchIn(coroutineScope) } + fun logConnectedInputDevices() { + val deviceIds = inputManager?.inputDeviceIds ?: return + for (deviceId in deviceIds) { + val device = InputDevice.getDevice(deviceId) ?: continue + + val supportedSources: String = InputDeviceUtils.SOURCE_NAMES + .filter { device.supportsSource(it.key) } + .values + .joinToString() + + Timber.d("Input device: ${device.id} ${device.name} Vendor=${device.vendorId} Product=${device.productId} Descriptor=${device.descriptor} Sources=$supportedSources") + } + } + override fun deviceHasKey(id: Int, keyCode: Int): Boolean { val device = InputDevice.getDevice(id) ?: return false @@ -151,13 +165,6 @@ class AndroidDevicesAdapter @Inject constructor( for (id in InputDevice.getDeviceIds()) { val device = InputDevice.getDevice(id) ?: continue - val supportedSources: String = InputDeviceUtils.SOURCE_NAMES - .filter { device.supportsSource(it.key) } - .values - .joinToString() - - Timber.d("Input device: $id ${device.name} Vendor=${device.vendorId} Product=${device.productId} Descriptor=${device.descriptor} Sources=$supportedSources") - devices.add(InputDeviceUtils.createInputDeviceInfo(device)) } From 107d95703d4ff76c55cf909056c7902368474481 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 21:53:49 +0100 Subject: [PATCH 080/215] #1394 remove some comments and add extra logging --- .../keymapper/base/keymaps/detection/KeyMapAlgorithm.kt | 1 + sysbridge/src/main/cpp/android/input/InputDevice.cpp | 4 +++- sysbridge/src/main/cpp/libevdev_jni.cpp | 3 +-- .../sds100/keymapper/sysbridge/service/SystemBridge.kt | 8 ++------ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index 6eac573abf..ec4d45dc1b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -585,6 +585,7 @@ class KeyMapAlgorithm( return consume } + // TODO detect by scancode if the keycode is unknown. /** * @return whether to consume the [KeyEvent]. */ diff --git a/sysbridge/src/main/cpp/android/input/InputDevice.cpp b/sysbridge/src/main/cpp/android/input/InputDevice.cpp index 4d7920a3fb..5fe45f94d5 100644 --- a/sysbridge/src/main/cpp/android/input/InputDevice.cpp +++ b/sysbridge/src/main/cpp/android/input/InputDevice.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#define LOG_TAG "InputDevice" #include #include @@ -74,6 +73,7 @@ namespace android { suffix), type); if (!versionPath.empty()) { + LOGI("Found key layout map by version path %s", versionPath.c_str()); return versionPath; } } @@ -87,6 +87,7 @@ namespace android { suffix), type); if (!productPath.empty()) { + LOGI("Found key layout map by product path %s", productPath.c_str()); return productPath; } } @@ -97,6 +98,7 @@ namespace android { type); if (!namePath.empty()) { + LOGI("Found key layout map by name path %s", namePath.c_str()); return namePath; } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index d7369318a6..a2ed753397 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -275,6 +275,7 @@ void handleCommand(const Command &cmd) { int evdevFd = libevdev_get_fd(dev); std::string klPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( data.identifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); + auto klResult = android::KeyLayoutMap::load(klPath, nullptr); if (!klResult.ok()) { @@ -347,8 +348,6 @@ void handleCommand(const Command &cmd) { } } -// TODO create separate loops for handling evdev events and commands so that ungrabbing can work if one is still processing a stream a of evdev events. -// The systemBridge should launch these loops on separate threads. extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 7988d0edf4..13cd155a9b 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -258,13 +258,9 @@ internal class SystemBridge : ISystemBridge.Stub() { override fun grabEvdevDevice( deviceId: Int, ): Boolean { - // Can not filter touchscreens because the volume and power buttons in the emulator come through touchscreen devices. - // Perhaps this will also happen on other real devices. - - // TODO whenever grabbing a touchscreen device, duplicate the input device val inputDevice = inputManager.getInputDevice(deviceId) ?: return false - // Don't grab any virtual devices or udev devices + // Don't grab any virtual devices if (inputDevice.isVirtual) { Log.i(TAG, "Not grabbing virtual device: $deviceId") return false @@ -280,7 +276,7 @@ internal class SystemBridge : ISystemBridge.Stub() { } override fun ungrabAllEvdevDevices(): Boolean { - Log.i(TAG, "Start ungrab all evdev devices"); + Log.i(TAG, "Start ungrab all evdev devices") ungrabAllEvdevDevicesNative() return true } From 9b1618bd0d37045e79dce35da1ec4f81e15ab6b2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 22:39:03 +0100 Subject: [PATCH 081/215] #1394 better detect grabbing an already grabbed device by comparing the device paths --- .../keymapper/base/input/InputEventHub.kt | 26 ++-------- sysbridge/src/main/cpp/libevdev_jni.cpp | 49 +++++++++++-------- .../system/devices/AndroidDevicesAdapter.kt | 12 +++++ .../system/devices/DevicesAdapter.kt | 1 + 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index ba7a457927..dc211f5a04 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -8,7 +8,6 @@ import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success -import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.success import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository @@ -141,26 +140,6 @@ class InputEventHubImpl @Inject constructor( if (keyEvent != null) { return onInputEvent(keyEvent, InputEventDetectionSource.EVDEV) - - // Passthrough the key event if it is not consumed. -// if (!consumed) { -// if (logInputEventsEnabled.value) { -// Timber.d("Passthrough key event from evdev: ${keyEvent.keyCode}") -// } -// runBlocking { -// injectKeyEvent( -// InjectKeyEventModel( -// keyCode = keyEvent.keyCode, -// action = keyEvent.action, -// metaState = keyEvent.metaState, -// deviceId = keyEvent.deviceId, -// scanCode = keyEvent.scanCode, -// repeatCount = keyEvent.repeatCount, -// source = keyEvent.source -// ) -// ) -// } -// } } else { return onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) } @@ -290,6 +269,7 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedEvdevDevices() } + // TODO invalidate when the input devices change private fun invalidateGrabbedEvdevDevices() { val descriptors: Set = clients.values.flatMap { it.grabbedEvdevDevices }.toSet() @@ -306,7 +286,9 @@ class InputEventHubImpl @Inject constructor( return } - val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() ?: return + // Must make sure we get up to date input device information and not from the Flow + // that is updated by broadcast receiver + val inputDevices = devicesAdapter.getInputDevicesNow() for (descriptor in descriptors) { val device = inputDevices.find { it.descriptor == descriptor } ?: continue diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index a2ed753397..2064bd59d1 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -46,6 +46,7 @@ struct DeviceContext { struct libevdev *evdev; struct libevdev_uinput *uinputDev; struct android::KeyLayoutMap keyLayoutMap; + char devicePath[256]; }; void ungrabDevice(jint device_id); @@ -65,7 +66,8 @@ static int findEvdevDevice( int bus, int vendor, int product, - libevdev **outDev + libevdev **outDev, + char *outPath ) { DIR *dir = opendir("/dev/input"); @@ -106,7 +108,7 @@ static int findEvdevDevice( int devProduct = libevdev_get_id_product(*outDev); int devBus = libevdev_get_id_bustype(*outDev); - if (name != devName || + if (devName != name || devVendor != vendor || devProduct != product || // The hidden device bus field was only added to InputDevice.java in Android 14. @@ -120,6 +122,7 @@ static int findEvdevDevice( closedir(dir); + strcpy(outPath, fullPath); return 0; } @@ -238,32 +241,33 @@ void handleCommand(const Command &cmd) { if (cmd.type == GRAB) { const GrabData &data = std::get(cmd.data); - { - std::lock_guard lock(evdevDevicesMutex); - for (auto pair: *evdevDevices) { - DeviceContext context = pair.second; - if (context.inputDeviceIdentifier.name == data.identifier.name - && context.inputDeviceIdentifier.bus == data.identifier.bus - && context.inputDeviceIdentifier.vendor == data.identifier.vendor - && context.inputDeviceIdentifier.product == data.identifier.product) { - LOGW("Device %s is already grabbed. Maybe it is a virtual uinput device.", - data.identifier.name.c_str()); - return; - } - } - } - struct libevdev *dev = nullptr; + char devicePath[256]; + int rc = findEvdevDevice(data.identifier.name, data.identifier.bus, data.identifier.vendor, data.identifier.product, - &dev); + &dev, + devicePath); if (rc < 0) { LOGE("Failed to find device for grab command"); return; } + { + std::lock_guard lock(evdevDevicesMutex); + for (const auto &pair: *evdevDevices) { + DeviceContext context = pair.second; + if (strcmp(context.devicePath, devicePath) == 0) { + LOGW("Device %s %s is already grabbed. Maybe it is a virtual uinput device.", + data.identifier.name.c_str(), + devicePath); + return; + } + } + } + rc = libevdev_grab(dev, LIBEVDEV_GRAB); if (rc < 0) { LOGE("Failed to grab evdev device %s: %s", @@ -305,9 +309,11 @@ void handleCommand(const Command &cmd) { data.identifier, dev, uinputDev, - *klResult.value() + *klResult.value(), }; + strcpy(context.devicePath, devicePath); + struct epoll_event event{}; event.events = EPOLLIN; event.data.fd = evdevFd; @@ -321,7 +327,10 @@ void handleCommand(const Command &cmd) { std::lock_guard lock(evdevDevicesMutex); evdevDevices->insert_or_assign(evdevFd, context); - LOGI("Grabbed device %d", context.deviceId); + // TODO add device input file to epoll so the state is cleared when it is disconnected. Remember to remove the epoll after it has disconnected. + + LOGI("Grabbed device %d, %s, %s", context.deviceId, + context.inputDeviceIdentifier.name.c_str(), context.devicePath); } else if (cmd.type == UNGRAB) { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index 6476052799..e907ffcb4f 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -159,6 +159,18 @@ class AndroidDevicesAdapter @Inject constructor( return InputDevice.getDevice(deviceId)?.let { InputDeviceUtils.createInputDeviceInfo(it) } } + override fun getInputDevicesNow(): List { + val devices = mutableListOf() + + for (id in InputDevice.getDeviceIds()) { + val device = InputDevice.getDevice(id) ?: continue + + devices.add(InputDeviceUtils.createInputDeviceInfo(device)) + } + + return devices + } + private fun updateInputDevices() { val devices = mutableListOf() diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt index f82be90881..42860699ec 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt @@ -18,4 +18,5 @@ interface DevicesAdapter { fun deviceHasKey(id: Int, keyCode: Int): Boolean fun getInputDeviceName(descriptor: String): KMResult fun getInputDevice(deviceId: Int): InputDeviceInfo? + fun getInputDevicesNow(): List } From 18e135586ae9563e40caabdb258f369ba9107353 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 8 Aug 2025 22:52:09 +0100 Subject: [PATCH 082/215] #1394 libevdev_jni.cpp add DEBUG_PROBE switch --- sysbridge/src/main/cpp/libevdev_jni.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 2064bd59d1..5cdbb85026 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -61,6 +61,8 @@ static std::mutex commandMutex; static std::map *evdevDevices = new std::map(); static std::mutex evdevDevicesMutex; +#define DEBUG_PROBE false + static int findEvdevDevice( std::string name, int bus, @@ -108,6 +110,11 @@ static int findEvdevDevice( int devProduct = libevdev_get_id_product(*outDev); int devBus = libevdev_get_id_bustype(*outDev); + if (DEBUG_PROBE) { + LOGD("Evdev device: %s, bus: %d, vendor: %d, product: %d, path: %s", + devName, devBus, devVendor, devProduct, fullPath); + } + if (devName != name || devVendor != vendor || devProduct != product || From 82523f9e3eb79246c0deffaf2f0bd884a20dfa1e Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 01:28:53 +0100 Subject: [PATCH 083/215] #1394 do not map evdev devices to Android input devices because Android does not report all of the input devices that actually exist --- .../sds100/keymapper/base/BaseMainActivity.kt | 3 - .../keymapper/base/input/EvdevHandleCache.kt | 53 ++++ .../base/input/EvdevKeyEventTracker.kt | 56 ---- .../keymapper/base/input/InputDeviceCache.kt | 37 --- .../base/input/InputEventDetectionSource.kt | 1 - .../keymapper/base/input/InputEventHub.kt | 169 ++++++----- .../base/keymaps/ConfigKeyMapUseCase.kt | 14 +- .../base/keymaps/KeyMapListItemCreator.kt | 19 +- .../base/keymaps/detection/KeyMapAlgorithm.kt | 162 ++++++---- .../detection/KeyMapDetectionController.kt | 17 +- .../RerouteKeyEventsController.kt | 4 +- .../trigger/BaseConfigTriggerViewModel.kt | 58 ++-- .../keymapper/base/trigger/EvdevTriggerKey.kt | 21 +- .../base/trigger/InputEventTriggerKey.kt | 14 + .../base/trigger/KeyCodeTriggerKey.kt | 13 - .../base/trigger/KeyEventTriggerKey.kt | 4 +- .../base/trigger/RecordTriggerController.kt | 63 ++-- .../keymapper/base/trigger/RecordedKey.kt | 25 +- .../base/keymaps/KeyMapAlgorithmTest.kt | 2 +- common/build.gradle.kts | 5 + .../common/models/EvdevDeviceHandle.aidl | 3 + .../common/models/EvdevDeviceHandle.kt | 16 + .../common/models/EvdevDeviceInfo.kt | 12 + .../data/entities/EvdevTriggerKeyEntity.kt | 16 +- .../data/entities/TriggerKeyEntity.kt | 10 +- .../keymapper/sysbridge/IEvdevCallback.aidl | 2 +- .../keymapper/sysbridge/ISystemBridge.aidl | 24 +- .../utils/InputDeviceIdentifier.aidl | 3 - .../keymapper/sysbridge/BnEvdevCallback.h | 9 +- .../keymapper/sysbridge/BpEvdevCallback.h | 6 +- .../keymapper/sysbridge/IEvdevCallback.cpp | 32 +- .../keymapper/sysbridge/IEvdevCallback.h | 12 +- .../main/cpp/android/input/InputDevice.cpp | 6 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 284 +++++++++++++----- .../sysbridge/service/SystemBridge.kt | 68 ++--- .../sysbridge/utils/InputDeviceIdentifier.kt | 15 - .../system/devices/AndroidDevicesAdapter.kt | 27 -- .../system/devices/DevicesAdapter.kt | 1 - .../system/inputevents/KMEvdevEvent.kt | 31 +- .../system/inputevents/KMGamePadEvent.kt | 2 +- .../system/inputevents/KMInputEvent.kt | 4 +- .../system/inputevents/KMKeyEvent.kt | 2 +- 42 files changed, 781 insertions(+), 544 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/input/InputDeviceCache.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt create mode 100644 common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl create mode 100644 common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt create mode 100644 common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt delete mode 100644 sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.aidl delete mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index efd10f0769..190dfaeb47 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -35,7 +35,6 @@ import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter -import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl @@ -193,8 +192,6 @@ abstract class BaseMainActivity : AppCompatActivity() { ContextCompat.RECEIVER_EXPORTED, ) } - - devicesAdapter.logConnectedInputDevices() } override fun onResume() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt new file mode 100644 index 0000000000..3812140504 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt @@ -0,0 +1,53 @@ +package io.github.sds100.keymapper.base.input + +import android.os.RemoteException +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.system.devices.DevicesAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +class EvdevHandleCache( + private val coroutineScope: CoroutineScope, + private val devicesAdapter: DevicesAdapter, + private val systemBridge: StateFlow +) { + private val devicesByPath: StateFlow> = + combine(devicesAdapter.connectedInputDevices, systemBridge) { _, systemBridge -> + systemBridge ?: return@combine emptyMap() + + try { + systemBridge.evdevInputDevices.associateBy { it.path } + } catch (_: RemoteException) { + emptyMap() + } + }.stateIn(coroutineScope, SharingStarted.Eagerly, emptyMap()) + + fun getDevices(): List { + return devicesByPath.value.values.map { device -> + EvdevDeviceInfo( + name = device.name, + bus = device.bus, + vendor = device.vendor, + product = device.product + ) + } + } + + fun getByPath(path: String): EvdevDeviceHandle? { + return devicesByPath.value[path] + } + + fun getByInfo(deviceInfo: EvdevDeviceInfo): EvdevDeviceHandle? { + return devicesByPath.value.values.firstOrNull { + it.name == deviceInfo.name && + it.bus == deviceInfo.bus && + it.vendor == deviceInfo.vendor && + it.product == deviceInfo.product + } + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt deleted file mode 100644 index 2443d4a85d..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevKeyEventTracker.kt +++ /dev/null @@ -1,56 +0,0 @@ -package io.github.sds100.keymapper.base.input - -import android.os.SystemClock -import android.view.InputDevice -import android.view.KeyEvent -import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent -import io.github.sds100.keymapper.system.inputevents.KMKeyEvent - -/** - * This keeps track of which evdev events have been sent so evdev events - * can be converted into key events with the correct metastate. - */ -class EvdevKeyEventTracker( - private val inputDeviceCache: InputDeviceCache -) { - - companion object { - private val IGNORED_CODES: Set = setOf( - 330 // BTN_TOUCH. This is sent when you sometimes tap the touch screen. - ) - } - - fun toKeyEvent(event: KMEvdevEvent): KMKeyEvent? { - if (!event.isKeyEvent) { - return null - } - - if (event.androidCode == null) { - return null - } - - if (IGNORED_CODES.contains(event.code)) { - return null - } - - val action = when (event.value) { - 0 -> KeyEvent.ACTION_UP - 1 -> KeyEvent.ACTION_DOWN - 2 -> KeyEvent.ACTION_MULTIPLE - else -> throw IllegalArgumentException("Unknown evdev event value for keycode: ${event.value}") - } - - val inputDevice = inputDeviceCache.getById(event.deviceId) ?: return null - - return KMKeyEvent( - keyCode = event.androidCode!!, - action = action, - metaState = 0, // TODO handle keeping track of metastate - scanCode = event.code, - device = inputDevice, - repeatCount = 0, // TODO does this need handling? - source = inputDevice.sources ?: InputDevice.SOURCE_UNKNOWN,// TODO - eventTime = SystemClock.uptimeMillis() - ) - } -} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputDeviceCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputDeviceCache.kt deleted file mode 100644 index 3489c1967d..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputDeviceCache.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.sds100.keymapper.base.input - -import io.github.sds100.keymapper.common.utils.InputDeviceInfo -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.system.devices.DevicesAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -class InputDeviceCache( - private val coroutineScope: CoroutineScope, - private val devicesAdapter: DevicesAdapter -) { - - private val inputDevicesById: StateFlow> = - devicesAdapter.connectedInputDevices - .filterIsInstance>>() - .map { state -> state.data.associateBy { it.id } } - .stateIn(coroutineScope, SharingStarted.Eagerly, emptyMap()) - - private val inputDevicesByDescriptor: StateFlow> = - devicesAdapter.connectedInputDevices - .filterIsInstance>>() - .map { state -> state.data.associateBy { it.descriptor } } - .stateIn(coroutineScope, SharingStarted.Eagerly, emptyMap()) - - fun getById(id: Int): InputDeviceInfo? { - return inputDevicesById.value[id] - } - - fun getByDescriptor(descriptor: String): InputDeviceInfo? { - return inputDevicesByDescriptor.value.values.firstOrNull { it.descriptor == descriptor } - } -} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt index 882b26c9bf..63926bbad3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt @@ -4,5 +4,4 @@ enum class InputEventDetectionSource { ACCESSIBILITY_SERVICE, INPUT_METHOD, EVDEV, - MAIN_ACTIVITY } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index dc211f5a04..d82f10477b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -5,6 +5,7 @@ import android.os.RemoteException import android.view.KeyEvent import io.github.sds100.keymapper.base.BuildConfig import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success @@ -15,7 +16,6 @@ import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager -import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent @@ -25,10 +25,12 @@ import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -49,10 +51,9 @@ class InputEventHubImpl @Inject constructor( private const val INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2 } - private val clients: ConcurrentHashMap = - ConcurrentHashMap() + private val clients: ConcurrentHashMap = ConcurrentHashMap() - private var systemBridge: ISystemBridge? = null + private var systemBridgeFlow: MutableStateFlow = MutableStateFlow(null) // Event queue for processing key events asynchronously in order private val keyEventQueue = Channel(capacity = 100) @@ -61,14 +62,14 @@ class InputEventHubImpl @Inject constructor( override fun onServiceConnected(service: ISystemBridge) { Timber.i("InputEventHub connected to SystemBridge") - systemBridge = service + systemBridgeFlow.update { service } service.registerEvdevCallback(this@InputEventHubImpl) } override fun onServiceDisconnected(service: ISystemBridge) { Timber.i("InputEventHub disconnected from SystemBridge") - systemBridge = null + systemBridgeFlow.update { null } } override fun onBindingDied() { @@ -76,9 +77,11 @@ class InputEventHubImpl @Inject constructor( } } - private val inputDeviceCache: InputDeviceCache = - InputDeviceCache(coroutineScope, devicesAdapter) - private val evdevKeyEventTracker: EvdevKeyEventTracker = EvdevKeyEventTracker(inputDeviceCache) + private val evdevHandles: EvdevHandleCache = EvdevHandleCache( + coroutineScope, + devicesAdapter, + systemBridgeFlow + ) private val logInputEventsEnabled: StateFlow = preferenceRepository.get(Keys.log).map { isLogEnabled -> @@ -113,7 +116,7 @@ class InputEventHubImpl @Inject constructor( } override fun isSystemBridgeConnected(): Boolean { - return systemBridge != null + return systemBridgeFlow.value != null } override fun onEvdevEventLoopStarted() { @@ -122,7 +125,7 @@ class InputEventHubImpl @Inject constructor( } override fun onEvdevEvent( - deviceId: Int, + devicePath: String?, timeSec: Long, timeUsec: Long, type: Int, @@ -130,19 +133,12 @@ class InputEventHubImpl @Inject constructor( value: Int, androidCode: Int ): Boolean { - val evdevEvent = KMEvdevEvent(deviceId, type, code, value, androidCode, timeSec, timeUsec) + devicePath ?: return false - if (logInputEventsEnabled.value) { - logInputEvent(evdevEvent) - } + val handle = evdevHandles.getByPath(devicePath) ?: return false + val evdevEvent = KMEvdevEvent(handle, type, code, value, androidCode, timeSec, timeUsec) - val keyEvent: KMKeyEvent? = evdevKeyEventTracker.toKeyEvent(evdevEvent) - - if (keyEvent != null) { - return onInputEvent(keyEvent, InputEventDetectionSource.EVDEV) - } else { - return onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) - } + return onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) } override fun onInputEvent( @@ -159,25 +155,36 @@ class InputEventHubImpl @Inject constructor( logInputEvent(uniqueEvent) } - if (detectionSource == InputEventDetectionSource.EVDEV && event.deviceId == null) { - return false - } - var consume = false for (clientContext in clients.values) { - if (detectionSource == InputEventDetectionSource.EVDEV) { - val deviceDescriptor = inputDeviceCache.getById(event.deviceId!!)?.descriptor + if (event is KMEvdevEvent) { + // TODO maybe flatmap all the client event types into one Set so this check + // can be done in onEvdevEvent. Hundreds of events may be sent per second synchronously so important to be as fast as possible. + // This client can ignore this event. + if (!clientContext.evdevEventTypes.contains(event.type) + || clientContext.grabbedEvdevDevices.isEmpty() + ) { + continue + } - // Only send events from evdev devices to the client if they grabbed it + val deviceInfo = EvdevDeviceInfo( + event.device.name, + event.device.bus, + event.device.vendor, + event.device.product + ) - if (clientContext.grabbedEvdevDevices.contains(deviceDescriptor)) { - // Lazy evaluation may not execute this if its inlined? - val result = clientContext.callback.onInputEvent(event, detectionSource) - consume = consume || result + // Only send events from evdev devices to the client if they grabbed it + if (!clientContext.grabbedEvdevDevices.contains(deviceInfo)) { + continue } + + // Lazy evaluation may not execute this if its inlined + val result = clientContext.callback.onInputEvent(event, detectionSource) + consume = consume || result } else { - // Lazy evaluation may not execute this if its inlined? + // Lazy evaluation may not execute this if its inlined val result = clientContext.callback.onInputEvent(event, detectionSource) consume = consume || result } @@ -214,7 +221,7 @@ class InputEventHubImpl @Inject constructor( when (event) { is KMEvdevEvent -> { Timber.d( - "Evdev event: deviceId=${event.deviceId}, type=${event.type}, code=${event.code}, value=${event.value}" + "Evdev event: devicePath=${event.device.path}, deviceName=${event.device.name}, type=${event.type}, code=${event.code}, value=${event.value}" ) } @@ -245,12 +252,13 @@ class InputEventHubImpl @Inject constructor( override fun registerClient( clientId: String, callback: InputEventHubCallback, + eventTypes: List ) { if (clients.containsKey(clientId)) { throw IllegalArgumentException("This client already has a callback registered!") } - clients[clientId] = ClientContext(callback, emptySet()) + clients[clientId] = ClientContext(callback, emptySet(), eventTypes.toSet()) } override fun unregisterClient(clientId: String) { @@ -258,42 +266,50 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedEvdevDevices() } - override fun setGrabbedEvdevDevices(clientId: String, deviceDescriptors: List) { + override fun setGrabbedEvdevDevices(clientId: String, devices: List) { if (!clients.containsKey(clientId)) { throw IllegalArgumentException("This client $clientId is not registered when trying to grab devices!") } - clients[clientId] = - clients[clientId]!!.copy(grabbedEvdevDevices = deviceDescriptors.toSet()) + clients[clientId] = clients[clientId]!!.copy(grabbedEvdevDevices = devices.toSet()) + + invalidateGrabbedEvdevDevices() + } + + override fun grabAllEvdevDevices(clientId: String) { + if (!clients.containsKey(clientId)) { + throw IllegalArgumentException("This client $clientId is not registered when trying to grab devices!") + } + + val devices = evdevHandles.getDevices().toSet() + clients[clientId] = clients[clientId]!!.copy(grabbedEvdevDevices = devices) invalidateGrabbedEvdevDevices() } // TODO invalidate when the input devices change private fun invalidateGrabbedEvdevDevices() { - val descriptors: Set = clients.values.flatMap { it.grabbedEvdevDevices }.toSet() + val evdevDevices: Set = + clients.values.flatMap { it.grabbedEvdevDevices }.toSet() + + val systemBridge = systemBridgeFlow.value if (systemBridge == null) { return } try { - val ungrabResult = systemBridge?.ungrabAllEvdevDevices() + val ungrabResult = systemBridge.ungrabAllEvdevDevices() Timber.i("Ungrabbed all evdev devices: $ungrabResult") - if (ungrabResult != true) { + if (!ungrabResult) { Timber.e("Failed to ungrab all evdev devices before grabbing.") return } - // Must make sure we get up to date input device information and not from the Flow - // that is updated by broadcast receiver - val inputDevices = devicesAdapter.getInputDevicesNow() - - for (descriptor in descriptors) { - val device = inputDevices.find { it.descriptor == descriptor } ?: continue - - val grabResult = systemBridge?.grabEvdevDevice(device.id) + for (device in evdevDevices) { + val handle = evdevHandles.getByInfo(device) ?: continue + val grabResult = systemBridge.grabEvdevDevice(handle.path) Timber.i("Grabbed evdev device ${device.name}: $grabResult") } @@ -302,27 +318,27 @@ class InputEventHubImpl @Inject constructor( } } - override fun injectEvdevEvent(event: KMEvdevEvent): KMResult { - val systemBridge = this.systemBridge - - if (systemBridge == null) { - return SystemBridgeError.Disconnected - } - - try { - return systemBridge.writeEvdevEvent( - event.deviceId, - event.type, - event.code, - event.value - ).success() - } catch (e: RemoteException) { - return KMError.Exception(e) - } - } +// override fun injectEvdevEvent(event: KMEvdevEvent): KMResult { +// val systemBridge = this.systemBridgeFlow +// +// if (systemBridge == null) { +// return SystemBridgeError.Disconnected +// } +// +// try { +// return systemBridge.writeEvdevEvent( +// event.deviceId, +// event.type, +// event.code, +// event.value +// ).success() +// } catch (e: RemoteException) { +// return KMError.Exception(e) +// } +// } override suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult { - val systemBridge = this.systemBridge + val systemBridge = this.systemBridgeFlow.value if (systemBridge == null) { imeInputEventInjector.inputKeyEvent(event) @@ -365,9 +381,10 @@ class InputEventHubImpl @Inject constructor( private data class ClientContext( val callback: InputEventHubCallback, /** - * The descriptors of the evdev devices that this client wants to grab. + * The evdev devices that this client wants to grab. */ - val grabbedEvdevDevices: Set + val grabbedEvdevDevices: Set, + val evdevEventTypes: Set ) } @@ -381,14 +398,13 @@ interface InputEventHub { fun registerClient( clientId: String, callback: InputEventHubCallback, + eventTypes: List ) fun unregisterClient(clientId: String) - /** - * Set the evdev devices that a client wants to listen to. - */ - fun setGrabbedEvdevDevices(clientId: String, deviceDescriptors: List) + fun setGrabbedEvdevDevices(clientId: String, devices: List) + fun grabAllEvdevDevices(clientId: String) /** * Inject a key event. This may either use the key event relay service or the system @@ -405,7 +421,8 @@ interface InputEventHub { */ fun injectKeyEventAsync(event: InjectKeyEventModel): KMResult - fun injectEvdevEvent(event: KMEvdevEvent): KMResult + // TODO make InjectEvdevEventModel +// fun injectEvdevEvent(event: KMEvdevEvent): KMResult /** * Send an input event to the connected clients. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt index 05a426cad1..1f9a670aa9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt @@ -19,6 +19,7 @@ import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey import io.github.sds100.keymapper.base.trigger.TriggerMode +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State @@ -409,8 +410,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( override fun addEvdevTriggerKey( keyCode: Int, scanCode: Int, - deviceDescriptor: String, - deviceName: String + device: EvdevDeviceInfo ) = editTrigger { trigger -> val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -423,14 +423,13 @@ class ConfigKeyMapUseCaseController @Inject constructor( val containsKey = trigger.keys .filterIsInstance() .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.deviceDescriptor == deviceDescriptor + keyToCompare.keyCode == keyCode && keyToCompare.device == device } val triggerKey = EvdevTriggerKey( keyCode = keyCode, scanCode = scanCode, - deviceDescriptor = deviceDescriptor, - deviceName = deviceName, + device = device, clickType = clickType, consumeEvent = true, ) @@ -509,7 +508,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( is FloatingButtonKey -> key.buttonUid is EvdevTriggerKey -> Pair( key.keyCode, - key.deviceDescriptor, + key.device, ) } } @@ -1064,8 +1063,7 @@ interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { fun addEvdevTriggerKey( keyCode: Int, scanCode: Int, - deviceDescriptor: String, - deviceName: String, + device: EvdevDeviceInfo ) fun removeTriggerKey(uid: String) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt index 27ee133bfa..db04aec4b3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt @@ -64,7 +64,7 @@ class KeyMapListItemCreator( is FloatingButtonKey -> floatingButtonKeyName(key) is FingerprintTriggerKey -> fingerprintKeyName(key) - is EvdevTriggerKey -> evdevTriggerKeyName(key, showDeviceDescriptors) + is EvdevTriggerKey -> evdevTriggerKeyName(key) } } @@ -296,10 +296,7 @@ class KeyMapListItemCreator( } } - private fun evdevTriggerKeyName( - key: EvdevTriggerKey, - showDeviceDescriptors: Boolean, - ): String = buildString { + private fun evdevTriggerKeyName(key: EvdevTriggerKey): String = buildString { when (key.clickType) { ClickType.LONG_PRESS -> append(longPressString).append(" ") ClickType.DOUBLE_PRESS -> append(doublePressString).append(" ") @@ -308,19 +305,9 @@ class KeyMapListItemCreator( append(InputEventStrings.keyCodeToString(key.keyCode)) - val deviceName = if (showDeviceDescriptors) { - InputDeviceUtils.appendDeviceDescriptorToName( - key.deviceDescriptor, - key.deviceName, - ) - } else { - key.deviceName - } - - val parts = buildList { add("PRO") - add(deviceName) + add(key.device.name) if (!key.consumeEvent) { add(getString(R.string.flag_dont_override_default_action)) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index ec4d45dc1b..d9d9f8acbd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -18,17 +18,20 @@ import io.github.sds100.keymapper.base.trigger.AssistantTriggerType import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.base.trigger.InputEventTriggerKey import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey import io.github.sds100.keymapper.base.trigger.TriggerMode +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -73,7 +76,7 @@ class KeyMapAlgorithm( /** * All sequence events that have the long press click type. */ - private var longPressSequenceTriggerKeys: Array = arrayOf() + private var longPressSequenceTriggerKeys: Array = arrayOf() /** * All double press keys and the index of their corresponding trigger. first is the event and second is @@ -165,7 +168,7 @@ class KeyMapAlgorithm( private var metaStateFromActions: Int = 0 private var metaStateFromKeyEvent: Int = 0 - private val eventDownTimeMap: MutableMap = mutableMapOf() + private val eventDownTimeMap: MutableMap = mutableMapOf() /** * This solves issue #1386. This stores the jobs that will wait until the sequence trigger @@ -268,7 +271,7 @@ class KeyMapAlgorithm( } else { detectKeyMaps = true - val longPressSequenceTriggerKeys = mutableListOf() + val longPressSequenceTriggerKeys = mutableListOf() val doublePressKeys = mutableListOf() @@ -303,7 +306,7 @@ class KeyMapAlgorithm( if (keyMap.trigger.mode == TriggerMode.Sequence && key.clickType == ClickType.LONG_PRESS && - key is KeyCodeTriggerKey + key is InputEventTriggerKey ) { if (keyMap.trigger.keys.size > 1) { longPressSequenceTriggerKeys.add(key) @@ -517,7 +520,7 @@ class KeyMapAlgorithm( val trigger = triggers[triggerIndex] for ((keyIndex, key) in trigger.keys.withIndex()) { - if (key is KeyCodeTriggerKey && isModifierKey(key.keyCode)) { + if (key is InputEventTriggerKey && isModifierKey(key.keyCode)) { parallelTriggerModifierKeyIndices.add(triggerIndex to keyIndex) } } @@ -589,30 +592,35 @@ class KeyMapAlgorithm( /** * @return whether to consume the [KeyEvent]. */ - fun onKeyEvent(keyEvent: KMKeyEvent): Boolean { + fun onInputEvent(event: KMInputEvent): Boolean { if (!detectKeyMaps) return false - if (dpadMotionEventTracker.onKeyEvent(keyEvent)) { - return true - } + if (event is KMKeyEvent) { + if (dpadMotionEventTracker.onKeyEvent(event)) { + return true + } - val device = keyEvent.device + val device = event.device - if ((device.isExternal && !detectExternalEvents) || (!device.isExternal && !detectInternalEvents)) { - return false + if ((device.isExternal && !detectExternalEvents) || (!device.isExternal && !detectInternalEvents)) { + return false + } } - return onKeyEventPostFilter(keyEvent) + return onKeyEventPostFilter(event) } - private fun onKeyEventPostFilter(keyEvent: KMKeyEvent): Boolean { - metaStateFromKeyEvent = keyEvent.metaState + private fun onKeyEventPostFilter(inputEvent: KMInputEvent): Boolean { + + if (inputEvent is KMKeyEvent) { + metaStateFromKeyEvent = inputEvent.metaState + } // remove the metastate from any modifier keys that remapped and are pressed down for ((triggerIndex, eventIndex) in parallelTriggerModifierKeyIndices) { val key = triggers[triggerIndex].keys[eventIndex] - if (key !is KeyCodeTriggerKey) { + if (key !is InputEventTriggerKey) { continue } @@ -622,34 +630,60 @@ class KeyMapAlgorithm( } } - val device = keyEvent.device - - val event = KeyCodeEvent( - keyCode = keyEvent.keyCode, - clickType = null, - descriptor = device.descriptor, - deviceId = device.id, - scanCode = keyEvent.scanCode, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source, - isExternal = device.isExternal - ) + when (inputEvent) { + is KMEvdevEvent -> { + val event = EvdevEventAlgo( + keyCode = inputEvent.androidCode, + clickType = null, + device = EvdevDeviceInfo( + name = inputEvent.device.name, + bus = inputEvent.device.bus, + vendor = inputEvent.device.vendor, + product = inputEvent.device.product, + ), + scanCode = inputEvent.code, + ) - when (keyEvent.action) { - KeyEvent.ACTION_DOWN -> return onKeyDown(event) - KeyEvent.ACTION_UP -> return onKeyUp(event) + if (inputEvent.isDownEvent) { + return onKeyDown(event) + } else if (inputEvent.isUpEvent) { + return onKeyUp(event) + } + } + + is KMKeyEvent -> { + val device = inputEvent.device + + val event = KeyEventAlgo( + keyCode = inputEvent.keyCode, + clickType = null, + descriptor = device.descriptor, + deviceId = device.id, + scanCode = inputEvent.scanCode, + repeatCount = inputEvent.repeatCount, + source = inputEvent.source, + isExternal = device.isExternal + ) + + when (inputEvent.action) { + KeyEvent.ACTION_DOWN -> return onKeyDown(event) + KeyEvent.ACTION_UP -> return onKeyUp(event) + } + } + + is KMGamePadEvent -> throw IllegalArgumentException("GamePad events are not supported by the key map algorithm") } return false } /** - * @return whether to consume the [KeyEvent]. + * @return whether to consume the event. */ - private fun onKeyDown(event: Event): Boolean { + private fun onKeyDown(event: AlgoEvent): Boolean { // Must come before saving the event down time because // there is no corresponding up key event for key events with a repeat count > 0 - if (event is KeyCodeEvent && event.repeatCount > 0) { + if (event is KeyEventAlgo && event.repeatCount > 0) { val matchingTriggerKey = triggerKeysThatSendRepeatedKeyEvents.any { it.matchesEvent(event.withShortPress) || it.matchesEvent(event.withLongPress) || @@ -664,7 +698,7 @@ class KeyMapAlgorithm( eventDownTimeMap[event] = currentTime var consumeEvent = false - val isModifierKeyCode = event is KeyCodeEvent && isModifierKey(event.keyCode) + val isModifierKeyCode = event is KeyEventAlgo && isModifierKey(event.keyCode) var mappedToParallelTriggerAction = false val constraintSnapshot: ConstraintSnapshot by lazy { detectConstraints.getSnapshot() } @@ -716,7 +750,7 @@ class KeyMapAlgorithm( consumeEvent = true } - key is KeyCodeTriggerKey && event is KeyCodeEvent -> + key is InputEventTriggerKey && event is KeyEventAlgo -> if (key.keyCode == event.keyCode && key.consumeEvent) { consumeEvent = true } @@ -908,7 +942,7 @@ class KeyMapAlgorithm( !isModifierKeyCode && metaStateFromActions != 0 && !mappedToParallelTriggerAction && - event is KeyCodeEvent + event is KeyEventAlgo ) { consumeEvent = true keyCodesToImitateUpAction.add(event.keyCode) @@ -1030,7 +1064,7 @@ class KeyMapAlgorithm( /** * @return whether to consume the event. */ - private fun onKeyUp(event: Event): Boolean { + private fun onKeyUp(event: AlgoEvent): Boolean { val downTime = eventDownTimeMap[event] ?: currentTime eventDownTimeMap.remove(event) @@ -1056,7 +1090,7 @@ class KeyMapAlgorithm( var metaStateFromActionsToRemove = 0 - if (event is KeyCodeEvent) { + if (event is KeyEventAlgo) { if (keyCodesToImitateUpAction.contains(event.keyCode)) { consumeEvent = true imitateUpKeyEvent = true @@ -1406,7 +1440,7 @@ class KeyMapAlgorithm( return@launch } - if (event is KeyCodeEvent) { + if (event is KeyEventAlgo) { useCase.imitateButtonPress( event.keyCode, action = KeyEvent.ACTION_DOWN, @@ -1429,7 +1463,7 @@ class KeyMapAlgorithm( detectedParallelTriggerIndexes.isEmpty() && !shortPressSingleKeyTriggerJustReleased && !mappedToDoublePress && - event is KeyCodeEvent + event is KeyEventAlgo ) { if (imitateUpKeyEvent) { useCase.imitateButtonPress( @@ -1523,7 +1557,7 @@ class KeyMapAlgorithm( /** * @return whether any actions were performed. */ - private fun performActionsOnFailedDoublePress(event: Event): Boolean { + private fun performActionsOnFailedDoublePress(event: AlgoEvent): Boolean { var showToast = false val detectedTriggerIndexes = mutableListOf() val vibrateDurations = mutableListOf() @@ -1663,14 +1697,14 @@ class KeyMapAlgorithm( } } - private fun Trigger.matchingEventAtIndex(event: Event, index: Int): Boolean { + private fun Trigger.matchingEventAtIndex(event: AlgoEvent, index: Int): Boolean { if (index >= this.keys.size) return false return this.keys[index].matchesEvent(event) } - private fun TriggerKey.matchesEvent(event: Event): Boolean { - if (this is KeyEventTriggerKey && event is KeyCodeEvent) { + private fun TriggerKey.matchesEvent(event: AlgoEvent): Boolean { + if (this is KeyEventTriggerKey && event is KeyEventAlgo) { return when (this.device) { KeyEventTriggerDevice.Any -> this.keyCode == event.keyCode && this.clickType == event.clickType is KeyEventTriggerDevice.External -> @@ -1681,8 +1715,8 @@ class KeyMapAlgorithm( this.keyCode == event.keyCode && this.clickType == event.clickType } - } else if (this is EvdevTriggerKey && event is KeyCodeEvent) { - return this.keyCode == event.keyCode && this.clickType == event.clickType && this.deviceDescriptor == event.descriptor + } else if (this is EvdevTriggerKey && event is EvdevEventAlgo) { + return this.keyCode == event.keyCode && this.clickType == event.clickType && this.device == event.device } else if (this is AssistantTriggerKey && event is AssistantEvent) { return if (this.type == AssistantTriggerType.ANY || event.type == AssistantTriggerType.ANY) { this.clickType == event.clickType @@ -1716,7 +1750,7 @@ class KeyMapAlgorithm( this.clickType == otherKey.clickType } } else if (this is EvdevTriggerKey && otherKey is EvdevTriggerKey) { - return this.keyCode == otherKey.keyCode && this.clickType == otherKey.clickType && this.deviceDescriptor == otherKey.deviceDescriptor + return this.keyCode == otherKey.keyCode && this.clickType == otherKey.clickType && this.device == otherKey.device } else if (this is AssistantTriggerKey && otherKey is AssistantTriggerKey) { return this.type == otherKey.type && this.clickType == otherKey.clickType } else if (this is FloatingButtonKey && otherKey is FloatingButtonKey) { @@ -1771,30 +1805,38 @@ class KeyMapAlgorithm( else -> false } - private val Event.withShortPress: Event + private val AlgoEvent.withShortPress: AlgoEvent get() = setClickType(clickType = ClickType.SHORT_PRESS) - private val Event.withLongPress: Event + private val AlgoEvent.withLongPress: AlgoEvent get() = setClickType(clickType = ClickType.LONG_PRESS) - private val Event.withDoublePress: Event + private val AlgoEvent.withDoublePress: AlgoEvent get() = setClickType(clickType = ClickType.DOUBLE_PRESS) /** * Represents the kind of event a trigger key is expecting to happen. */ - private sealed class Event { + private sealed class AlgoEvent { abstract val clickType: ClickType? - fun setClickType(clickType: ClickType?): Event = when (this) { - is KeyCodeEvent -> this.copy(clickType = clickType) + fun setClickType(clickType: ClickType?): AlgoEvent = when (this) { + is EvdevEventAlgo -> this.copy(clickType = clickType) + is KeyEventAlgo -> this.copy(clickType = clickType) is AssistantEvent -> this.copy(clickType = clickType) is FloatingButtonEvent -> this.copy(clickType = clickType) is FingerprintGestureEvent -> this.copy(clickType = clickType) } } - private data class KeyCodeEvent( + private data class EvdevEventAlgo( + val device: EvdevDeviceInfo, + val scanCode: Int, + val keyCode: Int, + override val clickType: ClickType? + ) : AlgoEvent() + + private data class KeyEventAlgo( val keyCode: Int, override val clickType: ClickType?, val descriptor: String, @@ -1803,22 +1845,22 @@ class KeyMapAlgorithm( val scanCode: Int, val repeatCount: Int, val source: Int, - ) : Event() + ) : AlgoEvent() private data class AssistantEvent( val type: AssistantTriggerType, override val clickType: ClickType?, - ) : Event() + ) : AlgoEvent() private data class FingerprintGestureEvent( val type: FingerprintGestureType, override val clickType: ClickType?, - ) : Event() + ) : AlgoEvent() private data class FloatingButtonEvent( val buttonUid: String, override val clickType: ClickType?, - ) : Event() + ) : AlgoEvent() private data class TriggerKeyLocation(val triggerIndex: Int, val keyIndex: Int) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt index fc9eb48f7f..6d895293e9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt @@ -12,8 +12,8 @@ import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.base.trigger.RecordTriggerState import io.github.sds100.keymapper.base.trigger.Trigger +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent -import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -42,7 +42,7 @@ class KeyMapDetectionController( init { // Must first register before collecting anything that may call reset() - inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this) + inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this, listOf(KMEvdevEvent.TYPE_KEY_EVENT)) coroutineScope.launch { detectUseCase.allKeyMapList.collect { keyMapList -> @@ -74,11 +74,7 @@ class KeyMapDetectionController( return false } - if (event is KMKeyEvent) { - return algorithm.onKeyEvent(event) - } else { - return false - } + return algorithm.onInputEvent(event) } fun onFingerprintGesture(type: FingerprintGestureType) { @@ -105,12 +101,15 @@ class KeyMapDetectionController( private fun grabEvdevDevicesForTriggers(triggers: Array) { val evdevDevices = triggers .flatMap { trigger -> trigger.keys.filterIsInstance() } - .map { it.deviceDescriptor } + .map { it.device } .distinct() .toList() Timber.i("Grab evdev devices for key map detection: ${evdevDevices.joinToString()}") - inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, evdevDevices) + inputEventHub.setGrabbedEvdevDevices( + INPUT_EVENT_HUB_ID, + evdevDevices, + ) } private fun reset() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt index 178d96e19d..8d1e4b19ad 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt @@ -9,6 +9,7 @@ import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope @@ -57,7 +58,8 @@ class RerouteKeyEventsController @AssistedInject constructor( if (isEnabled) { inputEventHub.registerClient( INPUT_EVENT_HUB_ID, - this@RerouteKeyEventsController + this@RerouteKeyEventsController, + listOf(KMEvdevEvent.TYPE_KEY_EVENT) ) } else { inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 537bf88911..1228fe7dca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -35,6 +35,7 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.ViewModelHelper import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.showDialog +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult @@ -209,8 +210,11 @@ abstract class BaseConfigTriggerViewModel( }.launchIn(coroutineScope) coroutineScope.launch { - recordTrigger.onRecordKey.collectLatest { - onRecordTriggerKey(it) + recordTrigger.onRecordKey.collect { key -> + when (key) { + is RecordedKey.EvdevEvent -> onRecordEvdevEvent(key) + is RecordedKey.KeyEvent -> onRecordKeyEvent(key) + } } } @@ -477,33 +481,18 @@ abstract class BaseConfigTriggerViewModel( } } - private suspend fun onRecordTriggerKey(key: RecordedKey) { - // Add the trigger key before showing the dialog so it doesn't - // need to be dismissed before it is added. - when (key.detectionSource) { - InputEventDetectionSource.EVDEV -> config.addEvdevTriggerKey( - key.keyCode, - key.scanCode, - key.deviceDescriptor, - key.deviceName - ) - - InputEventDetectionSource.ACCESSIBILITY_SERVICE, - InputEventDetectionSource.INPUT_METHOD, - InputEventDetectionSource.MAIN_ACTIVITY -> { - val triggerDevice = if (key.isExternalDevice) { - KeyEventTriggerDevice.External(key.deviceDescriptor, key.deviceName) - } else { - KeyEventTriggerDevice.Internal - } - - config.addKeyEventTriggerKey( - key.keyCode, key.scanCode, triggerDevice, - key.detectionSource != InputEventDetectionSource.ACCESSIBILITY_SERVICE - ) - } + private suspend fun onRecordKeyEvent(key: RecordedKey.KeyEvent) { + val triggerDevice = if (key.isExternalDevice) { + KeyEventTriggerDevice.External(key.deviceDescriptor, key.deviceName) + } else { + KeyEventTriggerDevice.Internal } + config.addKeyEventTriggerKey( + key.keyCode, key.scanCode, triggerDevice, + key.detectionSource != InputEventDetectionSource.ACCESSIBILITY_SERVICE + ) + if (key.keyCode >= InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET || key.keyCode < 0) { if (onboarding.shownKeyCodeToScanCodeTriggerExplanation) { return @@ -557,6 +546,19 @@ abstract class BaseConfigTriggerViewModel( } } + private fun onRecordEvdevEvent(key: RecordedKey.EvdevEvent) { + config.addEvdevTriggerKey( + key.keyCode, + key.scanCode, + EvdevDeviceInfo( + name = key.device.name, + bus = key.device.bus, + vendor = key.device.vendor, + product = key.device.product, + ) + ) + } + fun onParallelRadioButtonChecked() { config.setParallelTriggerMode() } @@ -760,7 +762,7 @@ abstract class BaseConfigTriggerViewModel( is EvdevTriggerKey -> EvdevEvent( id = key.uid, keyName = InputEventStrings.keyCodeToString(key.keyCode), - deviceName = key.deviceName, + deviceName = key.device.name, clickType = clickType, extraInfo = if (!key.consumeEvent) { getString(R.string.flag_dont_override_default_action) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt index a60905491f..85f2c71778 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.base.trigger import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.entities.EvdevTriggerKeyEntity @@ -16,11 +17,10 @@ data class EvdevTriggerKey( override val uid: String = UUID.randomUUID().toString(), override val keyCode: Int, override val scanCode: Int, - val deviceDescriptor: String, - val deviceName: String, + val device: EvdevDeviceInfo, override val clickType: ClickType = ClickType.SHORT_PRESS, override val consumeEvent: Boolean = true, -) : TriggerKey(), KeyCodeTriggerKey { +) : TriggerKey(), InputEventTriggerKey { override val allowedDoublePress: Boolean = true override val allowedLongPress: Boolean = true @@ -40,8 +40,12 @@ data class EvdevTriggerKey( uid = entity.uid, keyCode = entity.keyCode, scanCode = entity.scanCode, - deviceDescriptor = entity.deviceDescriptor, - deviceName = entity.deviceName, + device = EvdevDeviceInfo( + name = entity.deviceName, + bus = entity.deviceBus, + vendor = entity.deviceVendor, + product = entity.deviceProduct, + ), clickType = clickType, consumeEvent = consumeEvent, ) @@ -63,12 +67,15 @@ data class EvdevTriggerKey( return EvdevTriggerKeyEntity( keyCode = key.keyCode, scanCode = key.scanCode, - deviceDescriptor = key.deviceDescriptor, - deviceName = key.deviceName, + deviceName = key.device.name, + deviceBus = key.device.bus, + deviceVendor = key.device.vendor, + deviceProduct = key.device.product, clickType = clickType, flags = flags, uid = key.uid ) } } + } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt new file mode 100644 index 0000000000..5bcbcafa73 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt @@ -0,0 +1,14 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.keymaps.ClickType + +sealed interface InputEventTriggerKey { + val keyCode: Int + + /** + * Scancodes were only saved to KeyEvent trigger keys in version 4.0.0 so this is null + * to be backwards compatible. + */ + val scanCode: Int? + val clickType: ClickType +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt deleted file mode 100644 index 5e12f7616d..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -import io.github.sds100.keymapper.base.keymaps.ClickType - -/** - * This is a type for trigger keys that are detected by key code. This is a different meaning to - * key *event*. - */ -sealed interface KeyCodeTriggerKey { - val keyCode: Int - val scanCode: Int? - val clickType: ClickType -} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt index 1d4fb07589..73f2425287 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -21,7 +21,7 @@ data class KeyEventTriggerKey( */ val requiresIme: Boolean = false, override val scanCode: Int? = null, -) : TriggerKey(), KeyCodeTriggerKey { +) : TriggerKey(), InputEventTriggerKey { override val allowedLongPress: Boolean = true override val allowedDoublePress: Boolean = true @@ -32,7 +32,7 @@ data class KeyEventTriggerKey( is KeyEventTriggerDevice.External -> "external" KeyEventTriggerDevice.Internal -> "internal" } - return "KeyCodeTriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeEvent) " + return "InputEventTriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeEvent) " } // key code -> click type -> device -> consume key event diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 0d69cc1172..767ffddb8b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -7,7 +7,6 @@ import io.github.sds100.keymapper.base.input.InputEventHubCallback import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success -import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.isError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent @@ -62,6 +61,7 @@ class RecordTriggerControllerImpl @Inject constructor( * is received and it is cleared when recording starts. */ private val downKeyEvents: MutableSet = mutableSetOf() + private val downEvdevEvents: MutableSet = mutableSetOf() private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() override fun onInputEvent( @@ -74,9 +74,29 @@ class RecordTriggerControllerImpl @Inject constructor( when (event) { is KMEvdevEvent -> { - // Do nothing if receiving an evdev event that hasn't already been - // converted into a key event - return false + // Do not record evdev events that are not key events. + if (event.type != KMEvdevEvent.TYPE_KEY_EVENT) { + return false + } + + Timber.d("Recorded evdev event ${event.code} ${KeyEvent.keyCodeToString(event.androidCode)}") + + // Must also remove old down events if a new down even is received. + val matchingDownEvent: KMEvdevEvent? = downEvdevEvents.find { + it == event.copy(value = KMEvdevEvent.VALUE_DOWN) + } + + if (matchingDownEvent != null) { + downEvdevEvents.remove(matchingDownEvent) + } + + if (event.isDownEvent) { + downEvdevEvents.add(event) + } else if (event.isUpEvent) { + onRecordKey(createEvdevRecordedKey(event)) + } + + return true } is KMGamePadEvent -> { @@ -86,7 +106,7 @@ class RecordTriggerControllerImpl @Inject constructor( if (keyEvent.action == KeyEvent.ACTION_DOWN) { Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") - val recordedKey = createRecordedKey( + val recordedKey = createKeyEventRecordedKey( keyEvent, detectionSource ) @@ -108,7 +128,7 @@ class RecordTriggerControllerImpl @Inject constructor( if (matchingDownEvent != null) { downKeyEvents.remove(matchingDownEvent) } - + if (event.action == KeyEvent.ACTION_DOWN) { downKeyEvents.add(event) } else if (event.action == KeyEvent.ACTION_UP) { @@ -117,7 +137,7 @@ class RecordTriggerControllerImpl @Inject constructor( // Do not do this when recording motion events from the input method // or Activity because they intentionally only input a down event. if (matchingDownEvent != null) { - val recordedKey = createRecordedKey(event, detectionSource) + val recordedKey = createKeyEventRecordedKey(event, detectionSource) onRecordKey(recordedKey) } @@ -173,7 +193,7 @@ class RecordTriggerControllerImpl @Inject constructor( } if (keyEvent.action == KeyEvent.ACTION_UP) { - val recordedKey = createRecordedKey( + val recordedKey = createKeyEventRecordedKey( keyEvent, InputEventDetectionSource.INPUT_METHOD, ) @@ -192,11 +212,11 @@ class RecordTriggerControllerImpl @Inject constructor( runBlocking { onRecordKey.emit(recordedKey) } } - private fun createRecordedKey( + private fun createKeyEventRecordedKey( keyEvent: KMKeyEvent, detectionSource: InputEventDetectionSource - ): RecordedKey { - return RecordedKey( + ): RecordedKey.KeyEvent { + return RecordedKey.KeyEvent( keyCode = keyEvent.keyCode, scanCode = keyEvent.scanCode, deviceDescriptor = keyEvent.device.descriptor, @@ -206,6 +226,14 @@ class RecordTriggerControllerImpl @Inject constructor( ) } + private fun createEvdevRecordedKey(evdevEvent: KMEvdevEvent): RecordedKey.EvdevEvent { + return RecordedKey.EvdevEvent( + keyCode = evdevEvent.androidCode, + scanCode = evdevEvent.code, + device = evdevEvent.device + ) + } + // Run on a different thread in case the main thread is locked up while recording and // the evdev devices aren't ungrabbed. private fun recordTriggerJob(): Job = coroutineScope.launch(Dispatchers.Default) { @@ -213,15 +241,14 @@ class RecordTriggerControllerImpl @Inject constructor( dpadMotionEventTracker.reset() downKeyEvents.clear() - inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this@RecordTriggerControllerImpl) - - val inputDevices = devicesAdapter.connectedInputDevices.value.dataOrNull() + inputEventHub.registerClient( + INPUT_EVENT_HUB_ID, + this@RecordTriggerControllerImpl, + listOf(KMEvdevEvent.TYPE_KEY_EVENT) + ) // Grab all evdev devices - if (inputDevices != null) { - val allDeviceDescriptors = inputDevices.map { it.descriptor }.toList() - inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, allDeviceDescriptors) - } + inputEventHub.grabAllEvdevDevices(INPUT_EVENT_HUB_ID) repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt index 9cf7e89993..f8fb70d3a4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt @@ -1,12 +1,21 @@ package io.github.sds100.keymapper.base.trigger import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle -data class RecordedKey( - val keyCode: Int, - val scanCode: Int, - val deviceDescriptor: String, - val deviceName: String, - val isExternalDevice: Boolean, - val detectionSource: InputEventDetectionSource, -) +sealed class RecordedKey { + data class KeyEvent( + val keyCode: Int, + val scanCode: Int, + val deviceDescriptor: String, + val deviceName: String, + val isExternalDevice: Boolean, + val detectionSource: InputEventDetectionSource, + ) : RecordedKey() + + data class EvdevEvent( + val keyCode: Int, + val scanCode: Int, + val device: EvdevDeviceHandle + ) : RecordedKey() +} \ No newline at end of file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 887dad011b..0438ba808d 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -4411,7 +4411,7 @@ class KeyMapAlgorithmTest { metaState: Int? = null, scanCode: Int = 0, repeatCount: Int = 0, - ): Boolean = controller.onKeyEvent( + ): Boolean = controller.onInputEvent( KMKeyEvent( keyCode = keyCode, action = action, diff --git a/common/build.gradle.kts b/common/build.gradle.kts index bce29e1b31..d438004789 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -26,6 +26,11 @@ android { ) } } + + buildFeatures { + aidl = true + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl b/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl new file mode 100644 index 0000000000..8ea6d7f489 --- /dev/null +++ b/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl @@ -0,0 +1,3 @@ +package io.github.sds100.keymapper.common.models; + +parcelable EvdevDeviceHandle; \ No newline at end of file diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt new file mode 100644 index 0000000000..cb8cdcb1b5 --- /dev/null +++ b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt @@ -0,0 +1,16 @@ +package io.github.sds100.keymapper.common.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class EvdevDeviceHandle( + /** + * The path to the device. E.g /dev/input/event1 + */ + val path: String, + val name: String, + val bus: Int, + val vendor: Int, + val product: Int, +) : Parcelable \ No newline at end of file diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt new file mode 100644 index 0000000000..1e8c32e648 --- /dev/null +++ b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt @@ -0,0 +1,12 @@ +package io.github.sds100.keymapper.common.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class EvdevDeviceInfo( + val name: String, + val bus: Int, + val vendor: Int, + val product: Int +) : Parcelable diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt index 8ec0faf043..d52d869985 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt @@ -13,12 +13,18 @@ data class EvdevTriggerKeyEntity( @SerializedName(NAME_SCANCODE) val scanCode: Int, - @SerializedName(NAME_DEVICE_DESCRIPTOR) - val deviceDescriptor: String, - @SerializedName(NAME_DEVICE_NAME) val deviceName: String, + @SerializedName(NAME_DEVICE_BUS) + val deviceBus: Int, + + @SerializedName(NAME_DEVICE_VENDOR) + val deviceVendor: Int, + + @SerializedName(NAME_DEVICE_PRODUCT) + val deviceProduct: Int, + @SerializedName(NAME_CLICK_TYPE) override val clickType: Int = SHORT_PRESS, @@ -34,8 +40,10 @@ data class EvdevTriggerKeyEntity( // DON'T CHANGE THESE. Used for JSON serialization and parsing. const val NAME_KEYCODE = "keyCode" const val NAME_SCANCODE = "scanCode" - const val NAME_DEVICE_DESCRIPTOR = "deviceDescriptor" const val NAME_DEVICE_NAME = "deviceName" + const val NAME_DEVICE_BUS = "deviceBus" + const val NAME_DEVICE_VENDOR = "deviceVendor" + const val NAME_DEVICE_PRODUCT = "deviceProduct" const val NAME_FLAGS = "flags" const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1 diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt index a5b4719d4a..d8370e2c9e 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt @@ -61,7 +61,7 @@ sealed class TriggerKeyEntity : Parcelable { return@jsonDeserializer deserializeFingerprintTriggerKey(json, uid!!) } - json.obj.has(EvdevTriggerKeyEntity.NAME_DEVICE_DESCRIPTOR) -> { + json.obj.has(EvdevTriggerKeyEntity.NAME_DEVICE_PRODUCT) -> { return@jsonDeserializer deserializeEvdevTriggerKey(json, uid!!) } @@ -107,16 +107,20 @@ sealed class TriggerKeyEntity : Parcelable { ): EvdevTriggerKeyEntity { val keyCode by json.byInt(EvdevTriggerKeyEntity.NAME_KEYCODE) val scanCode by json.byInt(EvdevTriggerKeyEntity.NAME_SCANCODE) - val deviceDescriptor by json.byString(EvdevTriggerKeyEntity.NAME_DEVICE_DESCRIPTOR) val deviceName by json.byString(EvdevTriggerKeyEntity.NAME_DEVICE_NAME) + val deviceVendor by json.byInt(EvdevTriggerKeyEntity.NAME_DEVICE_VENDOR) + val deviceProduct by json.byInt(EvdevTriggerKeyEntity.NAME_DEVICE_PRODUCT) + val deviceBus by json.byInt(EvdevTriggerKeyEntity.NAME_DEVICE_BUS) val clickType by json.byInt(NAME_CLICK_TYPE) val flags by json.byNullableInt(EvdevTriggerKeyEntity.NAME_FLAGS) return EvdevTriggerKeyEntity( keyCode, scanCode, - deviceDescriptor, deviceName, + deviceBus, + deviceVendor, + deviceProduct, clickType, flags ?: 0, uid, diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl index 75dfa75bae..5b64744741 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl @@ -2,5 +2,5 @@ package io.github.sds100.keymapper.sysbridge; interface IEvdevCallback { void onEvdevEventLoopStarted(); - boolean onEvdevEvent(int deviceId, long timeSec, long timeUsec, int type, int code, int value, int androidCode); + boolean onEvdevEvent(String devicePath, long timeSec, long timeUsec, int type, int code, int value, int androidCode); } \ No newline at end of file diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 09b7a8a74d..6c2df2c6fd 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -1,21 +1,27 @@ package io.github.sds100.keymapper.sysbridge; import io.github.sds100.keymapper.sysbridge.IEvdevCallback; -import io.github.sds100.keymapper.sysbridge.utils.InputDeviceIdentifier; +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle; import android.view.InputEvent; interface ISystemBridge { // Destroy method defined by Shizuku server. This is required // for Shizuku user services. // See demo/service/UserService.java in the Shizuku-API repository. - // TODO is this used? + // TODO use this from Key Mapper to kill the system bridge void destroy() = 16777114; - boolean grabEvdevDevice(int deviceId) = 1; - boolean ungrabEvdevDevice(int deviceId) = 2; - boolean ungrabAllEvdevDevices() = 3; - void registerEvdevCallback(IEvdevCallback callback) = 4; - void unregisterEvdevCallback() = 5; - boolean writeEvdevEvent(int deviceId, int type, int code, int value) = 6; - boolean injectInputEvent(in InputEvent event, int mode) = 7; + boolean grabEvdevDevice(String devicePath) = 1; + boolean grabAllEvdevDevices() = 2; + + boolean ungrabEvdevDevice(String devicePath) = 3; + boolean ungrabAllEvdevDevices() = 4; + + void registerEvdevCallback(IEvdevCallback callback) = 5; + void unregisterEvdevCallback() = 6; + + boolean writeEvdevEvent(String devicePath, int type, int code, int value) = 7; + boolean injectInputEvent(in InputEvent event, int mode) = 8; + + EvdevDeviceHandle[] getEvdevInputDevices() = 9; } \ No newline at end of file diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.aidl deleted file mode 100644 index 4b942ea113..0000000000 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.aidl +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.sds100.keymapper.sysbridge.utils; - -parcelable InputDeviceIdentifier; \ No newline at end of file diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index 523b40c0ff..04bbee8896 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -37,8 +37,13 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { ::ndk::ScopedAStatus onEvdevEventLoopStarted() override { return _impl->onEvdevEventLoopStarted(); } - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override { - return _impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); + + ::ndk::ScopedAStatus + onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, + int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, + bool *_aidl_return) override { + return _impl->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, + in_value, in_androidCode, _aidl_return); } protected: private: diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index 463820b1ce..563f3b4c20 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -20,7 +20,11 @@ class BpEvdevCallback : public ::ndk::BpCInterface { virtual ~BpEvdevCallback(); ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; + + ::ndk::ScopedAStatus + onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, + int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, + bool *_aidl_return) override; }; } // namespace sysbridge } // namespace keymapper diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index e62851aa64..d7fdfb6598 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -31,7 +31,7 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback break; } case (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/): { - int32_t in_deviceId; + std::string in_devicePath; int64_t in_timeSec; int64_t in_timeUsec; int32_t in_type; @@ -40,7 +40,7 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback int32_t in_androidCode; bool _aidl_return; - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_deviceId); + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_devicePath); if (_aidl_ret_status != STATUS_OK) break; _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeSec); @@ -61,7 +61,10 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_androidCode); if (_aidl_ret_status != STATUS_OK) break; - ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, &_aidl_return); + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_devicePath, in_timeSec, + in_timeUsec, in_type, in_code, + in_value, in_androidCode, + &_aidl_return); _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); if (_aidl_ret_status != STATUS_OK) break; @@ -115,7 +118,11 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { _aidl_status_return: return _aidl_status; } -::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) { + + ::ndk::ScopedAStatus + BpEvdevCallback::onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, + int64_t in_timeUsec, int32_t in_type, int32_t in_code, + int32_t in_value, int32_t in_androidCode, bool *_aidl_return) { binder_status_t _aidl_ret_status = STATUS_OK; ::ndk::ScopedAStatus _aidl_status; ::ndk::ScopedAParcel _aidl_in; @@ -124,7 +131,7 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_deviceId); + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_devicePath); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeSec); @@ -156,7 +163,10 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(int32_t in_deviceId, int64_t #endif // BINDER_STABILITY_SUPPORT ); if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { - _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_deviceId, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_devicePath, in_timeSec, + in_timeUsec, in_type, in_code, + in_value, in_androidCode, + _aidl_return); goto _aidl_status_return; } if (_aidl_ret_status != STATUS_OK) goto _aidl_error; @@ -237,7 +247,15 @@ ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEventLoopStarted() { _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; } -::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(int32_t /*in_deviceId*/, int64_t /*in_timeSec*/, int64_t /*in_timeUsec*/, int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/, int32_t /*in_androidCode*/, bool* /*_aidl_return*/) { + + ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(const std::string & /*in_devicePath*/, + int64_t /*in_timeSec*/, + int64_t /*in_timeUsec*/, + int32_t /*in_type*/, + int32_t /*in_code*/, + int32_t /*in_value*/, + int32_t /*in_androidCode*/, + bool * /*_aidl_return*/) { ::ndk::ScopedAStatus _aidl_status; _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index e3cc7dcf53..4f0c1471dc 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -38,14 +38,22 @@ class IEvdevCallback : public ::ndk::ICInterface { static bool setDefaultImpl(const std::shared_ptr& impl); static const std::shared_ptr& getDefaultImpl(); virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; - virtual ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) = 0; + + virtual ::ndk::ScopedAStatus + onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, + int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, + bool *_aidl_return) = 0; private: static std::shared_ptr default_impl; }; class IEvdevCallbackDefault : public IEvdevCallback { public: ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; - ::ndk::ScopedAStatus onEvdevEvent(int32_t in_deviceId, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; + + ::ndk::ScopedAStatus + onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, + int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, + bool *_aidl_return) override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; }; diff --git a/sysbridge/src/main/cpp/android/input/InputDevice.cpp b/sysbridge/src/main/cpp/android/input/InputDevice.cpp index 5fe45f94d5..66c72d20a7 100644 --- a/sysbridge/src/main/cpp/android/input/InputDevice.cpp +++ b/sysbridge/src/main/cpp/android/input/InputDevice.cpp @@ -138,8 +138,10 @@ namespace android { } return path; } else if (errno != ENOENT) { - LOGW("Couldn't find a system-provided input device configuration file at %s due to error %d (%s); there may be an IDC file there that cannot be loaded.", - path.c_str(), errno, strerror(errno)); + if (DEBUG_PROBE) { + LOGW("Couldn't find a system-provided input device configuration file at %s due to error %d (%s); there may be an IDC file there that cannot be loaded.", + path.c_str(), errno, strerror(errno)); + } } else { if (DEBUG_PROBE) { LOGE("Didn't find system-provided input device configuration file at %s: %s", diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 5cdbb85026..a05f1e97c3 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -16,17 +16,17 @@ #include #include #include +#include #include using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; struct GrabData { - int deviceId; - android::InputDeviceIdentifier identifier; + char devicePath[256]; }; struct UngrabData { - int deviceId; + char devicePath[256]; }; enum CommandType { @@ -41,16 +41,12 @@ struct Command { }; struct DeviceContext { - int deviceId; - struct android::InputDeviceIdentifier inputDeviceIdentifier; struct libevdev *evdev; struct libevdev_uinput *uinputDev; struct android::KeyLayoutMap keyLayoutMap; char devicePath[256]; }; -void ungrabDevice(jint device_id); - static int epollFd = -1; static int commandEventFd = -1; @@ -142,32 +138,6 @@ static int findEvdevDevice( return -1; } -android::InputDeviceIdentifier -convertJInputDeviceIdentifier(JNIEnv *env, jobject jInputDeviceIdentifier) { - android::InputDeviceIdentifier deviceIdentifier; - - jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); - - jfieldID busFieldId = env->GetFieldID(inputDeviceIdentifierClass, "bus", "I"); - deviceIdentifier.bus = env->GetIntField(jInputDeviceIdentifier, busFieldId); - - jfieldID vendorFieldId = env->GetFieldID(inputDeviceIdentifierClass, "vendor", "I"); - deviceIdentifier.vendor = env->GetIntField(jInputDeviceIdentifier, vendorFieldId); - - jfieldID productFieldId = env->GetFieldID(inputDeviceIdentifierClass, "product", "I"); - deviceIdentifier.product = env->GetIntField(jInputDeviceIdentifier, productFieldId); - - jfieldID nameFieldId = env->GetFieldID(inputDeviceIdentifierClass, "name", - "Ljava/lang/String;"); - auto nameString = (jstring) env->GetObjectField(jInputDeviceIdentifier, nameFieldId); - - const char *nameChars = env->GetStringUTFChars(nameString, nullptr); - deviceIdentifier.name = std::string(nameChars); - env->ReleaseStringUTFChars(nameString, nameChars); - - return deviceIdentifier; -} - jint JNI_OnLoad(JavaVM *vm, void *reserved) { evdevDevices = new std::map(); return JNI_VERSION_1_6; @@ -177,14 +147,20 @@ extern "C" JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNative(JNIEnv *env, jobject thiz, - jobject jInputDeviceIdentifier) { - jclass inputDeviceIdentifierClass = env->GetObjectClass(jInputDeviceIdentifier); - jfieldID idFieldId = env->GetFieldID(inputDeviceIdentifierClass, "id", "I"); - android::InputDeviceIdentifier identifier = convertJInputDeviceIdentifier(env, - jInputDeviceIdentifier); - int deviceId = env->GetIntField(jInputDeviceIdentifier, idFieldId); + jstring jDevicePath) { + // TODO does this really need epoll now with the looper and handler? Can't it just be done here? Then one can return actually legit error codes - Command cmd = {GRAB, GrabData{deviceId, identifier}}; + const char *devicePath = env->GetStringUTFChars(jDevicePath, nullptr); + if (devicePath == nullptr) { + return false; + } + + Command cmd; + cmd.type = GRAB; + cmd.data = GrabData{}; + strcpy(std::get(cmd.data).devicePath, devicePath); + + env->ReleaseStringUTFChars(jDevicePath, devicePath); std::lock_guard lock(commandMutex); commandQueue.push(cmd); @@ -210,11 +186,10 @@ void onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { if (rc == LIBEVDEV_READ_STATUS_SUCCESS) { // rc == 0 int32_t outKeycode = -1; uint32_t outFlags = -1; - int deviceId = deviceContext->deviceId; deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); bool returnValue; - callback->onEvdevEvent(deviceId, + callback->onEvdevEvent(deviceContext->devicePath, inputEvent.time.tv_sec, inputEvent.time.tv_usec, inputEvent.type, @@ -251,14 +226,20 @@ void handleCommand(const Command &cmd) { struct libevdev *dev = nullptr; char devicePath[256]; - int rc = findEvdevDevice(data.identifier.name, - data.identifier.bus, - data.identifier.vendor, - data.identifier.product, - &dev, - devicePath); - if (rc < 0) { - LOGE("Failed to find device for grab command"); + strcpy(devicePath, data.devicePath); + + // MUST be NONBLOCK so that the loop reading the evdev events eventually returns + // due to an EAGAIN error. + int fd = open(devicePath, O_RDONLY | O_NONBLOCK); + if (fd == -1) { + LOGE("Failed to open device %s: %s", devicePath, strerror(errno)); + return; + } + + int rc = libevdev_new_from_fd(fd, &dev); + if (rc != 0) { + LOGE("Failed to create libevdev device from %s: %s", devicePath, strerror(errno)); + close(fd); return; } @@ -267,9 +248,9 @@ void handleCommand(const Command &cmd) { for (const auto &pair: *evdevDevices) { DeviceContext context = pair.second; if (strcmp(context.devicePath, devicePath) == 0) { - LOGW("Device %s %s is already grabbed. Maybe it is a virtual uinput device.", - data.identifier.name.c_str(), + LOGW("Device %s is already grabbed. Maybe it is a virtual uinput device.", devicePath); + libevdev_free(dev); return; } } @@ -284,13 +265,22 @@ void handleCommand(const Command &cmd) { } int evdevFd = libevdev_get_fd(dev); + + // Create a dummy InputDeviceIdentifier for key layout loading + android::InputDeviceIdentifier identifier; + identifier.name = std::string(libevdev_get_name(dev)); + identifier.bus = libevdev_get_id_bustype(dev); + identifier.vendor = libevdev_get_id_vendor(dev); + identifier.product = libevdev_get_id_product(dev); + std::string klPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( - data.identifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); + identifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); auto klResult = android::KeyLayoutMap::load(klPath, nullptr); if (!klResult.ok()) { LOGE("key layout map not found for device %s", libevdev_get_name(dev)); + libevdev_free(dev); return; } @@ -311,12 +301,11 @@ void handleCommand(const Command &cmd) { return; } - DeviceContext context{ - data.deviceId, - data.identifier, + DeviceContext context = { dev, uinputDev, *klResult.value(), + *data.devicePath, }; strcpy(context.devicePath, devicePath); @@ -336,8 +325,7 @@ void handleCommand(const Command &cmd) { // TODO add device input file to epoll so the state is cleared when it is disconnected. Remember to remove the epoll after it has disconnected. - LOGI("Grabbed device %d, %s, %s", context.deviceId, - context.inputDeviceIdentifier.name.c_str(), context.devicePath); + LOGI("Grabbed device %s, %s", libevdev_get_name(dev), context.devicePath); } else if (cmd.type == UNGRAB) { @@ -345,8 +333,7 @@ void handleCommand(const Command &cmd) { std::lock_guard lock(evdevDevicesMutex); for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { - int deviceId = it->second.deviceId; - if (it->second.deviceId == data.deviceId) { + if (strcmp(it->second.devicePath, data.devicePath) == 0) { // Do this before freeing the evdev file descriptor libevdev_uinput_destroy(it->second.uinputDev); @@ -357,7 +344,7 @@ void handleCommand(const Command &cmd) { libevdev_free(it->second.evdev); evdevDevices->erase(it); - LOGI("Ungrabbed device %d", deviceId); + LOGI("Ungrabbed device %s", data.devicePath); break; } } @@ -464,10 +451,18 @@ extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, jobject thiz, - jint deviceId) { + jstring jDevicePath) { + const char *devicePath = env->GetStringUTFChars(jDevicePath, nullptr); + if (devicePath == nullptr) { + return; + } + Command cmd; cmd.type = UNGRAB; - cmd.data = UngrabData{deviceId}; + cmd.data = UngrabData{}; + strcpy(std::get(cmd.data).devicePath, devicePath); + + env->ReleaseStringUTFChars(jDevicePath, devicePath); { std::lock_guard lock(commandMutex); @@ -504,32 +499,54 @@ extern "C" JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNative(JNIEnv *env, jobject thiz, - jint deviceId, + jstring jDevicePath, jint type, jint code, jint value) { - // TODO: implement writeEvdevEvent() + const char *devicePath = env->GetStringUTFChars(jDevicePath, nullptr); + if (devicePath == nullptr) { + return false; + } + + bool result = false; + { + std::lock_guard lock(evdevDevicesMutex); + for (const auto &pair: *evdevDevices) { + if (strcmp(pair.second.devicePath, devicePath) == 0) { + int rc = libevdev_uinput_write_event(pair.second.uinputDev, type, code, value); + if (rc == 0) { + rc = libevdev_uinput_write_event(pair.second.uinputDev, EV_SYN, SYN_REPORT, 0); + } + result = (rc == 0); + break; + } + } + } + + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return result; } extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDevicesNative( JNIEnv *env, jobject thiz) { - std::vector deviceIds; + std::vector devicePaths; { std::lock_guard evdevLock(evdevDevicesMutex); for (auto pair: *evdevDevices) { - deviceIds.push_back(pair.second.deviceId); + devicePaths.push_back(std::string(pair.second.devicePath)); } } std::lock_guard commandLock(commandMutex); - for (int id: deviceIds) { + for (const std::string &path: devicePaths) { Command cmd; cmd.type = UNGRAB; - cmd.data = UngrabData{id}; + cmd.data = UngrabData{}; + strcpy(std::get(cmd.data).devicePath, path.c_str()); commandQueue.push(cmd); } @@ -539,4 +556,133 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDev if (written < 0) { LOGE("Failed to write to commandEventFd: %s", strerror(errno)); } +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabAllEvdevDevicesNative( + JNIEnv *env, jobject thiz) { + // TODO: implement grabAllEvdevDevicesNative() +} + +// Helper function to create a Java EvdevDeviceHandle object +jobject +createEvdevDeviceHandle(JNIEnv *env, const char *path, const char *name, int bus, int vendor, + int product) { + // Find the EvdevDeviceHandle class + jclass evdevDeviceHandleClass = env->FindClass( + "io/github/sds100/keymapper/common/models/EvdevDeviceHandle"); + if (evdevDeviceHandleClass == nullptr) { + LOGE("Failed to find EvdevDeviceHandle class"); + return nullptr; + } + + // Get the constructor + jmethodID constructor = env->GetMethodID(evdevDeviceHandleClass, "", + "(Ljava/lang/String;Ljava/lang/String;III)V"); + if (constructor == nullptr) { + LOGE("Failed to find EvdevDeviceHandle constructor"); + return nullptr; + } + + // Create Java strings + jstring jPath = env->NewStringUTF(path); + jstring jName = env->NewStringUTF(name); + + // Create the object + jobject evdevDeviceHandle = env->NewObject(evdevDeviceHandleClass, constructor, jPath, jName, + bus, vendor, product); + + // Clean up local references + env->DeleteLocalRef(jPath); + env->DeleteLocalRef(jName); + env->DeleteLocalRef(evdevDeviceHandleClass); + + return evdevDeviceHandle; +} + +extern "C" +JNIEXPORT jobjectArray JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_getEvdevDevicesNative(JNIEnv *env, + jobject thiz) { + DIR *dir = opendir("/dev/input"); + + if (dir == nullptr) { + LOGE("Failed to open /dev/input directory"); + return nullptr; + } + + std::vector deviceHandles; + struct dirent *entry; + + while ((entry = readdir(dir)) != nullptr) { + // Skip . and .. entries + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + char fullPath[256]; + snprintf(fullPath, sizeof(fullPath), "/dev/input/%s", entry->d_name); + + int fd = open(fullPath, O_RDONLY); + + if (fd == -1) { + continue; + } + + struct libevdev *dev = nullptr; + int status = libevdev_new_from_fd(fd, &dev); + + if (status != 0) { + LOGE("Failed to open libevdev device from path %s: %s", fullPath, strerror(errno)); + close(fd); + continue; + } + + const char *devName = libevdev_get_name(dev); + int devVendor = libevdev_get_id_vendor(dev); + int devProduct = libevdev_get_id_product(dev); + int devBus = libevdev_get_id_bustype(dev); + + if (DEBUG_PROBE) { + LOGD("Evdev device: %s, bus: %d, vendor: %d, product: %d, path: %s", + devName, devBus, devVendor, devProduct, fullPath); + } + + // Create EvdevDeviceHandle object + jobject deviceHandle = createEvdevDeviceHandle(env, fullPath, devName, devBus, devVendor, + devProduct); + if (deviceHandle != nullptr) { + deviceHandles.push_back(deviceHandle); + } + + libevdev_free(dev); + close(fd); + } + + closedir(dir); + + // Create the Java array + jclass evdevDeviceHandleClass = env->FindClass( + "io/github/sds100/keymapper/common/models/EvdevDeviceHandle"); + if (evdevDeviceHandleClass == nullptr) { + LOGE("Failed to find EvdevDeviceHandle class for array creation"); + return nullptr; + } + + jobjectArray result = env->NewObjectArray(deviceHandles.size(), evdevDeviceHandleClass, + nullptr); + if (result == nullptr) { + LOGE("Failed to create EvdevDeviceHandle array"); + env->DeleteLocalRef(evdevDeviceHandleClass); + return nullptr; + } + + // Fill the array + for (size_t i = 0; i < deviceHandles.size(); i++) { + env->SetObjectArrayElement(result, i, deviceHandles[i]); + env->DeleteLocalRef(deviceHandles[i]); // Clean up local reference + } + + env->DeleteLocalRef(evdevDeviceHandleClass); + return result; } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 13cd155a9b..3ab5bd8967 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -12,16 +12,13 @@ import android.os.IBinder import android.os.Looper import android.os.ServiceManager import android.util.Log -import android.view.InputDevice import android.view.InputEvent -import io.github.sds100.keymapper.common.utils.getBluetoothAddress -import io.github.sds100.keymapper.common.utils.getDeviceBus +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.provider.BinderContainer import io.github.sds100.keymapper.sysbridge.provider.SystemBridgeBinderProvider import io.github.sds100.keymapper.sysbridge.utils.IContentProviderUtils -import io.github.sds100.keymapper.sysbridge.utils.InputDeviceIdentifier import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope @@ -42,13 +39,19 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO return error code and map this to a SystemBridgeError in key mapper - external fun grabEvdevDeviceNative( - deviceIdentifier: InputDeviceIdentifier - ): Boolean + external fun grabEvdevDeviceNative(devicePath: String): Boolean + external fun grabAllEvdevDevicesNative(): Boolean - external fun ungrabEvdevDeviceNative(deviceId: Int) + external fun ungrabEvdevDeviceNative(devicePath: String) external fun ungrabAllEvdevDevicesNative() - external fun writeEvdevEventNative(deviceId: Int, type: Int, code: Int, value: Int): Boolean + external fun writeEvdevEventNative( + devicePath: String, + type: Int, + code: Int, + value: Int + ): Boolean + + external fun getEvdevDevicesNative(): Array external fun startEvdevEventLoop(callback: IBinder) external fun stopEvdevEventLoop() @@ -254,50 +257,37 @@ internal class SystemBridge : ISystemBridge.Stub() { } } - // TODO passthrough a timeout that will automatically ungrab after that time. - override fun grabEvdevDevice( - deviceId: Int, - ): Boolean { - val inputDevice = inputManager.getInputDevice(deviceId) ?: return false - - // Don't grab any virtual devices - if (inputDevice.isVirtual) { - Log.i(TAG, "Not grabbing virtual device: $deviceId") - return false - } + // TODO passthrough a timeout that will automatically ungrab after that time. Use this when recording. + override fun grabEvdevDevice(devicePath: String?): Boolean { + devicePath ?: return false + return grabEvdevDeviceNative(devicePath) + } - val deviceIdentifier = buildInputDeviceIdentifier(inputDevice) ?: return false - return grabEvdevDeviceNative(deviceIdentifier) + override fun grabAllEvdevDevices(): Boolean { + return grabAllEvdevDevicesNative() } - override fun ungrabEvdevDevice(deviceId: Int): Boolean { - ungrabEvdevDeviceNative(deviceId) + override fun ungrabEvdevDevice(devicePath: String?): Boolean { + devicePath ?: return false + ungrabEvdevDeviceNative(devicePath) return true } override fun ungrabAllEvdevDevices(): Boolean { - Log.i(TAG, "Start ungrab all evdev devices") ungrabAllEvdevDevicesNative() return true } - private fun buildInputDeviceIdentifier(inputDevice: InputDevice): InputDeviceIdentifier? { - return InputDeviceIdentifier( - id = inputDevice.id, - name = inputDevice.name ?: return null, - vendor = inputDevice.vendorId, - product = inputDevice.productId, - descriptor = inputDevice.descriptor ?: return null, - bus = inputDevice.getDeviceBus(), - bluetoothAddress = inputDevice.getBluetoothAddress() - ) - } - override fun injectInputEvent(event: InputEvent?, mode: Int): Boolean { return inputManager.injectInputEvent(event, mode) } - override fun writeEvdevEvent(deviceId: Int, type: Int, code: Int, value: Int): Boolean { - return writeEvdevEventNative(deviceId, type, code, value) + override fun getEvdevInputDevices(): Array? { + return getEvdevDevicesNative() + } + + override fun writeEvdevEvent(devicePath: String?, type: Int, code: Int, value: Int): Boolean { + devicePath ?: return false + return writeEvdevEventNative(devicePath, type, code, value) } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt deleted file mode 100644 index 286f0648f6..0000000000 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/InputDeviceIdentifier.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.sds100.keymapper.sysbridge.utils - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -internal data class InputDeviceIdentifier( - val id: Int, - val name: String, - val bus: Int, - val vendor: Int, - val product: Int, - val descriptor: String, - val bluetoothAddress: String? -) : Parcelable diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index e907ffcb4f..72bd687201 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -123,20 +122,6 @@ class AndroidDevicesAdapter @Inject constructor( }.launchIn(coroutineScope) } - fun logConnectedInputDevices() { - val deviceIds = inputManager?.inputDeviceIds ?: return - for (deviceId in deviceIds) { - val device = InputDevice.getDevice(deviceId) ?: continue - - val supportedSources: String = InputDeviceUtils.SOURCE_NAMES - .filter { device.supportsSource(it.key) } - .values - .joinToString() - - Timber.d("Input device: ${device.id} ${device.name} Vendor=${device.vendorId} Product=${device.productId} Descriptor=${device.descriptor} Sources=$supportedSources") - } - } - override fun deviceHasKey(id: Int, keyCode: Int): Boolean { val device = InputDevice.getDevice(id) ?: return false @@ -159,18 +144,6 @@ class AndroidDevicesAdapter @Inject constructor( return InputDevice.getDevice(deviceId)?.let { InputDeviceUtils.createInputDeviceInfo(it) } } - override fun getInputDevicesNow(): List { - val devices = mutableListOf() - - for (id in InputDevice.getDeviceIds()) { - val device = InputDevice.getDevice(id) ?: continue - - devices.add(InputDeviceUtils.createInputDeviceInfo(device)) - } - - return devices - } - private fun updateInputDevices() { val devices = mutableListOf() diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt index 42860699ec..f82be90881 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt @@ -18,5 +18,4 @@ interface DevicesAdapter { fun deviceHasKey(id: Int, keyCode: Int): Boolean fun getInputDeviceName(descriptor: String): KMResult fun getInputDevice(deviceId: Int): InputDeviceInfo? - fun getInputDevicesNow(): List } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt index 458c731697..424f869477 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt @@ -1,25 +1,36 @@ package io.github.sds100.keymapper.system.inputevents +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle + data class KMEvdevEvent( - override val deviceId: Int, + val device: EvdevDeviceHandle, val type: Int, val code: Int, val value: Int, - - // This is only non null when receiving an event. If sending an event - // then these values do not need to be set. - val androidCode: Int? = null, - val timeSec: Long? = null, - val timeUsec: Long? = null + val androidCode: Int, + val timeSec: Long, + val timeUsec: Long ) : KMInputEvent { + companion object { + const val TYPE_SYN_EVENT = 0 + const val TYPE_KEY_EVENT = 1 + const val TYPE_REL_EVENT = 2 + + const val VALUE_DOWN = 1 + const val VALUE_UP = 0 + } + // Look at input-event-codes.h for where these are defined. // EV_SYN - val isSynEvent: Boolean = type == 0 + val isSynEvent: Boolean = type == TYPE_SYN_EVENT // EV_KEY - val isKeyEvent: Boolean = type == 1 + val isKeyEvent: Boolean = type == TYPE_KEY_EVENT // EV_REL - val isRelEvent: Boolean = type == 2 + val isRelEvent: Boolean = type == TYPE_REL_EVENT + + val isDownEvent: Boolean = isKeyEvent && value == VALUE_DOWN + val isUpEvent: Boolean = isKeyEvent && value == VALUE_UP } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt index f52c165dbc..55fe8c0440 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt @@ -30,5 +30,5 @@ data class KMGamePadEvent( } } - override val deviceId: Int = device.id + val deviceId: Int = device.id } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt index 7d65550b06..1e12eaed08 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt @@ -1,5 +1,3 @@ package io.github.sds100.keymapper.system.inputevents -sealed interface KMInputEvent { - val deviceId: Int? -} \ No newline at end of file +sealed interface KMInputEvent \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt index 246d52c348..2a77588af2 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -36,5 +36,5 @@ data class KMKeyEvent( } } - override val deviceId: Int = device.id + val deviceId: Int = device.id } From 042dee6dcb69ac549591e9caf4c0ce8586042836 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 01:55:14 +0100 Subject: [PATCH 084/215] #1394 ungrab devices when key maps are paused --- .../detection/KeyMapDetectionController.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt index 6d895293e9..de3465abe6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt @@ -17,6 +17,8 @@ import io.github.sds100.keymapper.system.inputevents.KMInputEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber @@ -45,20 +47,18 @@ class KeyMapDetectionController( inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this, listOf(KMEvdevEvent.TYPE_KEY_EVENT)) coroutineScope.launch { - detectUseCase.allKeyMapList.collect { keyMapList -> + combine(detectUseCase.allKeyMapList, isPaused) { keyMapList, isPaused -> algorithm.reset() - algorithm.loadKeyMaps(keyMapList) - // Only grab the triggers that are actually being listened to by the algorithm - grabEvdevDevicesForTriggers(algorithm.triggers) - } - } - coroutineScope.launch { - isPaused.collect { isPaused -> if (isPaused) { - algorithm.reset() + algorithm.loadKeyMaps(emptyList()) + inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList()) + } else { + algorithm.loadKeyMaps(keyMapList) + // Only grab the triggers that are actually being listened to by the algorithm + grabEvdevDevicesForTriggers(algorithm.triggers) } - } + }.launchIn(coroutineScope) } } From 777d54fc41b60b01153cb239a0b3bdf5a978eb18 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 01:55:33 +0100 Subject: [PATCH 085/215] #1394 imitate evdev events through evdev --- .../keymapper/base/input/InputEventHub.kt | 55 ++++++--- .../keymaps/detection/DetectKeyMapsUseCase.kt | 20 ++- .../base/keymaps/detection/KeyMapAlgorithm.kt | 115 ++++++++++++------ .../base/keymaps/KeyMapAlgorithmTest.kt | 52 ++++---- .../keymapper/sysbridge/BnEvdevCallback.h | 9 +- .../keymapper/sysbridge/BpEvdevCallback.h | 6 +- .../keymapper/sysbridge/IEvdevCallback.cpp | 32 ++--- .../keymapper/sysbridge/IEvdevCallback.h | 12 +- 8 files changed, 166 insertions(+), 135 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index d82f10477b..9ca7c35f97 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -16,6 +16,7 @@ import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent @@ -190,6 +191,10 @@ class InputEventHubImpl @Inject constructor( } } + if (logInputEventsEnabled.value) { + Timber.d("Consumed: $consume") + } + return consume } @@ -318,24 +323,35 @@ class InputEventHubImpl @Inject constructor( } } -// override fun injectEvdevEvent(event: KMEvdevEvent): KMResult { -// val systemBridge = this.systemBridgeFlow -// -// if (systemBridge == null) { -// return SystemBridgeError.Disconnected -// } -// -// try { -// return systemBridge.writeEvdevEvent( -// event.deviceId, -// event.type, -// event.code, -// event.value -// ).success() -// } catch (e: RemoteException) { -// return KMError.Exception(e) -// } -// } + override fun injectEvdevEvent( + devicePath: String, + type: Int, + code: Int, + value: Int + ): KMResult { + val systemBridge = this.systemBridgeFlow.value + + if (systemBridge == null) { + Timber.w("System bridge is not connected, cannot inject evdev event.") + return SystemBridgeError.Disconnected + } + + try { + val result = systemBridge.writeEvdevEvent( + devicePath, + type, + code, + value + ) + + Timber.d("Injected evdev event: $result") + + return Success(result) + } catch (e: RemoteException) { + Timber.e(e, "Failed to inject evdev event") + return KMError.Exception(e) + } + } override suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult { val systemBridge = this.systemBridgeFlow.value @@ -421,8 +437,7 @@ interface InputEventHub { */ fun injectKeyEventAsync(event: InjectKeyEventModel): KMResult - // TODO make InjectEvdevEventModel -// fun injectEvdevEvent(event: KMEvdevEvent): KMResult + fun injectEvdevEvent(devicePath: String, type: Int, code: Int, value: Int): KMResult /** * Send an input event to the connected clients. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt index 0bb074039e..8dad7f4449 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt @@ -184,7 +184,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( vibrator.vibrate(duration) } - override fun imitateButtonPress( + override fun imitateKeyEvent( keyCode: Int, metaState: Int, deviceId: Int, @@ -224,6 +224,15 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( } } } + + override fun imitateEvdevEvent(devicePath: String, type: Int, code: Int, value: Int) { + if (inputEventHub.isSystemBridgeConnected()) { + Timber.d("Imitate evdev event, device path: $devicePath, type: $type, code: $code, value: $value") + inputEventHub.injectEvdevEvent(devicePath, type, code, value) + } else { + Timber.w("Cannot imitate evdev event without system bridge connected. Device path: $devicePath, type: $type, code: $code, value: $value") + } + } } interface DetectKeyMapsUseCase { @@ -244,7 +253,7 @@ interface DetectKeyMapsUseCase { val currentTime: Long - fun imitateButtonPress( + fun imitateKeyEvent( keyCode: Int, metaState: Int = 0, deviceId: Int = 0, @@ -252,4 +261,11 @@ interface DetectKeyMapsUseCase { scanCode: Int = 0, source: Int = InputDevice.SOURCE_UNKNOWN, ) + + fun imitateEvdevEvent( + devicePath: String, + type: Int, + code: Int, + value: Int, + ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index d9d9f8acbd..a6dd91a9b6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -635,6 +635,7 @@ class KeyMapAlgorithm( val event = EvdevEventAlgo( keyCode = inputEvent.androidCode, clickType = null, + devicePath = inputEvent.device.path, device = EvdevDeviceInfo( name = inputEvent.device.name, bus = inputEvent.device.bus, @@ -947,7 +948,7 @@ class KeyMapAlgorithm( consumeEvent = true keyCodesToImitateUpAction.add(event.keyCode) - useCase.imitateButtonPress( + useCase.imitateKeyEvent( keyCode = event.keyCode, metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), deviceId = event.deviceId, @@ -1280,10 +1281,7 @@ class KeyMapAlgorithm( // short press if (keyAwaitingRelease && - trigger.matchingEventAtIndex( - event.withShortPress, - keyIndex, - ) + trigger.matchingEventAtIndex(event.withShortPress, keyIndex) ) { if (isSingleKeyTrigger) { shortPressSingleKeyTriggerJustReleased = true @@ -1295,8 +1293,7 @@ class KeyMapAlgorithm( if (modifierKeyEventActions) { val actionKeys = triggerActions[triggerIndex] - actionKeys.forEach { actionKey -> - + for (actionKey in actionKeys) { actionMap[actionKey]?.let { action -> if (action.data is ActionData.InputKeyEvent && isModifierKey(action.data.keyCode)) { val actionMetaState = @@ -1441,19 +1438,33 @@ class KeyMapAlgorithm( } if (event is KeyEventAlgo) { - useCase.imitateButtonPress( + useCase.imitateKeyEvent( event.keyCode, action = KeyEvent.ACTION_DOWN, scanCode = event.scanCode, source = event.source, ) - useCase.imitateButtonPress( + useCase.imitateKeyEvent( event.keyCode, action = KeyEvent.ACTION_UP, scanCode = event.scanCode, source = event.source, ) + } else if (event is EvdevEventAlgo) { + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + KMEvdevEvent.TYPE_KEY_EVENT, + event.scanCode, + KMEvdevEvent.VALUE_DOWN + ) + + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + KMEvdevEvent.TYPE_KEY_EVENT, + event.scanCode, + KMEvdevEvent.VALUE_UP + ) } } } @@ -1462,38 +1473,61 @@ class KeyMapAlgorithm( detectedSequenceTriggerIndexes.isEmpty() && detectedParallelTriggerIndexes.isEmpty() && !shortPressSingleKeyTriggerJustReleased && - !mappedToDoublePress && - event is KeyEventAlgo + !mappedToDoublePress ) { - if (imitateUpKeyEvent) { - useCase.imitateButtonPress( - keyCode = event.keyCode, - metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), - deviceId = event.deviceId, - action = KeyEvent.ACTION_UP, - scanCode = event.scanCode, - source = event.source, - ) - } else { - useCase.imitateButtonPress( - keyCode = event.keyCode, - metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), - deviceId = event.deviceId, - action = KeyEvent.ACTION_DOWN, - scanCode = event.scanCode, - source = event.source, - ) - useCase.imitateButtonPress( - keyCode = event.keyCode, - metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), - deviceId = event.deviceId, - action = KeyEvent.ACTION_UP, - scanCode = event.scanCode, - source = event.source, - ) + if (event is KeyEventAlgo) { + if (imitateUpKeyEvent) { + useCase.imitateKeyEvent( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_UP, + scanCode = event.scanCode, + source = event.source, + ) + } else { + useCase.imitateKeyEvent( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_DOWN, + scanCode = event.scanCode, + source = event.source, + ) + useCase.imitateKeyEvent( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_UP, + scanCode = event.scanCode, + source = event.source, + ) + } + keyCodesToImitateUpAction.remove(event.keyCode) + } else if (event is EvdevEventAlgo) { + if (imitateUpKeyEvent) { + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + type = KMEvdevEvent.TYPE_KEY_EVENT, + code = event.scanCode, + value = KMEvdevEvent.VALUE_UP + ) + } else { + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + type = KMEvdevEvent.TYPE_KEY_EVENT, + code = event.scanCode, + value = KMEvdevEvent.VALUE_DOWN + ) + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + type = KMEvdevEvent.TYPE_KEY_EVENT, + code = event.scanCode, + value = KMEvdevEvent.VALUE_UP + ) + } + keyCodesToImitateUpAction.remove(event.keyCode) } - - keyCodesToImitateUpAction.remove(event.keyCode) } return consumeEvent @@ -1627,7 +1661,7 @@ class KeyMapAlgorithm( delay(400) while (keyCodesToImitateUpAction.contains(keyCode)) { - useCase.imitateButtonPress( + useCase.imitateKeyEvent( keyCode = keyCode, metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), deviceId = deviceId, @@ -1830,6 +1864,7 @@ class KeyMapAlgorithm( } private data class EvdevEventAlgo( + val devicePath: String, val device: EvdevDeviceInfo, val scanCode: Int, val keyCode: Int, diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 0438ba808d..0885ca3e88 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -1313,7 +1313,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -1328,7 +1328,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -1342,7 +1342,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) verify(performActionsUseCase, never()).perform(TEST_ACTION.data) - verify(detectKeyMapsUseCase, times(2)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(2)).imitateKeyEvent( any(), any(), any(), @@ -2040,11 +2040,11 @@ class KeyMapAlgorithmTest { inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP) // THEN - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( keyCode = 1, action = KeyEvent.ACTION_DOWN ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( keyCode = 1, action = KeyEvent.ACTION_UP ) @@ -2056,19 +2056,19 @@ class KeyMapAlgorithmTest { assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(false)) // THEN - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 1, action = KeyEvent.ACTION_DOWN ) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 1, action = KeyEvent.ACTION_UP ) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 2, action = KeyEvent.ACTION_DOWN ) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 2, action = KeyEvent.ACTION_UP ) @@ -2122,19 +2122,19 @@ class KeyMapAlgorithmTest { inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP) // THEN - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 1, action = KeyEvent.ACTION_DOWN ) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 1, action = KeyEvent.ACTION_UP ) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 2, action = KeyEvent.ACTION_DOWN ) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 2, action = KeyEvent.ACTION_UP ) @@ -2603,7 +2603,7 @@ class KeyMapAlgorithmTest { metaState, ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, @@ -2617,7 +2617,7 @@ class KeyMapAlgorithmTest { 0, ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( KeyEvent.KEYCODE_E, 0, FAKE_KEYBOARD_DEVICE_ID, @@ -2672,7 +2672,7 @@ class KeyMapAlgorithmTest { metaState, ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, @@ -2680,7 +2680,7 @@ class KeyMapAlgorithmTest { scanCode = 33, ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, @@ -2874,7 +2874,7 @@ class KeyMapAlgorithmTest { inputKeyEvent(KeyEvent.KEYCODE_C, KeyEvent.ACTION_UP) inOrder(detectKeyMapsUseCase) { - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( any(), metaState = eq(KeyEvent.META_ALT_LEFT_ON + KeyEvent.META_ALT_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON), any(), @@ -2883,7 +2883,7 @@ class KeyMapAlgorithmTest { any(), ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( any(), metaState = eq(0), any(), @@ -3262,12 +3262,12 @@ class KeyMapAlgorithmTest { // then verify(detectKeyMapsUseCase, times(1)) - .imitateButtonPress( + .imitateKeyEvent( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, action = KeyEvent.ACTION_DOWN ) verify(detectKeyMapsUseCase, times(1)) - .imitateButtonPress( + .imitateKeyEvent( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, action = KeyEvent.ACTION_UP ) @@ -3294,7 +3294,7 @@ class KeyMapAlgorithmTest { // then verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -3327,7 +3327,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) // wait for the double press to try and imitate the key. - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -3356,7 +3356,7 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) // then - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -3384,13 +3384,13 @@ class KeyMapAlgorithmTest { ) verify(detectKeyMapsUseCase, times(1)) - .imitateButtonPress( + .imitateKeyEvent( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, action = KeyEvent.ACTION_DOWN ) verify(detectKeyMapsUseCase, times(1)) - .imitateButtonPress(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, action = KeyEvent.ACTION_UP) + .imitateKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, action = KeyEvent.ACTION_UP) } @Test diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index 04bbee8896..2ceb22294c 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -37,13 +37,8 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { ::ndk::ScopedAStatus onEvdevEventLoopStarted() override { return _impl->onEvdevEventLoopStarted(); } - - ::ndk::ScopedAStatus - onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, - int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, - bool *_aidl_return) override { - return _impl->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, - in_value, in_androidCode, _aidl_return); + ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override { + return _impl->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); } protected: private: diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index 563f3b4c20..6bab1bbf8b 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -20,11 +20,7 @@ class BpEvdevCallback : public ::ndk::BpCInterface { virtual ~BpEvdevCallback(); ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; - - ::ndk::ScopedAStatus - onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, - int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, - bool *_aidl_return) override; + ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; }; } // namespace sysbridge } // namespace keymapper diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index d7fdfb6598..45b91d99ea 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -31,7 +31,7 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback break; } case (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/): { - std::string in_devicePath; + std::string in_devicePath; int64_t in_timeSec; int64_t in_timeUsec; int32_t in_type; @@ -40,7 +40,7 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback int32_t in_androidCode; bool _aidl_return; - _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_devicePath); + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_devicePath); if (_aidl_ret_status != STATUS_OK) break; _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeSec); @@ -61,10 +61,7 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_androidCode); if (_aidl_ret_status != STATUS_OK) break; - ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_devicePath, in_timeSec, - in_timeUsec, in_type, in_code, - in_value, in_androidCode, - &_aidl_return); + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, &_aidl_return); _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); if (_aidl_ret_status != STATUS_OK) break; @@ -118,11 +115,7 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { _aidl_status_return: return _aidl_status; } - - ::ndk::ScopedAStatus - BpEvdevCallback::onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, - int64_t in_timeUsec, int32_t in_type, int32_t in_code, - int32_t in_value, int32_t in_androidCode, bool *_aidl_return) { +::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) { binder_status_t _aidl_ret_status = STATUS_OK; ::ndk::ScopedAStatus _aidl_status; ::ndk::ScopedAParcel _aidl_in; @@ -131,7 +124,7 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_devicePath); + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_devicePath); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeSec); @@ -163,10 +156,7 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { #endif // BINDER_STABILITY_SUPPORT ); if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { - _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_devicePath, in_timeSec, - in_timeUsec, in_type, in_code, - in_value, in_androidCode, - _aidl_return); + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); goto _aidl_status_return; } if (_aidl_ret_status != STATUS_OK) goto _aidl_error; @@ -247,15 +237,7 @@ ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEventLoopStarted() { _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; } - - ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(const std::string & /*in_devicePath*/, - int64_t /*in_timeSec*/, - int64_t /*in_timeUsec*/, - int32_t /*in_type*/, - int32_t /*in_code*/, - int32_t /*in_value*/, - int32_t /*in_androidCode*/, - bool * /*_aidl_return*/) { +::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(const std::string& /*in_devicePath*/, int64_t /*in_timeSec*/, int64_t /*in_timeUsec*/, int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/, int32_t /*in_androidCode*/, bool* /*_aidl_return*/) { ::ndk::ScopedAStatus _aidl_status; _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index 4f0c1471dc..007e2afbd2 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -38,22 +38,14 @@ class IEvdevCallback : public ::ndk::ICInterface { static bool setDefaultImpl(const std::shared_ptr& impl); static const std::shared_ptr& getDefaultImpl(); virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; - - virtual ::ndk::ScopedAStatus - onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, - int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, - bool *_aidl_return) = 0; + virtual ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) = 0; private: static std::shared_ptr default_impl; }; class IEvdevCallbackDefault : public IEvdevCallback { public: ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; - - ::ndk::ScopedAStatus - onEvdevEvent(const std::string &in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, - int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, - bool *_aidl_return) override; + ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; }; From b5e65a3b14a459016668da8978b5f1b5c7516424 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 02:10:06 +0100 Subject: [PATCH 086/215] #1394 ignore BTN_TOUCH key events from the touchscreen when recording --- .../keymapper/base/trigger/RecordTriggerController.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 767ffddb8b..a59fca7fc1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -10,7 +10,6 @@ import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.isError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent @@ -36,7 +35,6 @@ class RecordTriggerControllerImpl @Inject constructor( private val coroutineScope: CoroutineScope, private val inputEventHub: InputEventHub, private val accessibilityServiceAdapter: AccessibilityServiceAdapter, - private val devicesAdapter: DevicesAdapter ) : RecordTriggerController, InputEventHubCallback { companion object { /** @@ -44,6 +42,10 @@ class RecordTriggerControllerImpl @Inject constructor( */ private const val RECORD_TRIGGER_TIMER_LENGTH = 5 private const val INPUT_EVENT_HUB_ID = "record_trigger" + + private val SCAN_CODES_BLACKLIST = setOf( + 330 // BTN_TOUCH + ) } override val state = MutableStateFlow(RecordTriggerState.Idle) @@ -79,6 +81,10 @@ class RecordTriggerControllerImpl @Inject constructor( return false } + if (SCAN_CODES_BLACKLIST.contains(event.code)) { + return false + } + Timber.d("Recorded evdev event ${event.code} ${KeyEvent.keyCodeToString(event.androidCode)}") // Must also remove old down events if a new down even is received. From 0a5f8dd4d4994db3b1faaf1a4c0621ac6276e5bf Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 02:22:18 +0100 Subject: [PATCH 087/215] #1394 grab and ungrab devices synchronously --- .../keymapper/base/input/EvdevHandleCache.kt | 4 +- sysbridge/src/main/cpp/libevdev_jni.cpp | 343 ++++++++++-------- .../sysbridge/service/SystemBridge.kt | 4 +- 3 files changed, 188 insertions(+), 163 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt index 3812140504..73ee785fc6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import timber.log.Timber class EvdevHandleCache( private val coroutineScope: CoroutineScope, @@ -22,7 +23,8 @@ class EvdevHandleCache( try { systemBridge.evdevInputDevices.associateBy { it.path } - } catch (_: RemoteException) { + } catch (e: RemoteException) { + Timber.e("Failed to get evdev input devices from system bridge $e") emptyMap() } }.stateIn(coroutineScope, SharingStarted.Eagerly, emptyMap()) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index a05f1e97c3..62db424403 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -21,23 +21,12 @@ using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; -struct GrabData { - char devicePath[256]; -}; - -struct UngrabData { - char devicePath[256]; -}; - enum CommandType { - GRAB, - UNGRAB, STOP }; struct Command { CommandType type; - std::variant data; }; struct DeviceContext { @@ -148,112 +137,46 @@ JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNative(JNIEnv *env, jobject thiz, jstring jDevicePath) { - // TODO does this really need epoll now with the looper and handler? Can't it just be done here? Then one can return actually legit error codes - const char *devicePath = env->GetStringUTFChars(jDevicePath, nullptr); if (devicePath == nullptr) { return false; } - Command cmd; - cmd.type = GRAB; - cmd.data = GrabData{}; - strcpy(std::get(cmd.data).devicePath, devicePath); - - env->ReleaseStringUTFChars(jDevicePath, devicePath); - - std::lock_guard lock(commandMutex); - commandQueue.push(cmd); - - uint64_t val = 1; - ssize_t written = write(commandEventFd, &val, sizeof(val)); - - if (written < 0) { - LOGE("Failed to write to commandEventFd: %s", strerror(errno)); - return false; - } - - return true; -} - - -void onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { - struct input_event inputEvent{}; - - int rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); - - do { - if (rc == LIBEVDEV_READ_STATUS_SUCCESS) { // rc == 0 - int32_t outKeycode = -1; - uint32_t outFlags = -1; - deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); + bool result = false; - bool returnValue; - callback->onEvdevEvent(deviceContext->devicePath, - inputEvent.time.tv_sec, - inputEvent.time.tv_usec, - inputEvent.type, - inputEvent.code, - inputEvent.value, - outKeycode, - &returnValue); + { + // Lock to prevent concurrent grab/ungrab operations on the same device + std::lock_guard lock(evdevDevicesMutex); - if (!returnValue) { - libevdev_uinput_write_event(deviceContext->uinputDev, - inputEvent.type, - inputEvent.code, - inputEvent.value); + // Check if device is already grabbed + for (const auto &pair: *evdevDevices) { + DeviceContext context = pair.second; + if (strcmp(context.devicePath, devicePath) == 0) { + LOGW("Device %s is already grabbed. Maybe it is a virtual uinput device.", + devicePath); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } - - rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); - - } else if (rc == LIBEVDEV_READ_STATUS_SYNC) { - rc = libevdev_next_event(deviceContext->evdev, - LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC, - &inputEvent); } - } while (rc != -EAGAIN); -} - -// Set this to some upper limit. It is unlikely that Key Mapper will be polling -// more than a few evdev devices at once. -static int MAX_EPOLL_EVENTS = 100; - -void handleCommand(const Command &cmd) { - if (cmd.type == GRAB) { - const GrabData &data = std::get(cmd.data); + // Perform synchronous grab operation struct libevdev *dev = nullptr; - char devicePath[256]; - - strcpy(devicePath, data.devicePath); // MUST be NONBLOCK so that the loop reading the evdev events eventually returns // due to an EAGAIN error. int fd = open(devicePath, O_RDONLY | O_NONBLOCK); if (fd == -1) { LOGE("Failed to open device %s: %s", devicePath, strerror(errno)); - return; + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } int rc = libevdev_new_from_fd(fd, &dev); if (rc != 0) { LOGE("Failed to create libevdev device from %s: %s", devicePath, strerror(errno)); close(fd); - return; - } - - { - std::lock_guard lock(evdevDevicesMutex); - for (const auto &pair: *evdevDevices) { - DeviceContext context = pair.second; - if (strcmp(context.devicePath, devicePath) == 0) { - LOGW("Device %s is already grabbed. Maybe it is a virtual uinput device.", - devicePath); - libevdev_free(dev); - return; - } - } + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } rc = libevdev_grab(dev, LIBEVDEV_GRAB); @@ -261,7 +184,9 @@ void handleCommand(const Command &cmd) { LOGE("Failed to grab evdev device %s: %s", libevdev_get_name(dev), strerror(-rc)); libevdev_free(dev); - return; + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } int evdevFd = libevdev_get_fd(dev); @@ -280,15 +205,22 @@ void handleCommand(const Command &cmd) { if (!klResult.ok()) { LOGE("key layout map not found for device %s", libevdev_get_name(dev)); + libevdev_grab(dev, LIBEVDEV_UNGRAB); libevdev_free(dev); - return; + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } struct libevdev_uinput *uinputDev = nullptr; int uinputFd = open("/dev/uinput", O_RDWR); if (uinputFd < 0) { LOGE("Failed to open /dev/uinput to clone the device."); - return; + libevdev_grab(dev, LIBEVDEV_UNGRAB); + libevdev_free(dev); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } rc = libevdev_uinput_create_from_device(dev, uinputFd, &uinputDev); @@ -297,58 +229,94 @@ void handleCommand(const Command &cmd) { LOGE("Failed to create uinput device from evdev device %s: %s", libevdev_get_name(dev), strerror(-rc)); close(uinputFd); + libevdev_grab(dev, LIBEVDEV_UNGRAB); libevdev_free(dev); - return; + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } DeviceContext context = { dev, uinputDev, *klResult.value(), - *data.devicePath, + {} // Initialize devicePath array }; strcpy(context.devicePath, devicePath); - struct epoll_event event{}; - event.events = EPOLLIN; - event.data.fd = evdevFd; - - if (epoll_ctl(epollFd, EPOLL_CTL_ADD, evdevFd, &event) == -1) { - LOGE("Failed to add new device to epoll: %s", strerror(errno)); - libevdev_free(dev); - return; + // Add to epoll for event monitoring (only if event loop is running) + if (epollFd != -1) { + struct epoll_event event{}; + event.events = EPOLLIN; + event.data.fd = evdevFd; + + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, evdevFd, &event) == -1) { + LOGE("Failed to add new device to epoll: %s", strerror(errno)); + libevdev_uinput_destroy(uinputDev); + libevdev_grab(dev, LIBEVDEV_UNGRAB); + libevdev_free(dev); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } } - std::lock_guard lock(evdevDevicesMutex); evdevDevices->insert_or_assign(evdevFd, context); - - // TODO add device input file to epoll so the state is cleared when it is disconnected. Remember to remove the epoll after it has disconnected. + result = true; LOGI("Grabbed device %s, %s", libevdev_get_name(dev), context.devicePath); + } - } else if (cmd.type == UNGRAB) { + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return result; +} - const UngrabData &data = std::get(cmd.data); - std::lock_guard lock(evdevDevicesMutex); - for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { - if (strcmp(it->second.devicePath, data.devicePath) == 0) { +void onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { + struct input_event inputEvent{}; - // Do this before freeing the evdev file descriptor - libevdev_uinput_destroy(it->second.uinputDev); + int rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); - int fd = it->first; - epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr); - libevdev_grab(it->second.evdev, LIBEVDEV_UNGRAB); - libevdev_free(it->second.evdev); - evdevDevices->erase(it); + do { + if (rc == LIBEVDEV_READ_STATUS_SUCCESS) { // rc == 0 + int32_t outKeycode = -1; + uint32_t outFlags = -1; + deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); - LOGI("Ungrabbed device %s", data.devicePath); - break; + bool returnValue; + callback->onEvdevEvent(deviceContext->devicePath, + inputEvent.time.tv_sec, + inputEvent.time.tv_usec, + inputEvent.type, + inputEvent.code, + inputEvent.value, + outKeycode, + &returnValue); + + if (!returnValue) { + libevdev_uinput_write_event(deviceContext->uinputDev, + inputEvent.type, + inputEvent.code, + inputEvent.value); } + + rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); + + } else if (rc == LIBEVDEV_READ_STATUS_SYNC) { + rc = libevdev_next_event(deviceContext->evdev, + LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC, + &inputEvent); } - } + } while (rc != -EAGAIN); +} + +// Set this to some upper limit. It is unlikely that Key Mapper will be polling +// more than a few evdev devices at once. +static int MAX_EPOLL_EVENTS = 100; + +void handleCommand(const Command &cmd) { + // Only STOP commands are handled here now, grab/ungrab are synchronous } extern "C" @@ -448,33 +416,57 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo } extern "C" -JNIEXPORT void JNICALL +JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, jobject thiz, jstring jDevicePath) { const char *devicePath = env->GetStringUTFChars(jDevicePath, nullptr); if (devicePath == nullptr) { - return; + return false; } - Command cmd; - cmd.type = UNGRAB; - cmd.data = UngrabData{}; - strcpy(std::get(cmd.data).devicePath, devicePath); - - env->ReleaseStringUTFChars(jDevicePath, devicePath); + bool result = false; { - std::lock_guard lock(commandMutex); - commandQueue.push(cmd); - } + // Lock to prevent concurrent grab/ungrab operations + std::lock_guard lock(evdevDevicesMutex); - // Notify the event loop - uint64_t val = 1; - ssize_t written = write(commandEventFd, &val, sizeof(val)); - if (written < 0) { - LOGE("Failed to write to commandEventFd: %s", strerror(errno)); + for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { + if (strcmp(it->second.devicePath, devicePath) == 0) { + // Remove from epoll first (if event loop is running) + if (epollFd != -1) { + int fd = it->first; + if (epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr) == -1) { + LOGW("Failed to remove device from epoll: %s", strerror(errno)); + // Continue with ungrab even if epoll removal fails + } + } + + // Do this before freeing the evdev file descriptor + libevdev_uinput_destroy(it->second.uinputDev); + + // Ungrab the device + libevdev_grab(it->second.evdev, LIBEVDEV_UNGRAB); + + // Free resources + libevdev_free(it->second.evdev); + + // Remove from device map + evdevDevices->erase(it); + result = true; + + LOGI("Ungrabbed device %s", devicePath); + break; + } + } + + if (!result) { + LOGW("Device %s was not found in grabbed devices list", devicePath); + } } + + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return result; } @@ -527,35 +519,48 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNa return result; } extern "C" -JNIEXPORT void JNICALL +JNIEXPORT jboolean JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDevicesNative( JNIEnv *env, jobject thiz) { - std::vector devicePaths; { - std::lock_guard evdevLock(evdevDevicesMutex); + // Lock to prevent concurrent grab/ungrab operations + std::lock_guard lock(evdevDevicesMutex); + + // Create a copy of the iterator to avoid issues with erasing during iteration + auto devicesCopy = *evdevDevices; + + for (const auto &pair: devicesCopy) { + int fd = pair.first; + const DeviceContext &context = pair.second; - for (auto pair: *evdevDevices) { - devicePaths.push_back(std::string(pair.second.devicePath)); + // Remove from epoll first (if event loop is running) + if (epollFd != -1) { + if (epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr) == -1) { + LOGW("Failed to remove device %s from epoll: %s", context.devicePath, + strerror(errno)); + // Continue with ungrab even if epoll removal fails + } + } + + // Do this before freeing the evdev file descriptor + libevdev_uinput_destroy(context.uinputDev); + + // Ungrab the device + libevdev_grab(context.evdev, LIBEVDEV_UNGRAB); + + // Free resources + libevdev_free(context.evdev); + + LOGI("Ungrabbed device %s", context.devicePath); } - } - std::lock_guard commandLock(commandMutex); - for (const std::string &path: devicePaths) { - Command cmd; - cmd.type = UNGRAB; - cmd.data = UngrabData{}; - strcpy(std::get(cmd.data).devicePath, path.c_str()); - commandQueue.push(cmd); + // Clear all devices from the map + evdevDevices->clear(); } - // Notify the event loop - uint64_t val = 1; - ssize_t written = write(commandEventFd, &val, sizeof(val)); - if (written < 0) { - LOGE("Failed to write to commandEventFd: %s", strerror(errno)); - } + return true; } extern "C" JNIEXPORT jboolean JNICALL @@ -623,6 +628,24 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_getEvdevDevicesNa char fullPath[256]; snprintf(fullPath, sizeof(fullPath), "/dev/input/%s", entry->d_name); + bool ignoreDevice = false; + + // Ignore this device if it is a uinput device we created + for (const auto &pair: *evdevDevices) { + DeviceContext context = pair.second; + const char *uinputDevicePath = libevdev_uinput_get_devnode(context.uinputDev); + + if (strcmp(fullPath, uinputDevicePath) == 0) { + LOGW("Ignoring uinput device %s.", uinputDevicePath); + ignoreDevice = true; + break; + } + } + + if (ignoreDevice) { + continue; + } + int fd = open(fullPath, O_RDONLY); if (fd == -1) { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 3ab5bd8967..7658529e6e 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -42,8 +42,8 @@ internal class SystemBridge : ISystemBridge.Stub() { external fun grabEvdevDeviceNative(devicePath: String): Boolean external fun grabAllEvdevDevicesNative(): Boolean - external fun ungrabEvdevDeviceNative(devicePath: String) - external fun ungrabAllEvdevDevicesNative() + external fun ungrabEvdevDeviceNative(devicePath: String): Boolean + external fun ungrabAllEvdevDevicesNative(): Boolean external fun writeEvdevEventNative( devicePath: String, type: Int, From 5df3cd9983c2531a98d0c59d8da422b23ddf8ff7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 02:54:39 +0100 Subject: [PATCH 088/215] #1394 fix tests --- .../keymapper/base/input/InputEventHub.kt | 13 +- .../keymapper/base/ConfigKeyMapUseCaseTest.kt | 161 +++++++++--------- .../base/actions/PerformActionsUseCaseTest.kt | 37 ++-- .../base/keymaps/KeyMapAlgorithmTest.kt | 142 +++++++++------ 4 files changed, 193 insertions(+), 160 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 9ca7c35f97..4423e3f7c9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -9,7 +9,6 @@ import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success -import io.github.sds100.keymapper.common.utils.success import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.IEvdevCallback @@ -353,12 +352,12 @@ class InputEventHubImpl @Inject constructor( } } - override suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult { + override suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult { val systemBridge = this.systemBridgeFlow.value if (systemBridge == null) { imeInputEventInjector.inputKeyEvent(event) - return Success(true) + return Success(Unit) } else { try { val androidKeyEvent = event.toAndroidKeyEvent(flags = KeyEvent.FLAG_FROM_SYSTEM) @@ -367,14 +366,16 @@ class InputEventHubImpl @Inject constructor( Timber.d("Injecting key event $androidKeyEvent with system bridge") } - return withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { // All injected events have their device id set to -1 (VIRTUAL_KEYBOARD_ID) // in InputDispatcher.cpp injectInputEvent. systemBridge.injectInputEvent( androidKeyEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH - ).success() + ) } + + return Success(Unit) } catch (e: RemoteException) { return KMError.Exception(e) } @@ -428,7 +429,7 @@ interface InputEventHub { * * Must be suspend so injecting to the systembridge can happen on another thread. */ - suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult + suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult /** * Some callers don't care about the result from injecting and it isn't critical diff --git a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt index da3447c85e..261a7919f5 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt @@ -20,6 +20,7 @@ import io.github.sds100.keymapper.base.utils.parallelTrigger import io.github.sds100.keymapper.base.utils.sequenceTrigger import io.github.sds100.keymapper.base.utils.singleKeyTrigger import io.github.sds100.keymapper.base.utils.triggerKey +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.system.inputevents.InputEventUtils @@ -32,7 +33,6 @@ import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.instanceOf import org.hamcrest.Matchers.`is` -import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock @@ -58,39 +58,6 @@ class ConfigKeyMapUseCaseTest { ) } - @Test - fun `Any device can not be selected for evdev trigger key`() = - runTest(testDispatcher) { - val triggerKey = EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - deviceDescriptor = "keyboard0", - deviceName = "Keyboard", - clickType = ClickType.SHORT_PRESS, - consumeEvent = true - ) - - useCase.keyMap.value = State.Data( - KeyMap( - trigger = sequenceTrigger( - triggerKey, - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - deviceDescriptor = "keyboard0", - deviceName = "Keyboard", - clickType = ClickType.SHORT_PRESS, - consumeEvent = true - ) - ) - ) - ) - - assertThrows(IllegalArgumentException::class.java) { - useCase.setTriggerKeyDevice(triggerKey.uid, KeyEventTriggerDevice.Any) - } - } - @Test fun `Adding a non evdev key deletes all evdev keys in the trigger`() = runTest(testDispatcher) { @@ -105,8 +72,12 @@ class ConfigKeyMapUseCaseTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = 123, - deviceDescriptor = "keyboard0", - deviceName = "Keyboard 0", + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ), AssistantTriggerKey( type = AssistantTriggerType.ANY, @@ -115,8 +86,12 @@ class ConfigKeyMapUseCaseTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = 100, - deviceDescriptor = "gpio", - deviceName = "GPIO", + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ), ) ) @@ -166,11 +141,16 @@ class ConfigKeyMapUseCaseTest { ) ) + val evdevDevice = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) useCase.addEvdevTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - "keyboard0", - "Keyboard" + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = evdevDevice ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger @@ -179,7 +159,7 @@ class ConfigKeyMapUseCaseTest { assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) assertThat(trigger.keys[2], instanceOf(EvdevTriggerKey::class.java)) assertThat( - (trigger.keys[2] as EvdevTriggerKey).deviceDescriptor, `is`("keyboard0") + (trigger.keys[2] as EvdevTriggerKey).device, `is`(evdevDevice) ) } @@ -192,18 +172,21 @@ class ConfigKeyMapUseCaseTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = 0, - deviceDescriptor = "keyboard0", - deviceName = "Keyboard", - clickType = ClickType.SHORT_PRESS, - consumeEvent = true + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - deviceDescriptor = "keyboard0", - deviceName = "Keyboard", - clickType = ClickType.SHORT_PRESS, - consumeEvent = true + scanCode = 0, device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ) ) ) @@ -211,15 +194,6 @@ class ConfigKeyMapUseCaseTest { useCase.setParallelTriggerMode() - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - deviceDescriptor = "keyboard0", - deviceName = "Keyboard", - clickType = ClickType.SHORT_PRESS, - consumeEvent = true - ) - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger assertThat(trigger.keys, hasSize(1)) assertThat(trigger.keys[0], instanceOf(EvdevTriggerKey::class.java)) @@ -227,7 +201,6 @@ class ConfigKeyMapUseCaseTest { (trigger.keys[0] as EvdevTriggerKey).keyCode, `is`(KeyEvent.KEYCODE_VOLUME_DOWN) ) - assertThat((trigger.keys[0] as EvdevTriggerKey).deviceDescriptor, `is`("keyboard0")) } @Test @@ -238,15 +211,23 @@ class ConfigKeyMapUseCaseTest { useCase.addEvdevTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, 0, - "keyboard0", - "Keyboard" + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ) useCase.addEvdevTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, 0, - "keyboard0", - "Keyboard" + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger @@ -262,18 +243,22 @@ class ConfigKeyMapUseCaseTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = 0, - deviceDescriptor = "keyboard0", - deviceName = "Keyboard", - clickType = ClickType.SHORT_PRESS, - consumeEvent = true + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = 0, - deviceDescriptor = "keyboard0", - deviceName = "Keyboard", - clickType = ClickType.SHORT_PRESS, - consumeEvent = true + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ) ) ) @@ -281,10 +266,14 @@ class ConfigKeyMapUseCaseTest { // Add a third key and it should still be a sequence trigger now useCase.addEvdevTriggerKey( - KeyEvent.KEYCODE_VOLUME_UP, - 0, - "keyboard0", - "Keyboard" + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 0, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger @@ -299,15 +288,23 @@ class ConfigKeyMapUseCaseTest { useCase.addEvdevTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, 0, - "keyboard0", - "Keyboard 0" + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) ) useCase.addEvdevTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, 0, - "keyboard1", - "Keyboard 1" + device = EvdevDeviceInfo( + name = "Fake Controller", + bus = 1, + vendor = 2, + product = 1 + ) ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index b74c50e16e..099d4cc084 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -3,16 +3,17 @@ package io.github.sds100.keymapper.base.actions import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.base.input.InjectKeyEventModel +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.base.system.devices.FakeDevicesAdapter -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.system.popup.ToastAdapter import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before @@ -33,20 +34,21 @@ import org.mockito.kotlin.whenever class PerformActionsUseCaseTest { private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) private lateinit var useCase: PerformActionsUseCaseImpl - private lateinit var mockImeInputEventInjector: ImeInputEventInjector private lateinit var fakeDevicesAdapter: FakeDevicesAdapter private lateinit var mockAccessibilityService: IAccessibilityService private lateinit var mockToastAdapter: ToastAdapter + private lateinit var mockInputEventHub: InputEventHub @Before fun init() { - mockImeInputEventInjector = mock() fakeDevicesAdapter = FakeDevicesAdapter() mockAccessibilityService = mock() mockToastAdapter = mock() + mockInputEventHub = mock { + on { runBlocking { injectKeyEvent(any()) } }.then { Success(Unit) } + } useCase = PerformActionsUseCaseImpl( service = mockAccessibilityService, @@ -56,7 +58,7 @@ class PerformActionsUseCaseTest { shell = mock(), intentAdapter = mock(), getActionErrorUseCase = mock(), - keyMapperImeMessenger = mockImeInputEventInjector, + keyMapperImeMessenger = mock(), packageManagerAdapter = mock(), appShortcutAdapter = mock(), toastAdapter = mockToastAdapter, @@ -77,7 +79,7 @@ class PerformActionsUseCaseTest { soundsManager = mock(), notificationReceiverAdapter = mock(), ringtoneAdapter = mock(), - inputEventHub = mock() + inputEventHub = mockInputEventHub ) } @@ -132,7 +134,6 @@ class PerformActionsUseCaseTest { // THEN val expectedDownEvent = InjectKeyEventModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, action = KeyEvent.ACTION_DOWN, metaState = 0, @@ -144,8 +145,8 @@ class PerformActionsUseCaseTest { val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) } /** @@ -179,8 +180,8 @@ class PerformActionsUseCaseTest { val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) } /** @@ -236,8 +237,8 @@ class PerformActionsUseCaseTest { val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) } /** @@ -301,8 +302,8 @@ class PerformActionsUseCaseTest { val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) } @Test @@ -348,7 +349,7 @@ class PerformActionsUseCaseTest { val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedDownEvent) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedUpEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 0885ca3e88..41351578e9 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -29,11 +29,14 @@ import io.github.sds100.keymapper.base.utils.parallelTrigger import io.github.sds100.keymapper.base.utils.sequenceTrigger import io.github.sds100.keymapper.base.utils.singleKeyTrigger import io.github.sds100.keymapper.base.utils.triggerKey +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.system.camera.CameraLens +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import junitparams.JUnitParamsRunner @@ -106,6 +109,20 @@ class KeyMapAlgorithmTest { sources = InputDevice.SOURCE_GAMEPAD ) + private val FAKE_CONTROLLER_EVDEV_DEVICE = EvdevDeviceInfo( + name = "Fake Controller", + bus = 1, + vendor = 2, + product = 1 + ) + + private val FAKE_VOLUME_EVDEV_DEVICE = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2 + ) + private const val FAKE_PACKAGE_NAME = "test_package" private const val LONG_PRESS_DELAY = 500L @@ -211,24 +228,22 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, scanCode = 900, - deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, - deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + device = FAKE_CONTROLLER_EVDEV_DEVICE ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_B, scanCode = 901, - deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, - deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + device = FAKE_CONTROLLER_EVDEV_DEVICE ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) - inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_DOWN, FAKE_CONTROLLER_INPUT_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP, FAKE_CONTROLLER_INPUT_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_A, 900, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_A, 900, FAKE_CONTROLLER_EVDEV_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_B, KeyEvent.ACTION_DOWN, FAKE_CONTROLLER_INPUT_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_B, KeyEvent.ACTION_UP, FAKE_CONTROLLER_INPUT_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_B, 901, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, 901, FAKE_CONTROLLER_EVDEV_DEVICE) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } @@ -240,62 +255,40 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, scanCode = 900, - deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, - deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + device = FAKE_CONTROLLER_EVDEV_DEVICE ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_B, scanCode = 901, - deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, - deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + device = FAKE_CONTROLLER_EVDEV_DEVICE ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) - inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_DOWN, FAKE_CONTROLLER_INPUT_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_B, KeyEvent.ACTION_DOWN, FAKE_CONTROLLER_INPUT_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_A, 900, FAKE_CONTROLLER_EVDEV_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_B, 901, FAKE_CONTROLLER_EVDEV_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP, FAKE_CONTROLLER_INPUT_DEVICE) - inputKeyEvent(KeyEvent.KEYCODE_B, KeyEvent.ACTION_UP, FAKE_CONTROLLER_INPUT_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_A, 900, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, 901, FAKE_CONTROLLER_EVDEV_DEVICE) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } @Test - fun `Evdev trigger is not triggered from events from other internal devices`() = + fun `Evdev trigger is not triggered from events from other devices`() = runTest(testDispatcher) { val trigger = singleKeyTrigger( EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_POWER, scanCode = 900, - deviceDescriptor = "gpio_keys", - deviceName = "GPIO", + device = FAKE_VOLUME_EVDEV_DEVICE ) ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) - mockEvdevKeyInput(trigger.keys[0], FAKE_INTERNAL_DEVICE) - - verify(performActionsUseCase, never()).perform(TEST_ACTION.data) - } - - @Test - fun `Evdev trigger is not triggered from events from other external devices`() = - runTest(testDispatcher) { - val trigger = singleKeyTrigger( - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_A, - scanCode = 900, - deviceDescriptor = FAKE_KEYBOARD_TRIGGER_KEY_DEVICE.descriptor, - deviceName = FAKE_KEYBOARD_TRIGGER_KEY_DEVICE.name, - ) - ) - - loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) - - mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_INPUT_DEVICE) + mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_EVDEV_DEVICE) verify(performActionsUseCase, never()).perform(TEST_ACTION.data) } @@ -306,14 +299,13 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, scanCode = 900, - deviceDescriptor = FAKE_CONTROLLER_INPUT_DEVICE.descriptor, - deviceName = FAKE_CONTROLLER_INPUT_DEVICE.name, + device = FAKE_CONTROLLER_EVDEV_DEVICE ) ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) - mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_INPUT_DEVICE) + mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_EVDEV_DEVICE) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } @@ -324,14 +316,13 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_POWER, scanCode = 900, - deviceDescriptor = FAKE_INTERNAL_DEVICE.descriptor, - deviceName = FAKE_INTERNAL_DEVICE.name, + device = FAKE_VOLUME_EVDEV_DEVICE ) ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) - mockEvdevKeyInput(trigger.keys[0], FAKE_INTERNAL_DEVICE) + mockEvdevKeyInput(trigger.keys[0], FAKE_VOLUME_EVDEV_DEVICE) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } @@ -4303,7 +4294,7 @@ class KeyMapAlgorithmTest { private suspend fun mockTriggerKeyInput(key: TriggerKey, delay: Long? = null) { if (key !is KeyEventTriggerKey) { - return + throw IllegalArgumentException("Key must be a KeyEventTriggerKey") } val inputDevice = triggerKeyDeviceToInputDevice(key.device) @@ -4339,11 +4330,11 @@ class KeyMapAlgorithmTest { private suspend fun mockEvdevKeyInput( key: TriggerKey, - inputDevice: InputDeviceInfo, + evdevDevice: EvdevDeviceInfo, delay: Long? = null, ) { if (key !is EvdevTriggerKey) { - return + throw IllegalArgumentException("Key must be an EvdevTriggerKey") } val pressDuration: Long = delay ?: when (key.clickType) { @@ -4351,27 +4342,27 @@ class KeyMapAlgorithmTest { else -> 50L } - inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) + inputDownEvdevEvent(key.keyCode, key.scanCode, evdevDevice) when (key.clickType) { ClickType.SHORT_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + inputUpEvdevEvent(key.keyCode, key.scanCode, evdevDevice) } ClickType.LONG_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + inputUpEvdevEvent(key.keyCode, key.scanCode, evdevDevice) } ClickType.DOUBLE_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + inputUpEvdevEvent(key.keyCode, key.scanCode, evdevDevice) delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) + inputDownEvdevEvent(key.keyCode, key.scanCode, evdevDevice) delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + inputUpEvdevEvent(key.keyCode, key.scanCode, evdevDevice) } } } @@ -4424,6 +4415,49 @@ class KeyMapAlgorithmTest { ), ) + private fun inputDownEvdevEvent( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo + ): Boolean = controller.onInputEvent( + KMEvdevEvent( + type = KMEvdevEvent.TYPE_KEY_EVENT, + device = EvdevDeviceHandle( + path = "/dev/input${device.name}", + name = device.name, + bus = device.bus, + vendor = device.vendor, + product = device.product + ), + code = scanCode, + androidCode = keyCode, + value = KMEvdevEvent.VALUE_DOWN, + timeSec = testScope.currentTime, + timeUsec = 0 + ), + ) + + private fun inputUpEvdevEvent( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo + ): Boolean = controller.onInputEvent( + KMEvdevEvent( + type = KMEvdevEvent.TYPE_KEY_EVENT, + device = EvdevDeviceHandle( + path = "/dev/input${device.name}", + name = device.name, + bus = device.bus, + vendor = device.vendor, + product = device.product + ), code = scanCode, + androidCode = keyCode, + value = KMEvdevEvent.VALUE_UP, + timeSec = testScope.currentTime, + timeUsec = 0 + ), + ) + private suspend fun mockParallelTrigger( trigger: Trigger, delay: Long? = null, From 02ca732da26bf098902756a58ab4e8700f8e83cb Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 02:56:15 +0100 Subject: [PATCH 089/215] #1394 style: reformat --- .../AccessibilityServiceController.kt | 4 +- .../base/actions/PerformActionsUseCase.kt | 4 +- .../keymapper/base/compose/ComposeColors.kt | 2 +- .../keymapper/base/input/EvdevHandleCache.kt | 6 +- .../base/input/InjectKeyEventModel.kt | 4 +- .../base/input/InputEventDetectionSource.kt | 2 +- .../keymapper/base/input/InputEventHub.kt | 30 ++--- .../base/input/InputEventHubCallback.kt | 2 +- .../base/keymaps/ConfigKeyMapUseCase.kt | 8 +- .../keymaps/detection/DetectKeyMapsUseCase.kt | 2 +- .../detection/DpadMotionEventTracker.kt | 2 +- .../base/keymaps/detection/KeyMapAlgorithm.kt | 15 +-- .../detection/KeyMapDetectionController.kt | 6 +- .../base/promode/ProModeViewModel.kt | 2 +- .../RerouteKeyEventsController.kt | 12 +- .../base/settings/SettingsViewModel.kt | 2 +- .../BaseAccessibilityServiceController.kt | 8 +- .../trigger/BaseConfigTriggerViewModel.kt | 23 ++-- .../keymapper/base/trigger/EvdevTriggerKey.kt | 3 +- .../base/trigger/InputEventTriggerKey.kt | 2 +- .../base/trigger/KeyEventTriggerDevice.kt | 2 +- .../base/trigger/KeyEventTriggerKey.kt | 8 +- .../base/trigger/RecordTriggerController.kt | 17 ++- .../keymapper/base/trigger/RecordedKey.kt | 4 +- .../keymapper/base/utils/InputEventStrings.kt | 1 - .../utils/ui/compose/icons/ProModeIcon.kt | 16 +-- .../keymapper/base/ConfigKeyMapUseCaseTest.kt | 126 +++++++++--------- .../base/actions/PerformActionsUseCaseTest.kt | 14 +- ...onfigKeyServiceEventActionViewModelTest.kt | 4 +- .../keymaps/DpadMotionEventTrackerTest.kt | 14 +- .../base/keymaps/KeyMapAlgorithmTest.kt | 83 ++++++------ .../base/trigger/TriggerKeyDeviceTest.kt | 2 +- .../keymapper/base/utils/KeyMapUtils.kt | 2 +- .../common/models/EvdevDeviceHandle.kt | 2 +- .../common/models/EvdevDeviceInfo.kt | 2 +- .../keymapper/common/utils/InputDeviceInfo.kt | 4 +- .../common/utils/InputDeviceUtils.kt | 4 +- .../sds100/keymapper/common/utils/KMResult.kt | 2 +- .../data/entities/EvdevTriggerKeyEntity.kt | 2 +- .../data/entities/KeyEventTriggerKeyEntity.kt | 2 +- 40 files changed, 224 insertions(+), 226 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 9a0fb138fc..a59b75e914 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -31,7 +31,7 @@ class AccessibilityServiceController @AssistedInject constructor( systemBridgeSetupController: SystemBridgeSetupController, keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, inputEventHub: InputEventHub, - recordTriggerController: RecordTriggerController + recordTriggerController: RecordTriggerController, ) : BaseAccessibilityServiceController( service = service, rerouteKeyEventsControllerFactory = rerouteKeyEventsControllerFactory, @@ -45,7 +45,7 @@ class AccessibilityServiceController @AssistedInject constructor( systemBridgeSetupController = systemBridgeSetupController, keyEventRelayServiceWrapper = keyEventRelayServiceWrapper, inputEventHub = inputEventHub, - recordTriggerController = recordTriggerController + recordTriggerController = recordTriggerController, ) { @AssistedFactory interface Factory { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 59d6fce1bf..bc3c518b7f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -101,7 +101,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( private val notificationReceiverAdapter: NotificationReceiverAdapter, private val ringtoneAdapter: RingtoneAdapter, private val settingsRepository: PreferenceRepository, - private val inputEventHub: InputEventHub + private val inputEventHub: InputEventHub, ) : PerformActionsUseCase { @AssistedFactory @@ -164,7 +164,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( deviceId = deviceId, source = source, repeatCount = 0, - scanCode = 0 + scanCode = 0, ) if (inputEventAction == InputEventAction.DOWN_UP) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt index a8c1ae2390..e76ded65f4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt @@ -95,4 +95,4 @@ object ComposeColors { val onMagiskTealDark = Color(0xFFFFFFFF) val shizukuBlueDark = Color(0xFFB7C4F4) val onShizukuBlueDark = Color(0xFF0A305F) -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt index 73ee785fc6..f22223f400 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt @@ -15,7 +15,7 @@ import timber.log.Timber class EvdevHandleCache( private val coroutineScope: CoroutineScope, private val devicesAdapter: DevicesAdapter, - private val systemBridge: StateFlow + private val systemBridge: StateFlow, ) { private val devicesByPath: StateFlow> = combine(devicesAdapter.connectedInputDevices, systemBridge) { _, systemBridge -> @@ -35,7 +35,7 @@ class EvdevHandleCache( name = device.name, bus = device.bus, vendor = device.vendor, - product = device.product + product = device.product, ) } } @@ -52,4 +52,4 @@ class EvdevHandleCache( it.product == deviceInfo.product } } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt index 3875fa5ab6..276c6b4bb5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt @@ -10,7 +10,7 @@ data class InjectKeyEventModel( val deviceId: Int, val scanCode: Int, val source: Int, - val repeatCount: Int = 0 + val repeatCount: Int = 0, ) { fun toAndroidKeyEvent(flags: Int = 0): KeyEvent { val eventTime = SystemClock.uptimeMillis() @@ -23,7 +23,7 @@ data class InjectKeyEventModel( metaState, deviceId, scanCode, - flags, // flags + flags, source, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt index 63926bbad3..8b06b10e01 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt @@ -4,4 +4,4 @@ enum class InputEventDetectionSource { ACCESSIBILITY_SERVICE, INPUT_METHOD, EVDEV, -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 4423e3f7c9..69d14ea7f7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -80,7 +80,7 @@ class InputEventHubImpl @Inject constructor( private val evdevHandles: EvdevHandleCache = EvdevHandleCache( coroutineScope, devicesAdapter, - systemBridgeFlow + systemBridgeFlow, ) private val logInputEventsEnabled: StateFlow = @@ -131,7 +131,7 @@ class InputEventHubImpl @Inject constructor( type: Int, code: Int, value: Int, - androidCode: Int + androidCode: Int, ): Boolean { devicePath ?: return false @@ -143,7 +143,7 @@ class InputEventHubImpl @Inject constructor( override fun onInputEvent( event: KMInputEvent, - detectionSource: InputEventDetectionSource + detectionSource: InputEventDetectionSource, ): Boolean { val uniqueEvent: KMInputEvent = if (event is KMKeyEvent) { makeUniqueKeyEvent(event) @@ -162,8 +162,8 @@ class InputEventHubImpl @Inject constructor( // TODO maybe flatmap all the client event types into one Set so this check // can be done in onEvdevEvent. Hundreds of events may be sent per second synchronously so important to be as fast as possible. // This client can ignore this event. - if (!clientContext.evdevEventTypes.contains(event.type) - || clientContext.grabbedEvdevDevices.isEmpty() + if (!clientContext.evdevEventTypes.contains(event.type) || + clientContext.grabbedEvdevDevices.isEmpty() ) { continue } @@ -172,7 +172,7 @@ class InputEventHubImpl @Inject constructor( event.device.name, event.device.bus, event.device.vendor, - event.device.product + event.device.product, ) // Only send events from evdev devices to the client if they grabbed it @@ -225,13 +225,13 @@ class InputEventHubImpl @Inject constructor( when (event) { is KMEvdevEvent -> { Timber.d( - "Evdev event: devicePath=${event.device.path}, deviceName=${event.device.name}, type=${event.type}, code=${event.code}, value=${event.value}" + "Evdev event: devicePath=${event.device.path}, deviceName=${event.device.name}, type=${event.type}, code=${event.code}, value=${event.value}", ) } is KMGamePadEvent -> { Timber.d( - "GamePad event: deviceId=${event.deviceId}, axisHatX=${event.axisHatX}, axisHatY=${event.axisHatY}" + "GamePad event: deviceId=${event.deviceId}, axisHatX=${event.axisHatX}, axisHatY=${event.axisHatY}", ) } @@ -256,7 +256,7 @@ class InputEventHubImpl @Inject constructor( override fun registerClient( clientId: String, callback: InputEventHubCallback, - eventTypes: List + eventTypes: List, ) { if (clients.containsKey(clientId)) { throw IllegalArgumentException("This client already has a callback registered!") @@ -326,7 +326,7 @@ class InputEventHubImpl @Inject constructor( devicePath: String, type: Int, code: Int, - value: Int + value: Int, ): KMResult { val systemBridge = this.systemBridgeFlow.value @@ -340,7 +340,7 @@ class InputEventHubImpl @Inject constructor( devicePath, type, code, - value + value, ) Timber.d("Injected evdev event: $result") @@ -371,7 +371,7 @@ class InputEventHubImpl @Inject constructor( // in InputDispatcher.cpp injectInputEvent. systemBridge.injectInputEvent( androidKeyEvent, - INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH + INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH, ) } @@ -401,7 +401,7 @@ class InputEventHubImpl @Inject constructor( * The evdev devices that this client wants to grab. */ val grabbedEvdevDevices: Set, - val evdevEventTypes: Set + val evdevEventTypes: Set, ) } @@ -415,7 +415,7 @@ interface InputEventHub { fun registerClient( clientId: String, callback: InputEventHubCallback, - eventTypes: List + eventTypes: List, ) fun unregisterClient(clientId: String) @@ -448,4 +448,4 @@ interface InputEventHub { fun onInputEvent(event: KMInputEvent, detectionSource: InputEventDetectionSource): Boolean fun isSystemBridgeConnected(): Boolean -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt index c0f86d205e..68e6806eee 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt @@ -7,4 +7,4 @@ interface InputEventHubCallback { * @return whether to consume the event. */ fun onInputEvent(event: KMInputEvent, detectionSource: InputEventDetectionSource): Boolean -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt index 1f9a670aa9..0b9a757e0b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt @@ -357,7 +357,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( keyCode: Int, scanCode: Int, device: KeyEventTriggerDevice, - requiresIme: Boolean + requiresIme: Boolean, ) = editTrigger { trigger -> val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -410,7 +410,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( override fun addEvdevTriggerKey( keyCode: Int, scanCode: Int, - device: EvdevDeviceInfo + device: EvdevDeviceInfo, ) = editTrigger { trigger -> val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -1054,7 +1054,7 @@ interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { keyCode: Int, scanCode: Int, device: KeyEventTriggerDevice, - requiresIme: Boolean + requiresIme: Boolean, ) suspend fun addFloatingButtonTriggerKey(buttonUid: String) @@ -1063,7 +1063,7 @@ interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { fun addEvdevTriggerKey( keyCode: Int, scanCode: Int, - device: EvdevDeviceInfo + device: EvdevDeviceInfo, ) fun removeTriggerKey(uid: String) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt index 8dad7f4449..44c129a77f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt @@ -53,7 +53,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( private val vibrator: VibratorAdapter, @Assisted private val coroutineScope: CoroutineScope, - private val inputEventHub: InputEventHub + private val inputEventHub: InputEventHub, ) : DetectKeyMapsUseCase { @AssistedFactory diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt index 0b153e03ce..95fb74e0cd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt @@ -109,7 +109,7 @@ class DpadMotionEventTracker { device = event.device, repeatCount = 0, source = InputDevice.SOURCE_DPAD, - eventTime = event.eventTime + eventTime = event.eventTime, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index a6dd91a9b6..75b04c5b43 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -611,7 +611,6 @@ class KeyMapAlgorithm( } private fun onKeyEventPostFilter(inputEvent: KMInputEvent): Boolean { - if (inputEvent is KMKeyEvent) { metaStateFromKeyEvent = inputEvent.metaState } @@ -663,7 +662,7 @@ class KeyMapAlgorithm( scanCode = inputEvent.scanCode, repeatCount = inputEvent.repeatCount, source = inputEvent.source, - isExternal = device.isExternal + isExternal = device.isExternal, ) when (inputEvent.action) { @@ -1456,14 +1455,14 @@ class KeyMapAlgorithm( devicePath = event.devicePath, KMEvdevEvent.TYPE_KEY_EVENT, event.scanCode, - KMEvdevEvent.VALUE_DOWN + KMEvdevEvent.VALUE_DOWN, ) useCase.imitateEvdevEvent( devicePath = event.devicePath, KMEvdevEvent.TYPE_KEY_EVENT, event.scanCode, - KMEvdevEvent.VALUE_UP + KMEvdevEvent.VALUE_UP, ) } } @@ -1510,20 +1509,20 @@ class KeyMapAlgorithm( devicePath = event.devicePath, type = KMEvdevEvent.TYPE_KEY_EVENT, code = event.scanCode, - value = KMEvdevEvent.VALUE_UP + value = KMEvdevEvent.VALUE_UP, ) } else { useCase.imitateEvdevEvent( devicePath = event.devicePath, type = KMEvdevEvent.TYPE_KEY_EVENT, code = event.scanCode, - value = KMEvdevEvent.VALUE_DOWN + value = KMEvdevEvent.VALUE_DOWN, ) useCase.imitateEvdevEvent( devicePath = event.devicePath, type = KMEvdevEvent.TYPE_KEY_EVENT, code = event.scanCode, - value = KMEvdevEvent.VALUE_UP + value = KMEvdevEvent.VALUE_UP, ) } keyCodesToImitateUpAction.remove(event.keyCode) @@ -1868,7 +1867,7 @@ class KeyMapAlgorithm( val device: EvdevDeviceInfo, val scanCode: Int, val keyCode: Int, - override val clickType: ClickType? + override val clickType: ClickType?, ) : AlgoEvent() private data class KeyEventAlgo( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt index de3465abe6..a6eac5f71f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt @@ -30,7 +30,7 @@ class KeyMapDetectionController( private val detectConstraints: DetectConstraintsUseCase, private val inputEventHub: InputEventHub, private val pauseKeyMapsUseCase: PauseKeyMapsUseCase, - private val recordTriggerController: RecordTriggerController + private val recordTriggerController: RecordTriggerController, ) : InputEventHubCallback { companion object { private const val INPUT_EVENT_HUB_ID = "key_map_controller" @@ -64,7 +64,7 @@ class KeyMapDetectionController( override fun onInputEvent( event: KMInputEvent, - detectionSource: InputEventDetectionSource + detectionSource: InputEventDetectionSource, ): Boolean { if (isPaused.value) { return false @@ -116,4 +116,4 @@ class KeyMapDetectionController( algorithm.reset() inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList()) } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index 0cf890cdc3..236f4623f5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -21,7 +21,7 @@ class ProModeViewModel @Inject constructor( private val useCase: ProModeSetupUseCase, resourceProvider: ResourceProvider, dialogProvider: DialogProvider, - navigationProvider: NavigationProvider + navigationProvider: NavigationProvider, ) : ViewModel(), ResourceProvider by resourceProvider, DialogProvider by dialogProvider, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt index 8d1e4b19ad..9dc545c9ac 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt @@ -30,7 +30,7 @@ class RerouteKeyEventsController @AssistedInject constructor( private val coroutineScope: CoroutineScope, private val keyMapperImeMessenger: ImeInputEventInjector, private val useCase: RerouteKeyEventsUseCase, - private val inputEventHub: InputEventHub + private val inputEventHub: InputEventHub, ) : InputEventHubCallback { companion object { @@ -59,7 +59,7 @@ class RerouteKeyEventsController @AssistedInject constructor( inputEventHub.registerClient( INPUT_EVENT_HUB_ID, this@RerouteKeyEventsController, - listOf(KMEvdevEvent.TYPE_KEY_EVENT) + listOf(KMEvdevEvent.TYPE_KEY_EVENT), ) } else { inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) @@ -74,7 +74,7 @@ class RerouteKeyEventsController @AssistedInject constructor( override fun onInputEvent( event: KMInputEvent, - detectionSource: InputEventDetectionSource + detectionSource: InputEventDetectionSource, ): Boolean { if (event !is KMKeyEvent) { return false @@ -95,7 +95,7 @@ class RerouteKeyEventsController @AssistedInject constructor( * @return whether to consume the key event. */ private fun onKeyDown( - event: KMKeyEvent + event: KMKeyEvent, ): Boolean { val injectModel = InjectKeyEventModel( keyCode = event.keyCode, @@ -104,7 +104,7 @@ class RerouteKeyEventsController @AssistedInject constructor( deviceId = event.deviceId, scanCode = event.scanCode, repeatCount = event.repeatCount, - source = event.source + source = event.source, ) useCase.inputKeyEvent(injectModel) @@ -136,7 +136,7 @@ class RerouteKeyEventsController @AssistedInject constructor( deviceId = event.deviceId, scanCode = event.scanCode, repeatCount = event.repeatCount, - source = event.source + source = event.source, ) useCase.inputKeyEvent(inputKeyModel) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 0dbbf59b92..81028e19d4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -34,7 +34,7 @@ class SettingsViewModel @Inject constructor( private val resourceProvider: ResourceProvider, val sharedPrefsDataStoreWrapper: SharedPrefsDataStoreWrapper, dialogProvider: DialogProvider, - navigationProvider: NavigationProvider + navigationProvider: NavigationProvider, ) : ViewModel(), DialogProvider by dialogProvider, ResourceProvider by resourceProvider, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 6fa5d322b4..0bbef14720 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -67,7 +67,7 @@ abstract class BaseAccessibilityServiceController( private val systemBridgeSetupController: SystemBridgeSetupController, private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, private val inputEventHub: InputEventHub, - private val recordTriggerController: RecordTriggerController + private val recordTriggerController: RecordTriggerController, ) { companion object { private const val DEFAULT_NOTIFICATION_TIMEOUT = 200L @@ -92,7 +92,7 @@ abstract class BaseAccessibilityServiceController( detectConstraintsUseCase, inputEventHub, pauseKeyMapsUseCase, - recordTriggerController + recordTriggerController, ) val triggerKeyMapFromOtherAppsController = TriggerKeyMapFromOtherAppsController( @@ -166,7 +166,6 @@ abstract class BaseAccessibilityServiceController( private val outputEvents: MutableSharedFlow = service.accessibilityServiceAdapter.eventReceiver - private val relayServiceCallback: IKeyEventRelayServiceCallback = object : IKeyEventRelayServiceCallback.Stub() { override fun onKeyEvent(event: KeyEvent?): Boolean { @@ -185,7 +184,6 @@ abstract class BaseAccessibilityServiceController( } } - init { serviceFlags.onEach { flags -> // check that it isn't null because this can only be called once the service is bound @@ -343,7 +341,7 @@ abstract class BaseAccessibilityServiceController( keyEventRelayServiceWrapper.registerClient( CALLBACK_ID_ACCESSIBILITY_SERVICE, - relayServiceCallback + relayServiceCallback, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 1228fe7dca..9c57bfb74c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -23,7 +23,6 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.ProductId import io.github.sds100.keymapper.base.purchasing.PurchasingManager import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.trigger.TriggerKeyListItemModel.* import io.github.sds100.keymapper.base.utils.InputEventStrings import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem @@ -405,7 +404,7 @@ abstract class BaseConfigTriggerViewModel( return TriggerKeyOptionsState.EvdevEvent( doNotRemapChecked = !key.consumeEvent, clickType = key.clickType, - showClickTypes = showClickTypes + showClickTypes = showClickTypes, ) } } @@ -489,8 +488,10 @@ abstract class BaseConfigTriggerViewModel( } config.addKeyEventTriggerKey( - key.keyCode, key.scanCode, triggerDevice, - key.detectionSource != InputEventDetectionSource.ACCESSIBILITY_SERVICE + key.keyCode, + key.scanCode, + triggerDevice, + key.detectionSource != InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) if (key.keyCode >= InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET || key.keyCode < 0) { @@ -555,7 +556,7 @@ abstract class BaseConfigTriggerViewModel( bus = key.device.bus, vendor = key.device.vendor, product = key.device.product, - ) + ), ) } @@ -712,7 +713,7 @@ abstract class BaseConfigTriggerViewModel( } when (key) { - is AssistantTriggerKey -> Assistant( + is AssistantTriggerKey -> TriggerKeyListItemModel.Assistant( id = key.uid, assistantType = key.type, clickType = clickType, @@ -720,7 +721,7 @@ abstract class BaseConfigTriggerViewModel( error = error, ) - is FingerprintTriggerKey -> FingerprintGesture( + is FingerprintTriggerKey -> TriggerKeyListItemModel.FingerprintGesture( id = key.uid, gestureType = key.type, clickType = clickType, @@ -728,7 +729,7 @@ abstract class BaseConfigTriggerViewModel( error = error, ) - is KeyEventTriggerKey -> KeyEvent( + is KeyEventTriggerKey -> TriggerKeyListItemModel.KeyEvent( id = key.uid, keyName = getTriggerKeyName(key), clickType = clickType, @@ -742,13 +743,13 @@ abstract class BaseConfigTriggerViewModel( is FloatingButtonKey -> { if (key.button == null) { - FloatingButtonDeleted( + TriggerKeyListItemModel.FloatingButtonDeleted( id = key.uid, clickType = clickType, linkType = linkType, ) } else { - FloatingButton( + TriggerKeyListItemModel.FloatingButton( id = key.uid, buttonName = key.button.appearance.text, layoutName = key.button.layoutName, @@ -759,7 +760,7 @@ abstract class BaseConfigTriggerViewModel( } } - is EvdevTriggerKey -> EvdevEvent( + is EvdevTriggerKey -> TriggerKeyListItemModel.EvdevEvent( id = key.uid, keyName = InputEventStrings.keyCodeToString(key.keyCode), deviceName = key.device.name, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt index 85f2c71778..5f52b1fec6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt @@ -73,9 +73,8 @@ data class EvdevTriggerKey( deviceProduct = key.device.product, clickType = clickType, flags = flags, - uid = key.uid + uid = key.uid, ) } } - } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt index 5bcbcafa73..f3ff02dcf7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt @@ -11,4 +11,4 @@ sealed interface InputEventTriggerKey { */ val scanCode: Int? val clickType: ClickType -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt index 2f3c7ee069..bff15d411b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt @@ -40,4 +40,4 @@ sealed class KeyEventTriggerDevice() : Comparable { return this is Internal && other is Internal } } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt index 73f2425287..bf7b5b4635 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -11,7 +11,7 @@ import java.util.UUID @Serializable data class KeyEventTriggerKey( override val uid: String = UUID.randomUUID().toString(), - override val keyCode: Int, + override val keyCode: Int, val device: KeyEventTriggerDevice, override val clickType: ClickType, override val consumeEvent: Boolean = true, @@ -20,7 +20,7 @@ data class KeyEventTriggerKey( * do not send key events to the accessibility service. */ val requiresIme: Boolean = false, - override val scanCode: Int? = null, + override val scanCode: Int? = null, ) : TriggerKey(), InputEventTriggerKey { override val allowedLongPress: Boolean = true @@ -80,7 +80,7 @@ data class KeyEventTriggerKey( clickType = clickType, consumeEvent = consumeEvent, requiresIme = requiresIme, - scanCode = entity.scanCode + scanCode = entity.scanCode, ) } @@ -121,7 +121,7 @@ data class KeyEventTriggerKey( clickType = clickType, flags = flags, uid = key.uid, - scanCode = key.scanCode + scanCode = key.scanCode, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index a59fca7fc1..19f7a5d6c3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -44,7 +44,8 @@ class RecordTriggerControllerImpl @Inject constructor( private const val INPUT_EVENT_HUB_ID = "record_trigger" private val SCAN_CODES_BLACKLIST = setOf( - 330 // BTN_TOUCH + // BTN_TOUCH + 330, ) } @@ -68,7 +69,7 @@ class RecordTriggerControllerImpl @Inject constructor( override fun onInputEvent( event: KMInputEvent, - detectionSource: InputEventDetectionSource + detectionSource: InputEventDetectionSource, ): Boolean { if (!recordingTrigger) { return false @@ -114,7 +115,7 @@ class RecordTriggerControllerImpl @Inject constructor( val recordedKey = createKeyEventRecordedKey( keyEvent, - detectionSource + detectionSource, ) onRecordKey(recordedKey) } @@ -138,7 +139,6 @@ class RecordTriggerControllerImpl @Inject constructor( if (event.action == KeyEvent.ACTION_DOWN) { downKeyEvents.add(event) } else if (event.action == KeyEvent.ACTION_UP) { - // Only record the key if there is a matching down event. // Do not do this when recording motion events from the input method // or Activity because they intentionally only input a down event. @@ -146,7 +146,6 @@ class RecordTriggerControllerImpl @Inject constructor( val recordedKey = createKeyEventRecordedKey(event, detectionSource) onRecordKey(recordedKey) } - } return true } @@ -220,7 +219,7 @@ class RecordTriggerControllerImpl @Inject constructor( private fun createKeyEventRecordedKey( keyEvent: KMKeyEvent, - detectionSource: InputEventDetectionSource + detectionSource: InputEventDetectionSource, ): RecordedKey.KeyEvent { return RecordedKey.KeyEvent( keyCode = keyEvent.keyCode, @@ -228,7 +227,7 @@ class RecordTriggerControllerImpl @Inject constructor( deviceDescriptor = keyEvent.device.descriptor, deviceName = keyEvent.device.name, isExternalDevice = keyEvent.device.isExternal, - detectionSource = detectionSource + detectionSource = detectionSource, ) } @@ -236,7 +235,7 @@ class RecordTriggerControllerImpl @Inject constructor( return RecordedKey.EvdevEvent( keyCode = evdevEvent.androidCode, scanCode = evdevEvent.code, - device = evdevEvent.device + device = evdevEvent.device, ) } @@ -250,7 +249,7 @@ class RecordTriggerControllerImpl @Inject constructor( inputEventHub.registerClient( INPUT_EVENT_HUB_ID, this@RecordTriggerControllerImpl, - listOf(KMEvdevEvent.TYPE_KEY_EVENT) + listOf(KMEvdevEvent.TYPE_KEY_EVENT), ) // Grab all evdev devices diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt index f8fb70d3a4..cad2f10f2d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt @@ -16,6 +16,6 @@ sealed class RecordedKey { data class EvdevEvent( val keyCode: Int, val scanCode: Int, - val device: EvdevDeviceHandle + val device: EvdevDeviceHandle, ) : RecordedKey() -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt index 54a34737a1..ea2bf1c8f0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt @@ -365,7 +365,6 @@ object InputEventStrings { KeyEvent.KEYCODE_SCREENSHOT to "Screenshot", ) - /** * Create a text representation of a key event. E.g if the control key was pressed, * "Ctrl" will be returned diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt index f67b37bc8c..ec3368f4da 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt @@ -18,7 +18,7 @@ val KeyMapperIcons.ProModeIcon: ImageVector defaultWidth = 32.dp, defaultHeight = 32.dp, viewportWidth = 32f, - viewportHeight = 32f + viewportHeight = 32f, ).apply { group( clipPathData = PathData { @@ -27,7 +27,7 @@ val KeyMapperIcons.ProModeIcon: ImageVector lineTo(32f, 32f) lineTo(0f, 32f) close() - } + }, ) { } group( @@ -37,7 +37,7 @@ val KeyMapperIcons.ProModeIcon: ImageVector lineTo(32f, 32f) lineTo(0f, 32f) close() - } + }, ) { } group( @@ -47,7 +47,7 @@ val KeyMapperIcons.ProModeIcon: ImageVector lineTo(32f, 32f) lineTo(-0f, 32f) close() - } + }, ) { } group( @@ -57,7 +57,7 @@ val KeyMapperIcons.ProModeIcon: ImageVector lineTo(32f, -0f) lineTo(-0f, -0f) close() - } + }, ) { } group( @@ -67,7 +67,7 @@ val KeyMapperIcons.ProModeIcon: ImageVector lineTo(32f, 32f) lineTo(0f, 32f) close() - } + }, ) { } group( @@ -77,7 +77,7 @@ val KeyMapperIcons.ProModeIcon: ImageVector lineTo(32f, -0f) lineTo(-0f, -0f) close() - } + }, ) { } group( @@ -87,7 +87,7 @@ val KeyMapperIcons.ProModeIcon: ImageVector lineTo(32f, -0f) lineTo(-0f, -0f) close() - } + }, ) { } path(fill = SolidColor(Color.Black)) { diff --git a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt index 261a7919f5..7cec55bf22 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt @@ -67,7 +67,7 @@ class ConfigKeyMapUseCaseTest { FloatingButtonKey( buttonUid = "floating_button", button = null, - clickType = ClickType.SHORT_PRESS + clickType = ClickType.SHORT_PRESS, ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_UP, @@ -76,12 +76,12 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) + product = 2, + ), ), AssistantTriggerKey( type = AssistantTriggerType.ANY, - clickType = ClickType.SHORT_PRESS + clickType = ClickType.SHORT_PRESS, ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, @@ -90,18 +90,18 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) + product = 2, + ), ), - ) - ) + ), + ), ) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger @@ -110,7 +110,8 @@ class ConfigKeyMapUseCaseTest { assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) assertThat(trigger.keys[2], instanceOf(KeyEventTriggerKey::class.java)) assertThat( - (trigger.keys[2] as KeyEventTriggerKey).requiresIme, `is`(false) + (trigger.keys[2] as KeyEventTriggerKey).requiresIme, + `is`(false), ) } @@ -123,34 +124,34 @@ class ConfigKeyMapUseCaseTest { FloatingButtonKey( buttonUid = "floating_button", button = null, - clickType = ClickType.SHORT_PRESS + clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - KeyEventTriggerDevice.Internal + KeyEventTriggerDevice.Internal, ), AssistantTriggerKey( type = AssistantTriggerType.ANY, - clickType = ClickType.SHORT_PRESS + clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - KeyEventTriggerDevice.Internal - ) - ) - ) + KeyEventTriggerDevice.Internal, + ), + ), + ), ) val evdevDevice = EvdevDeviceInfo( name = "Volume Keys", bus = 0, vendor = 1, - product = 2 + product = 2, ) useCase.addEvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = 0, - device = evdevDevice + device = evdevDevice, ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger @@ -159,7 +160,8 @@ class ConfigKeyMapUseCaseTest { assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) assertThat(trigger.keys[2], instanceOf(EvdevTriggerKey::class.java)) assertThat( - (trigger.keys[2] as EvdevTriggerKey).device, `is`(evdevDevice) + (trigger.keys[2] as EvdevTriggerKey).device, + `is`(evdevDevice), ) } @@ -176,20 +178,21 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) + product = 2, + ), ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, device = EvdevDeviceInfo( + scanCode = 0, + device = EvdevDeviceInfo( name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) - ) - ) - ) + product = 2, + ), + ), + ), + ), ) useCase.setParallelTriggerMode() @@ -199,7 +202,7 @@ class ConfigKeyMapUseCaseTest { assertThat(trigger.keys[0], instanceOf(EvdevTriggerKey::class.java)) assertThat( (trigger.keys[0] as EvdevTriggerKey).keyCode, - `is`(KeyEvent.KEYCODE_VOLUME_DOWN) + `is`(KeyEvent.KEYCODE_VOLUME_DOWN), ) } @@ -215,8 +218,8 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) + product = 2, + ), ) useCase.addEvdevTriggerKey( @@ -226,8 +229,8 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) + product = 2, + ), ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger @@ -247,8 +250,8 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) + product = 2, + ), ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, @@ -257,11 +260,11 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) - ) - ) - ) + product = 2, + ), + ), + ), + ), ) // Add a third key and it should still be a sequence trigger now @@ -272,8 +275,8 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) + product = 2, + ), ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger @@ -292,8 +295,8 @@ class ConfigKeyMapUseCaseTest { name = "Volume Keys", bus = 0, vendor = 1, - product = 2 - ) + product = 2, + ), ) useCase.addEvdevTriggerKey( @@ -303,13 +306,12 @@ class ConfigKeyMapUseCaseTest { name = "Fake Controller", bus = 1, vendor = 2, - product = 1 - ) + product = 1, + ), ) val trigger = useCase.keyMap.value.dataOrNull()!!.trigger assertThat(trigger.mode, `is`(TriggerMode.Parallel(ClickType.SHORT_PRESS))) - } @Test @@ -321,7 +323,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -342,7 +344,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -389,7 +391,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.setTriggerDoublePress() @@ -411,7 +413,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.setTriggerDoublePress() @@ -433,7 +435,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.setTriggerLongPress() @@ -455,7 +457,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.setTriggerLongPress() @@ -476,7 +478,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_DPAD_LEFT, 0, KeyEventTriggerDevice.Internal, - true + true, ) useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) @@ -498,7 +500,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) @@ -522,7 +524,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) @@ -546,13 +548,13 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_UP, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.setTriggerLongPress() @@ -571,7 +573,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.setTriggerDoublePress() useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -589,7 +591,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_VOLUME_DOWN, 0, KeyEventTriggerDevice.Internal, - false + false, ) useCase.setTriggerLongPress() useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -651,7 +653,7 @@ class ConfigKeyMapUseCaseTest { modifierKeyCode, 0, KeyEventTriggerDevice.Internal, - false + false, ) // THEN @@ -675,7 +677,7 @@ class ConfigKeyMapUseCaseTest { KeyEvent.KEYCODE_A, 0, KeyEventTriggerDevice.Internal, - false + false, ) // THEN diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index 099d4cc084..8c22c9788e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -79,7 +79,7 @@ class PerformActionsUseCaseTest { soundsManager = mock(), notificationReceiverAdapter = mock(), ringtoneAdapter = mock(), - inputEventHub = mockInputEventHub + inputEventHub = mockInputEventHub, ) } @@ -119,7 +119,7 @@ class PerformActionsUseCaseTest { id = 1, isExternal = true, isGameController = true, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ) fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad)) @@ -197,7 +197,7 @@ class PerformActionsUseCaseTest { id = 1, isExternal = true, isGameController = true, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ) val fakeKeyboard = InputDeviceInfo( @@ -206,7 +206,7 @@ class PerformActionsUseCaseTest { id = 2, isExternal = true, isGameController = false, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ) fakeDevicesAdapter.connectedInputDevices.value = @@ -268,7 +268,7 @@ class PerformActionsUseCaseTest { id = 10, isExternal = true, isGameController = false, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ), InputDeviceInfo( @@ -277,7 +277,7 @@ class PerformActionsUseCaseTest { id = 11, isExternal = true, isGameController = false, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ), ), ) @@ -327,7 +327,7 @@ class PerformActionsUseCaseTest { id = 10, isExternal = true, isGameController = false, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ), ), ) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt index 2a60387a7a..06c15a8e01 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt @@ -72,7 +72,7 @@ class ConfigKeyServiceEventActionViewModelTest { id = 0, isExternal = false, isGameController = false, - sources = InputDevice.SOURCE_KEYBOARD + sources = InputDevice.SOURCE_KEYBOARD, ) val fakeDevice2 = InputDeviceInfo( @@ -81,7 +81,7 @@ class ConfigKeyServiceEventActionViewModelTest { id = 1, isExternal = false, isGameController = false, - sources = InputDevice.SOURCE_KEYBOARD + sources = InputDevice.SOURCE_KEYBOARD, ) // WHEN diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt index 0447de4325..b91700dec6 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt @@ -27,7 +27,7 @@ class DpadMotionEventTrackerTest { name = "Controller 1", isExternal = true, isGameController = true, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ) private val CONTROLLER_2_DEVICE = InputDeviceInfo( @@ -36,7 +36,7 @@ class DpadMotionEventTrackerTest { name = "Controller 2", isExternal = true, isGameController = true, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ) } @@ -70,7 +70,7 @@ class DpadMotionEventTrackerTest { device = CONTROLLER_1_DEVICE, repeatCount = 0, source = InputDevice.SOURCE_DPAD, - eventTime = motionEvent.eventTime + eventTime = motionEvent.eventTime, ), ), ) @@ -85,7 +85,7 @@ class DpadMotionEventTrackerTest { device = CONTROLLER_1_DEVICE, repeatCount = 0, source = InputDevice.SOURCE_DPAD, - eventTime = motionEvent.eventTime + eventTime = motionEvent.eventTime, ), ), ) @@ -267,7 +267,7 @@ class DpadMotionEventTrackerTest { device = device, axisHatX = axisHatX, axisHatY = axisHatY, - eventTime = System.currentTimeMillis() + eventTime = System.currentTimeMillis(), ) } @@ -280,7 +280,7 @@ class DpadMotionEventTrackerTest { device = device, repeatCount = 0, source = 0, - eventTime = System.currentTimeMillis() + eventTime = System.currentTimeMillis(), ) } @@ -293,7 +293,7 @@ class DpadMotionEventTrackerTest { device = device, repeatCount = 0, source = 0, - eventTime = System.currentTimeMillis() + eventTime = System.currentTimeMillis(), ) } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 41351578e9..4e2e68c4f9 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -86,7 +86,7 @@ class KeyMapAlgorithmTest { id = 0, isExternal = false, isGameController = false, - sources = InputDevice.SOURCE_UNKNOWN + sources = InputDevice.SOURCE_UNKNOWN, ) private const val FAKE_HEADPHONE_DESCRIPTOR = "fake_headphone" @@ -106,21 +106,21 @@ class KeyMapAlgorithmTest { id = 1, isExternal = true, isGameController = true, - sources = InputDevice.SOURCE_GAMEPAD + sources = InputDevice.SOURCE_GAMEPAD, ) private val FAKE_CONTROLLER_EVDEV_DEVICE = EvdevDeviceInfo( name = "Fake Controller", bus = 1, vendor = 2, - product = 1 + product = 1, ) private val FAKE_VOLUME_EVDEV_DEVICE = EvdevDeviceInfo( name = "Volume Keys", bus = 0, vendor = 1, - product = 2 + product = 2, ) private const val FAKE_PACKAGE_NAME = "test_package" @@ -228,12 +228,12 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, scanCode = 900, - device = FAKE_CONTROLLER_EVDEV_DEVICE + device = FAKE_CONTROLLER_EVDEV_DEVICE, ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_B, scanCode = 901, - device = FAKE_CONTROLLER_EVDEV_DEVICE + device = FAKE_CONTROLLER_EVDEV_DEVICE, ), ) @@ -255,12 +255,12 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, scanCode = 900, - device = FAKE_CONTROLLER_EVDEV_DEVICE + device = FAKE_CONTROLLER_EVDEV_DEVICE, ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_B, scanCode = 901, - device = FAKE_CONTROLLER_EVDEV_DEVICE + device = FAKE_CONTROLLER_EVDEV_DEVICE, ), ) @@ -282,8 +282,8 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_POWER, scanCode = 900, - device = FAKE_VOLUME_EVDEV_DEVICE - ) + device = FAKE_VOLUME_EVDEV_DEVICE, + ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -299,8 +299,8 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, scanCode = 900, - device = FAKE_CONTROLLER_EVDEV_DEVICE - ) + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -316,8 +316,8 @@ class KeyMapAlgorithmTest { EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_POWER, scanCode = 900, - device = FAKE_VOLUME_EVDEV_DEVICE - ) + device = FAKE_VOLUME_EVDEV_DEVICE, + ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -2033,11 +2033,11 @@ class KeyMapAlgorithmTest { // THEN verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( keyCode = 1, - action = KeyEvent.ACTION_DOWN + action = KeyEvent.ACTION_DOWN, ) verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( keyCode = 1, - action = KeyEvent.ACTION_UP + action = KeyEvent.ACTION_UP, ) verifyNoMoreInteractions() @@ -2049,19 +2049,19 @@ class KeyMapAlgorithmTest { // THEN verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 1, - action = KeyEvent.ACTION_DOWN + action = KeyEvent.ACTION_DOWN, ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 1, - action = KeyEvent.ACTION_UP + action = KeyEvent.ACTION_UP, ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 2, - action = KeyEvent.ACTION_DOWN + action = KeyEvent.ACTION_DOWN, ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 2, - action = KeyEvent.ACTION_UP + action = KeyEvent.ACTION_UP, ) verify(performActionsUseCase, never()).perform(action = TEST_ACTION.data) @@ -2115,19 +2115,19 @@ class KeyMapAlgorithmTest { // THEN verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 1, - action = KeyEvent.ACTION_DOWN + action = KeyEvent.ACTION_DOWN, ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 1, - action = KeyEvent.ACTION_UP + action = KeyEvent.ACTION_UP, ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 2, - action = KeyEvent.ACTION_DOWN + action = KeyEvent.ACTION_DOWN, ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( keyCode = 2, - action = KeyEvent.ACTION_UP + action = KeyEvent.ACTION_UP, ) } @@ -2966,7 +2966,7 @@ class KeyMapAlgorithmTest { inputKeyEvent( KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_DOWN, - FAKE_INTERNAL_DEVICE + FAKE_INTERNAL_DEVICE, ) inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_INTERNAL_DEVICE) @@ -3255,12 +3255,12 @@ class KeyMapAlgorithmTest { verify(detectKeyMapsUseCase, times(1)) .imitateKeyEvent( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - action = KeyEvent.ACTION_DOWN + action = KeyEvent.ACTION_DOWN, ) verify(detectKeyMapsUseCase, times(1)) .imitateKeyEvent( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - action = KeyEvent.ACTION_UP + action = KeyEvent.ACTION_UP, ) } @@ -3377,7 +3377,7 @@ class KeyMapAlgorithmTest { verify(detectKeyMapsUseCase, times(1)) .imitateKeyEvent( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - action = KeyEvent.ACTION_DOWN + action = KeyEvent.ACTION_DOWN, ) verify(detectKeyMapsUseCase, times(1)) @@ -4377,7 +4377,7 @@ class KeyMapAlgorithmTest { device = device, axisHatX = axisHatX, axisHatY = axisHatY, - eventTime = System.currentTimeMillis() + eventTime = System.currentTimeMillis(), ) } @@ -4391,7 +4391,7 @@ class KeyMapAlgorithmTest { device = device, axisHatX = axisHatX, axisHatY = axisHatY, - eventTime = System.currentTimeMillis() + eventTime = System.currentTimeMillis(), ), ) @@ -4411,14 +4411,14 @@ class KeyMapAlgorithmTest { device = device, repeatCount = repeatCount, source = 0, - eventTime = System.currentTimeMillis() + eventTime = System.currentTimeMillis(), ), ) private fun inputDownEvdevEvent( keyCode: Int, scanCode: Int, - device: EvdevDeviceInfo + device: EvdevDeviceInfo, ): Boolean = controller.onInputEvent( KMEvdevEvent( type = KMEvdevEvent.TYPE_KEY_EVENT, @@ -4427,20 +4427,20 @@ class KeyMapAlgorithmTest { name = device.name, bus = device.bus, vendor = device.vendor, - product = device.product + product = device.product, ), code = scanCode, androidCode = keyCode, value = KMEvdevEvent.VALUE_DOWN, timeSec = testScope.currentTime, - timeUsec = 0 + timeUsec = 0, ), ) private fun inputUpEvdevEvent( keyCode: Int, scanCode: Int, - device: EvdevDeviceInfo + device: EvdevDeviceInfo, ): Boolean = controller.onInputEvent( KMEvdevEvent( type = KMEvdevEvent.TYPE_KEY_EVENT, @@ -4449,12 +4449,13 @@ class KeyMapAlgorithmTest { name = device.name, bus = device.bus, vendor = device.vendor, - product = device.product - ), code = scanCode, + product = device.product, + ), + code = scanCode, androidCode = keyCode, value = KMEvdevEvent.VALUE_UP, timeSec = testScope.currentTime, - timeUsec = 0 + timeUsec = 0, ), ) @@ -4507,7 +4508,7 @@ class KeyMapAlgorithmTest { isExternal = false, id = deviceId, isGameController = isGameController, - sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD, ) is KeyEventTriggerDevice.External -> InputDeviceInfo( @@ -4516,7 +4517,7 @@ class KeyMapAlgorithmTest { isExternal = true, id = deviceId, isGameController = isGameController, - sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD, ) KeyEventTriggerDevice.Internal -> InputDeviceInfo( @@ -4525,7 +4526,7 @@ class KeyMapAlgorithmTest { isExternal = false, id = deviceId, isGameController = isGameController, - sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD, ) } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt index 6e9e673d4e..2543222291 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt @@ -60,4 +60,4 @@ class TriggerKeyDeviceTest { val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 0") assertThat(device1.isSameDevice(device2), `is`(true)) } -} \ No newline at end of file +} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt index 1fc5742b88..a7aaf7c89e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt @@ -34,4 +34,4 @@ fun triggerKey( clickType = clickType, consumeEvent = consume, requiresIme = requiresIme, -) \ No newline at end of file +) diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt index cb8cdcb1b5..1b14c03b86 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt @@ -13,4 +13,4 @@ data class EvdevDeviceHandle( val bus: Int, val vendor: Int, val product: Int, -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt index 1e8c32e648..1150b3c45e 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt @@ -8,5 +8,5 @@ data class EvdevDeviceInfo( val name: String, val bus: Int, val vendor: Int, - val product: Int + val product: Int, ) : Parcelable diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt index bb2f541446..e5605a3f8e 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt @@ -12,9 +12,9 @@ data class InputDeviceInfo( val id: Int, val isExternal: Boolean, val isGameController: Boolean, - val sources: Int + val sources: Int, ) : Parcelable { fun supportsSource(source: Int): Boolean { return sources and source == source } -} \ No newline at end of file +} diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt index d1e39d6102..ca94033015 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt @@ -32,7 +32,7 @@ object InputDeviceUtils { inputDevice.id, inputDevice.isExternalCompat, isGameController = inputDevice.controllerNumber != 0, - sources = inputDevice.sources + sources = inputDevice.sources, ) } @@ -79,4 +79,4 @@ fun InputDevice.getDeviceBus(): Int { } catch (e: InvocationTargetException) { -1 } -} \ No newline at end of file +} diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt index c8fbdcf24f..ac7493b303 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt @@ -164,4 +164,4 @@ fun KMResult.handle(onSuccess: (value: T) -> U, onError: (error: KMErr is KMError -> onError(this) } -fun T.success() = Success(this) \ No newline at end of file +fun T.success() = Success(this) diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt index d52d869985..789313a55a 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt @@ -32,7 +32,7 @@ data class EvdevTriggerKeyEntity( val flags: Int = 0, @SerializedName(NAME_UID) - override val uid: String = UUID.randomUUID().toString() + override val uid: String = UUID.randomUUID().toString(), ) : TriggerKeyEntity(), Parcelable { diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt index d5eb554d6e..f962abf064 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt @@ -26,7 +26,7 @@ data class KeyEventTriggerKeyEntity( override val uid: String = UUID.randomUUID().toString(), @SerializedName(NAME_SCANCODE) - val scanCode: Int? = null + val scanCode: Int? = null, ) : TriggerKeyEntity(), Parcelable { From 9a26c86bf32eccfbff2e9349237dc429f6f547d9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 16:49:05 +0100 Subject: [PATCH 090/215] remove code from #1276 that created a fake key code when it was unknown --- .../keymapper/base/input/InputEventHub.kt | 33 +------------------ .../base/onboarding/OnboardingUseCase.kt | 6 ---- .../trigger/BaseConfigTriggerViewModel.kt | 18 ---------- base/src/main/res/values/strings.xml | 3 -- .../io/github/sds100/keymapper/data/Keys.kt | 2 -- 5 files changed, 1 insertion(+), 61 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 69d14ea7f7..810f1d2bca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -17,7 +17,6 @@ import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent @@ -145,14 +144,8 @@ class InputEventHubImpl @Inject constructor( event: KMInputEvent, detectionSource: InputEventDetectionSource, ): Boolean { - val uniqueEvent: KMInputEvent = if (event is KMKeyEvent) { - makeUniqueKeyEvent(event) - } else { - event - } - if (logInputEventsEnabled.value) { - logInputEvent(uniqueEvent) + logInputEvent(event) } var consume = false @@ -197,30 +190,6 @@ class InputEventHubImpl @Inject constructor( return consume } - /** - * Sometimes key events are sent with an unknown key code so to make it unique, - * this will set a unique key code to the key event that won't conflict. - */ - private fun makeUniqueKeyEvent(event: KMKeyEvent): KMKeyEvent { - // Guard to ignore processing when not applicable - if (event.keyCode != KeyEvent.KEYCODE_UNKNOWN) { - return event - } - - // Don't offset negative values - val scanCodeOffset: Int = if (event.scanCode >= 0) { - InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET - } else { - 0 - } - - return event.copy( - // Fallback to scanCode when keyCode is unknown as it's typically more unique - // Add offset to go past possible keyCode values - keyCode = event.scanCode + scanCodeOffset, - ) - } - private fun logInputEvent(event: KMInputEvent) { when (event) { is KMEvdevEvent -> { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt index d01fa193b0..b385ec5821 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt @@ -77,11 +77,6 @@ class OnboardingUseCaseImpl @Inject constructor( Keys.shownSequenceTriggerExplanation, false, ) - override var shownKeyCodeToScanCodeTriggerExplanation by PrefDelegate( - Keys.shownKeyCodeToScanCodeTriggerExplanation, - false, - ) - override val showWhatsNew = get(Keys.lastInstalledVersionCodeHomeScreen) .map { (it ?: -1) < buildConfigProvider.versionCode } @@ -251,7 +246,6 @@ interface OnboardingUseCase { var shownParallelTriggerOrderExplanation: Boolean var shownSequenceTriggerExplanation: Boolean - var shownKeyCodeToScanCodeTriggerExplanation: Boolean val showFloatingButtonFeatureNotification: Flow fun showedFloatingButtonFeatureNotification() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 9c57bfb74c..0c5eefcd10 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -494,24 +494,6 @@ abstract class BaseConfigTriggerViewModel( key.detectionSource != InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) - if (key.keyCode >= InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET || key.keyCode < 0) { - if (onboarding.shownKeyCodeToScanCodeTriggerExplanation) { - return - } - - val dialog = DialogModel.Alert( - title = getString(R.string.dialog_title_keycode_to_scancode_trigger_explanation), - message = getString(R.string.dialog_message_keycode_to_scancode_trigger_explanation), - positiveButtonText = getString(R.string.pos_understood), - ) - - val response = showDialog("keycode_to_scancode_message", dialog) - - if (response == DialogResponse.POSITIVE) { - onboarding.shownKeyCodeToScanCodeTriggerExplanation = true - } - } - if (key.keyCode == KeyEvent.KEYCODE_CAPS_LOCK) { val dialog = DialogModel.Ok( message = getString(R.string.dialog_message_enable_physical_keyboard_caps_lock_a_keyboard_layout), diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 52b07396e5..c20924e98b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -507,9 +507,6 @@ Drag handle for %1$s Show example - 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. diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index f4f760ad78..cdc95f350e 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -52,8 +52,6 @@ object Keys { booleanPreferencesKey("key_shown_parallel_trigger_order_warning") val shownSequenceTriggerExplanation = booleanPreferencesKey("key_shown_sequence_trigger_explanation_dialog") - val shownKeyCodeToScanCodeTriggerExplanation = - booleanPreferencesKey("key_shown_keycode_to_scancode_trigger_explanation_dialog") val lastInstalledVersionCodeHomeScreen = intPreferencesKey("last_installed_version_home_screen") val lastInstalledVersionCodeBackground = From ca724b915d2b61451a9503d78fcd27f9a1e11319 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 17:59:22 +0100 Subject: [PATCH 091/215] #1394 #761 fall back to scan code automatically if the key code is unknown --- .../keymapper/base/actions/ActionUiHelper.kt | 4 +- .../base/actions/PerformActionsUseCase.kt | 8 +- .../keyevent/ChooseKeyCodeViewModel.kt | 4 +- .../keyevent/ConfigKeyEventActionViewModel.kt | 4 +- .../base/keymaps/ConfigKeyMapUseCase.kt | 24 +- .../base/keymaps/KeyMapListItemCreator.kt | 6 +- .../detection/DpadMotionEventTracker.kt | 4 +- .../base/keymaps/detection/KeyMapAlgorithm.kt | 35 +- .../ParallelTriggerActionPerformer.kt | 4 +- .../base/trigger/AssistantTriggerKey.kt | 4 - .../trigger/BaseConfigTriggerViewModel.kt | 31 +- .../keymapper/base/trigger/EvdevTriggerKey.kt | 11 +- .../base/trigger/FingerprintTriggerKey.kt | 1 - .../base/trigger/FloatingButtonKey.kt | 1 - .../base/trigger/InputEventTriggerKey.kt | 14 - .../base/trigger/KeyCodeTriggerKey.kt | 52 ++ .../base/trigger/KeyEventTriggerKey.kt | 13 +- .../base/trigger/RecordTriggerController.kt | 4 +- .../sds100/keymapper/base/trigger/Trigger.kt | 10 +- .../base/trigger/TriggerErrorSnapshot.kt | 4 +- .../keymapper/base/trigger/TriggerKey.kt | 6 - .../base/trigger/TriggerKeyListItem.kt | 5 +- .../trigger/TriggerKeyOptionsBottomSheet.kt | 11 + ...InputEventStrings.kt => KeyCodeStrings.kt} | 11 +- .../keymapper/base/utils/ScancodeStrings.kt | 579 ++++++++++++++++ base/src/main/res/values/strings.xml | 2 + .../keymapper/base/ConfigKeyMapUseCaseTest.kt | 4 +- .../keymapper/base/trigger/TriggerKeyTest.kt | 70 ++ .../common/utils/InputDeviceUtils.kt | 45 +- .../data/entities/EvdevTriggerKeyEntity.kt | 1 + .../data/entities/KeyEventTriggerKeyEntity.kt | 1 + .../{InputEventUtils.kt => KeyEventUtils.kt} | 40 +- .../keymapper/system/inputevents/Scancode.kt | 635 ++++++++++++++++++ 33 files changed, 1465 insertions(+), 183 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt rename base/src/main/java/io/github/sds100/keymapper/base/utils/{InputEventStrings.kt => KeyCodeStrings.kt} (97%) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt create mode 100644 base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt rename system/src/main/java/io/github/sds100/keymapper/system/inputevents/{InputEventUtils.kt => KeyEventUtils.kt} (93%) create mode 100644 system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt index 2bbd7c69a3..47ec8de250 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt @@ -7,7 +7,7 @@ import androidx.compose.material.icons.outlined.Android import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.utils.DndModeStrings -import io.github.sds100.keymapper.base.utils.InputEventStrings +import io.github.sds100.keymapper.base.utils.KeyCodeStrings import io.github.sds100.keymapper.base.utils.RingerModeStrings import io.github.sds100.keymapper.base.utils.VolumeStreamStrings import io.github.sds100.keymapper.base.utils.ui.IconInfo @@ -50,7 +50,7 @@ class ActionUiHelper( getString(R.string.description_keyevent_through_shell, keyCodeString) } else { val metaStateString = buildString { - for (label in InputEventStrings.MODIFIER_LABELS.entries) { + for (label in KeyCodeStrings.MODIFIER_LABELS.entries) { val modifier = label.key val labelRes = label.value diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index bc3c518b7f..a0adcbf0de 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -46,7 +46,7 @@ import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.files.FileAdapter import io.github.sds100.keymapper.system.files.FileUtils -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.intents.IntentAdapter import io.github.sds100.keymapper.system.intents.IntentTarget @@ -146,8 +146,8 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( // 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 + KeyEventUtils.isDpadKeyCode(action.keyCode) -> InputDevice.SOURCE_DPAD + KeyEventUtils.isGamepadButton(action.keyCode) -> InputDevice.SOURCE_GAMEPAD else -> InputDevice.SOURCE_KEYBOARD } @@ -892,7 +892,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( if (action.device?.descriptor == null) { // automatically select a game controller as the input device for game controller key events - if (InputEventUtils.isGamepadKeyCode(action.keyCode)) { + if (KeyEventUtils.isGamepadKeyCode(action.keyCode)) { devicesAdapter.connectedInputDevices.value.ifIsData { inputDevices -> val device = inputDevices.find { it.isGameController } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ChooseKeyCodeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ChooseKeyCodeViewModel.kt index 67987c742a..0502fc93ca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ChooseKeyCodeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ChooseKeyCodeViewModel.kt @@ -8,7 +8,7 @@ import io.github.sds100.keymapper.base.utils.filterByQuery import io.github.sds100.keymapper.base.utils.ui.DefaultSimpleListItem import io.github.sds100.keymapper.base.utils.ui.SimpleListItemOld import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -31,7 +31,7 @@ class ChooseKeyCodeViewModel @Inject constructor() : ViewModel() { private val allListItems = flow { withContext(Dispatchers.Default) { - InputEventUtils.getKeyCodes().sorted().map { keyCode -> + KeyEventUtils.getKeyCodes().sorted().map { keyCode -> DefaultSimpleListItem( id = keyCode.toString(), title = "$keyCode \t\t ${KeyEvent.keyCodeToString(keyCode)}", diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt index 283c3d8059..a036b06f54 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.ActionData -import io.github.sds100.keymapper.base.utils.InputEventStrings +import io.github.sds100.keymapper.base.utils.KeyCodeStrings import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider @@ -190,7 +190,7 @@ class ConfigKeyEventActionViewModel @Inject constructor( onError = { "" }, ) - val modifierListItems = InputEventStrings.MODIFIER_LABELS.map { (modifier, label) -> + val modifierListItems = KeyCodeStrings.MODIFIER_LABELS.map { (modifier, label) -> CheckBoxListItem( id = modifier.toString(), label = getString(label), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt index 0b9a757e0b..60ca8f1574 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt @@ -36,7 +36,7 @@ import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -376,7 +376,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( var consumeKeyEvent = true // Issue #753 - if (InputEventUtils.isModifierKey(keyCode)) { + if (KeyEventUtils.isModifierKey(keyCode)) { consumeKeyEvent = false } @@ -618,10 +618,18 @@ class ConfigKeyMapUseCaseController @Inject constructor( override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) { editTriggerKey(keyUid) { key -> - if (key is KeyEventTriggerKey) { - key.copy(consumeEvent = consumeKeyEvent) - } else { - key + when (key) { + is KeyEventTriggerKey -> { + key.copy(consumeEvent = consumeKeyEvent) + } + + is EvdevTriggerKey -> { + key.copy(consumeEvent = consumeKeyEvent) + } + + else -> { + key + } } } } @@ -850,10 +858,10 @@ class ConfigKeyMapUseCaseController @Inject constructor( } else { trigger.keys .mapNotNull { it as? KeyEventTriggerKey } - .any { InputEventUtils.isDpadKeyCode(it.keyCode) } + .any { KeyEventUtils.isDpadKeyCode(it.keyCode) } } - if (InputEventUtils.isModifierKey(data.keyCode) || containsDpadKey) { + if (KeyEventUtils.isModifierKey(data.keyCode) || containsDpadKey) { holdDown = true repeat = false } else { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt index db04aec4b3..572f70bcd9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt @@ -21,7 +21,7 @@ import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.base.trigger.TriggerMode -import io.github.sds100.keymapper.base.utils.InputEventStrings +import io.github.sds100.keymapper.base.trigger.getCodeLabel import io.github.sds100.keymapper.base.utils.isFixable import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.compose.ComposeChipModel @@ -256,7 +256,7 @@ class KeyMapListItemCreator( else -> Unit } - append(InputEventStrings.keyCodeToString(key.keyCode)) + append(key.getCodeLabel(this@KeyMapListItemCreator)) val deviceName = when (key.device) { is KeyEventTriggerDevice.Internal -> null @@ -303,7 +303,7 @@ class KeyMapListItemCreator( else -> Unit } - append(InputEventStrings.keyCodeToString(key.keyCode)) + append(key.getCodeLabel(this@KeyMapListItemCreator)) val parts = buildList { add("PRO") diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt index 95fb74e0cd..d03e31b301 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt @@ -3,9 +3,9 @@ package io.github.sds100.keymapper.base.keymaps.detection import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils /** * See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad @@ -35,7 +35,7 @@ class DpadMotionEventTracker { fun onKeyEvent(event: KMKeyEvent): Boolean { val device = event.device ?: return false - if (!InputEventUtils.isDpadKeyCode(event.keyCode)) { + if (!KeyEventUtils.isDpadKeyCode(event.keyCode)) { return false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt index 75b04c5b43..ca5616c05e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt @@ -18,7 +18,7 @@ import io.github.sds100.keymapper.base.trigger.AssistantTriggerType import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.InputEventTriggerKey +import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger @@ -28,11 +28,11 @@ import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.PreferenceDefaults -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -76,7 +76,7 @@ class KeyMapAlgorithm( /** * All sequence events that have the long press click type. */ - private var longPressSequenceTriggerKeys: Array = arrayOf() + private var longPressSequenceTriggerKeys: Array = arrayOf() /** * All double press keys and the index of their corresponding trigger. first is the event and second is @@ -271,7 +271,7 @@ class KeyMapAlgorithm( } else { detectKeyMaps = true - val longPressSequenceTriggerKeys = mutableListOf() + val longPressSequenceTriggerKeys = mutableListOf() val doublePressKeys = mutableListOf() @@ -306,7 +306,7 @@ class KeyMapAlgorithm( if (keyMap.trigger.mode == TriggerMode.Sequence && key.clickType == ClickType.LONG_PRESS && - key is InputEventTriggerKey + key is KeyCodeTriggerKey ) { if (keyMap.trigger.keys.size > 1) { longPressSequenceTriggerKeys.add(key) @@ -520,7 +520,7 @@ class KeyMapAlgorithm( val trigger = triggers[triggerIndex] for ((keyIndex, key) in trigger.keys.withIndex()) { - if (key is InputEventTriggerKey && isModifierKey(key.keyCode)) { + if (key is KeyCodeTriggerKey && isModifierKey(key.keyCode)) { parallelTriggerModifierKeyIndices.add(triggerIndex to keyIndex) } } @@ -619,13 +619,13 @@ class KeyMapAlgorithm( for ((triggerIndex, eventIndex) in parallelTriggerModifierKeyIndices) { val key = triggers[triggerIndex].keys[eventIndex] - if (key !is InputEventTriggerKey) { + if (key !is KeyCodeTriggerKey) { continue } if (parallelTriggerEventsAwaitingRelease[triggerIndex][eventIndex]) { metaStateFromKeyEvent = - metaStateFromKeyEvent.minusFlag(InputEventUtils.modifierKeycodeToMetaState(key.keyCode)) + metaStateFromKeyEvent.minusFlag(KeyEventUtils.modifierKeycodeToMetaState(key.keyCode)) } } @@ -750,7 +750,7 @@ class KeyMapAlgorithm( consumeEvent = true } - key is InputEventTriggerKey && event is KeyEventAlgo -> + key is KeyCodeTriggerKey && event is KeyEventAlgo -> if (key.keyCode == event.keyCode && key.consumeEvent) { consumeEvent = true } @@ -775,7 +775,7 @@ class KeyMapAlgorithm( val doublePressEvent = triggers[eventLocation.triggerIndex].keys[eventLocation.keyIndex] - triggers[triggerIndex].keys.forEachIndexed { eventIndex, event -> + for ((eventIndex, event) in triggers[triggerIndex].keys.withIndex()) { if (event == doublePressEvent && triggers[triggerIndex].keys[eventIndex].consumeEvent ) { @@ -895,7 +895,7 @@ class KeyMapAlgorithm( if (isModifierKey(actionKeyCode)) { val actionMetaState = - InputEventUtils.modifierKeycodeToMetaState(actionKeyCode) + KeyEventUtils.modifierKeycodeToMetaState(actionKeyCode) metaStateFromActions = metaStateFromActions.withFlag(actionMetaState) } @@ -1296,7 +1296,7 @@ class KeyMapAlgorithm( actionMap[actionKey]?.let { action -> if (action.data is ActionData.InputKeyEvent && isModifierKey(action.data.keyCode)) { val actionMetaState = - InputEventUtils.modifierKeycodeToMetaState(action.data.keyCode) + KeyEventUtils.modifierKeycodeToMetaState(action.data.keyCode) metaStateFromActionsToRemove = metaStateFromActionsToRemove.withFlag(actionMetaState) @@ -1847,6 +1847,17 @@ class KeyMapAlgorithm( private val AlgoEvent.withDoublePress: AlgoEvent get() = setClickType(clickType = ClickType.DOUBLE_PRESS) + private val TriggerKey.consumeEvent: Boolean + get() { + return when (this) { + is AssistantTriggerKey -> true + is EvdevTriggerKey -> consumeEvent + is FingerprintTriggerKey -> true + is FloatingButtonKey -> true + is KeyEventTriggerKey -> consumeEvent + } + } + /** * Represents the kind of event a trigger key is expecting to happen. */ diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt index 9f3a4584fa..532ada958b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt @@ -6,7 +6,7 @@ import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.actions.RepeatMode import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.data.PreferenceDefaults -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -112,7 +112,7 @@ class ParallelTriggerActionPerformer( continue } - if (action.data is ActionData.InputKeyEvent && InputEventUtils.isModifierKey(action.data.keyCode)) { + if (action.data is ActionData.InputKeyEvent && KeyEventUtils.isModifierKey(action.data.keyCode)) { continue } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/AssistantTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/AssistantTriggerKey.kt index 0acb6cde87..a3eba1b2a7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/AssistantTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/AssistantTriggerKey.kt @@ -18,10 +18,6 @@ data class AssistantTriggerKey( override val clickType: ClickType, ) : TriggerKey() { - // This is always true for an assistant key event because Key Mapper can't forward the - // assistant event to another app (or can it??). - override val consumeEvent: Boolean = true - override val allowedLongPress: Boolean = false override val allowedDoublePress: Boolean = false diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 0c5eefcd10..3251149d22 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -23,7 +23,6 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.ProductId import io.github.sds100.keymapper.base.purchasing.PurchasingManager import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.utils.InputEventStrings import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.DialogModel @@ -43,7 +42,6 @@ import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.ifIsData import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.common.utils.onSuccess -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -178,6 +176,7 @@ abstract class BaseConfigTriggerViewModel( * when no buttons are recorded is not shown. */ private var isRecordingCompletionUserInitiated: Boolean = false + private val midDot = getString(R.string.middot) init { val showTapTargetsPairFlow: Flow> = combine( @@ -715,7 +714,7 @@ abstract class BaseConfigTriggerViewModel( id = key.uid, keyName = getTriggerKeyName(key), clickType = clickType, - extraInfo = getTriggerKeyExtraInfo( + extraInfo = getKeyEventTriggerKeyExtraInfo( key, showDeviceDescriptors, ).takeIf { it.isNotBlank() }, @@ -744,14 +743,9 @@ abstract class BaseConfigTriggerViewModel( is EvdevTriggerKey -> TriggerKeyListItemModel.EvdevEvent( id = key.uid, - keyName = InputEventStrings.keyCodeToString(key.keyCode), - deviceName = key.device.name, + keyName = key.getCodeLabel(this), clickType = clickType, - extraInfo = if (!key.consumeEvent) { - getString(R.string.flag_dont_override_default_action) - } else { - null - }, + extraInfo = getEvdevTriggerKeyExtraInfo(key), linkType = linkType, error = error, ) @@ -759,13 +753,22 @@ abstract class BaseConfigTriggerViewModel( } } - private fun getTriggerKeyExtraInfo( + private fun getEvdevTriggerKeyExtraInfo(key: EvdevTriggerKey): String { + return buildString { + append(key.device.name) + + if (!key.consumeEvent) { + append(" $midDot ${getString(R.string.flag_dont_override_default_action)}") + } + } + } + + private fun getKeyEventTriggerKeyExtraInfo( key: KeyEventTriggerKey, showDeviceDescriptors: Boolean, ): String { return buildString { append(getTriggerKeyDeviceName(key.device, showDeviceDescriptors)) - val midDot = getString(R.string.middot) if (!key.consumeEvent) { append(" $midDot ${getString(R.string.flag_dont_override_default_action)}") @@ -775,10 +778,9 @@ abstract class BaseConfigTriggerViewModel( private fun getTriggerKeyName(key: KeyEventTriggerKey): String { return buildString { - append(InputEventStrings.keyCodeToString(key.keyCode)) + append(key.getCodeLabel(this@BaseConfigTriggerViewModel)) if (key.requiresIme) { - val midDot = getString(R.string.middot) append(" $midDot ${getString(R.string.flag_detect_from_input_method)}") } } @@ -878,7 +880,6 @@ sealed class TriggerKeyListItemModel { override val id: String, override val linkType: LinkType, val keyName: String, - val deviceName: String, override val clickType: ClickType, val extraInfo: String?, override val error: TriggerError?, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt index 5f52b1fec6..c0c3013003 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt @@ -20,7 +20,8 @@ data class EvdevTriggerKey( val device: EvdevDeviceInfo, override val clickType: ClickType = ClickType.SHORT_PRESS, override val consumeEvent: Boolean = true, -) : TriggerKey(), InputEventTriggerKey { + override val detectWithScanCodeUserSetting: Boolean = false +) : TriggerKey(), KeyCodeTriggerKey { override val allowedDoublePress: Boolean = true override val allowedLongPress: Boolean = true @@ -36,6 +37,9 @@ data class EvdevTriggerKey( val consumeEvent = !entity.flags.hasFlag(EvdevTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + val detectWithScancode = + entity.flags.hasFlag(EvdevTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + return EvdevTriggerKey( uid = entity.uid, keyCode = entity.keyCode, @@ -48,6 +52,7 @@ data class EvdevTriggerKey( ), clickType = clickType, consumeEvent = consumeEvent, + detectWithScanCodeUserSetting = detectWithScancode ) } @@ -64,6 +69,10 @@ data class EvdevTriggerKey( flags = flags.withFlag(EvdevTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) } + if (key.detectWithScanCodeUserSetting) { + flags = flags.withFlag(EvdevTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + } + return EvdevTriggerKeyEntity( keyCode = key.keyCode, scanCode = key.scanCode, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/FingerprintTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/FingerprintTriggerKey.kt index 820d11df43..cb233a0b2a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/FingerprintTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/FingerprintTriggerKey.kt @@ -17,7 +17,6 @@ data class FingerprintTriggerKey( override val clickType: ClickType, ) : TriggerKey() { - override val consumeEvent: Boolean = true override val allowedLongPress: Boolean = false override val allowedDoublePress: Boolean = false diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/FloatingButtonKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/FloatingButtonKey.kt index 65b51d395e..348d9fa489 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/FloatingButtonKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/FloatingButtonKey.kt @@ -17,7 +17,6 @@ data class FloatingButtonKey( override val clickType: ClickType, ) : TriggerKey() { - override val consumeEvent: Boolean = true override val allowedLongPress: Boolean = true override val allowedDoublePress: Boolean = true diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt deleted file mode 100644 index f3ff02dcf7..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/InputEventTriggerKey.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -import io.github.sds100.keymapper.base.keymaps.ClickType - -sealed interface InputEventTriggerKey { - val keyCode: Int - - /** - * Scancodes were only saved to KeyEvent trigger keys in version 4.0.0 so this is null - * to be backwards compatible. - */ - val scanCode: Int? - val clickType: ClickType -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt new file mode 100644 index 0000000000..bfda8b4bf5 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt @@ -0,0 +1,52 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.utils.KeyCodeStrings +import io.github.sds100.keymapper.base.utils.ScancodeStrings +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils + +sealed interface KeyCodeTriggerKey { + val keyCode: Int + + /** + * Scancodes were only saved to KeyEvent trigger keys in version 4.0.0 so this is null + * to be backwards compatible. + */ + val scanCode: Int? + val clickType: ClickType + + /** + * The user can specify they want to detect with the scancode instead of the key code. + */ + val detectWithScanCodeUserSetting: Boolean + + /** + * Whether the event that triggers this key will be consumed and not passed + * onto subsequent apps. E.g consuming the volume down key event will mean the volume + * doesn't change. + */ + val consumeEvent: Boolean +} + +fun KeyCodeTriggerKey.detectWithScancode(): Boolean { + return scanCode != null && (detectWithScanCodeUserSetting || isKeyCodeUnknown()) +} + +fun KeyCodeTriggerKey.isKeyCodeUnknown(): Boolean { + return KeyEventUtils.isKeyCodeUnknown(keyCode) +} + +/** + * Get the label for the key code or scan code, depending on whether to detect it with a scan code. + */ +fun KeyCodeTriggerKey.getCodeLabel(resourceProvider: ResourceProvider): String { + if (detectWithScancode() && scanCode != null) { + return ScancodeStrings.getScancodeLabel(scanCode!!) + ?: resourceProvider.getString(R.string.trigger_key_unknown_scan_code, scanCode!!) + } else { + return KeyCodeStrings.keyCodeToString(keyCode) + ?: resourceProvider.getString(R.string.trigger_key_unknown_key_code, keyCode) + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt index bf7b5b4635..0e7a4f3abf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -21,7 +21,8 @@ data class KeyEventTriggerKey( */ val requiresIme: Boolean = false, override val scanCode: Int? = null, -) : TriggerKey(), InputEventTriggerKey { + override val detectWithScanCodeUserSetting: Boolean = false +) : TriggerKey(), KeyCodeTriggerKey { override val allowedLongPress: Boolean = true override val allowedDoublePress: Boolean = true @@ -32,7 +33,7 @@ data class KeyEventTriggerKey( is KeyEventTriggerDevice.External -> "external" KeyEventTriggerDevice.Internal -> "internal" } - return "InputEventTriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeEvent) " + return "KeyCodeTriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeEvent) " } // key code -> click type -> device -> consume key event @@ -73,6 +74,9 @@ data class KeyEventTriggerKey( val requiresIme = entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) + val detectWithScancode = + entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + return KeyEventTriggerKey( uid = entity.uid, keyCode = entity.keyCode, @@ -81,6 +85,7 @@ data class KeyEventTriggerKey( consumeEvent = consumeEvent, requiresIme = requiresIme, scanCode = entity.scanCode, + detectWithScanCodeUserSetting = detectWithScancode ) } @@ -114,6 +119,10 @@ data class KeyEventTriggerKey( flags = flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) } + if (key.detectWithScanCodeUserSetting) { + flags = flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + } + return KeyEventTriggerKeyEntity( keyCode = key.keyCode, deviceId = deviceId, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 19f7a5d6c3..76550d7dd1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -10,11 +10,11 @@ import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.isError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -193,7 +193,7 @@ class RecordTriggerControllerImpl @Inject constructor( val keyEvent = dpadMotionEventTracker.convertMotionEvent(event).firstOrNull() ?: return false - if (!InputEventUtils.isDpadKeyCode(keyEvent.keyCode)) { + if (!KeyEventUtils.isDpadKeyCode(keyEvent.keyCode)) { return false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt index 7e6b661027..575d3ad175 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt @@ -14,7 +14,6 @@ import io.github.sds100.keymapper.data.entities.FloatingButtonKeyEntity import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity import io.github.sds100.keymapper.data.entities.TriggerEntity import io.github.sds100.keymapper.data.entities.getData -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import kotlinx.serialization.Serializable @Serializable @@ -48,13 +47,8 @@ data class Trigger( * anyway. */ fun isDetectingWhenScreenOffAllowed(): Boolean { - return keys.isNotEmpty() && - keys.all { - it is KeyEventTriggerKey && - InputEventUtils.canDetectKeyWhenScreenOff( - it.keyCode, - ) - } + // TODO triggers should always detect when screen is off if possible + return false } fun isChangingSequenceTriggerTimeoutAllowed(): Boolean = keys.isNotEmpty() && keys.size > 1 && mode is TriggerMode.Sequence diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt index 3ec72589d2..2f5a6f33eb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt @@ -9,7 +9,7 @@ import io.github.sds100.keymapper.base.purchasing.PurchasingError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils /** * Store the data required for determining trigger errors to reduce the number of calls with @@ -71,7 +71,7 @@ data class TriggerErrorSnapshot( val containsDpadKey = key is KeyEventTriggerKey && - InputEventUtils.isDpadKeyCode(key.keyCode) && key.requiresIme + KeyEventUtils.isDpadKeyCode(key.keyCode) && key.requiresIme if (showDpadImeSetupError && !isKeyMapperImeChosen && containsDpadKey) { return TriggerError.DPAD_IME_NOT_SELECTED diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt index 7ba6463ac2..e3681f1426 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt @@ -7,12 +7,6 @@ import kotlinx.serialization.Serializable sealed class TriggerKey : Comparable { abstract val clickType: ClickType - /** - * Whether the event that triggers this key will be consumed and not passed - * onto subsequent apps. E.g consuming the volume down key event will mean the volume - * doesn't change. - */ - abstract val consumeEvent: Boolean abstract val uid: String abstract val allowedLongPress: Boolean abstract val allowedDoublePress: Boolean diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index 79c3aef869..f8f68af9d1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -141,7 +141,7 @@ fun TriggerKeyListItem( ) is TriggerKeyListItemModel.KeyEvent -> model.keyName - is TriggerKeyListItemModel.EvdevEvent -> "${model.keyName} (${model.deviceName})" + is TriggerKeyListItemModel.EvdevEvent -> model.keyName is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource(R.string.trigger_error_floating_button_deleted_title) @@ -350,9 +350,8 @@ private fun EvdevEventPreview() { model = TriggerKeyListItemModel.EvdevEvent( id = "id", keyName = "Volume Up", - deviceName = "Gpio-keys", clickType = ClickType.SHORT_PRESS, - extraInfo = "Do not consume", + extraInfo = "Gpio-keys", linkType = LinkType.ARROW, error = null, ), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 4a53c2d2e3..83488caea2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -96,6 +96,8 @@ fun TriggerKeyOptionsBottomSheet( Spacer(modifier = Modifier.height(8.dp)) + // TODO use segmented button to switch between key code and scancode. + if (state is TriggerKeyOptionsState.KeyEvent) { CheckBoxText( modifier = Modifier.padding(8.dp), @@ -105,6 +107,15 @@ fun TriggerKeyOptionsBottomSheet( ) } + if (state is TriggerKeyOptionsState.EvdevEvent) { + CheckBoxText( + modifier = Modifier.padding(8.dp), + text = stringResource(R.string.flag_dont_override_default_action), + isChecked = state.doNotRemapChecked, + onCheckedChange = onCheckDoNotRemap, + ) + } + if (state.showClickTypes) { Text( modifier = Modifier.padding(horizontal = 16.dp), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt similarity index 97% rename from base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt rename to base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt index ea2bf1c8f0..d1e4f2cc52 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt @@ -2,9 +2,8 @@ package io.github.sds100.keymapper.base.utils import android.view.KeyEvent import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.system.inputevents.InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET -object InputEventStrings { +object KeyCodeStrings { val MODIFIER_LABELS = mapOf( KeyEvent.META_CTRL_ON to R.string.meta_state_ctrl, @@ -369,11 +368,7 @@ object InputEventStrings { * Create a text representation of a key event. E.g if the control key was pressed, * "Ctrl" will be returned */ - fun keyCodeToString(keyCode: Int): String = NON_CHARACTER_KEY_LABELS[keyCode].let { - if (keyCode >= KEYCODE_TO_SCANCODE_OFFSET || keyCode < 0) { - "scancode $keyCode" - } else { - it ?: "unknown keycode $keyCode" - } + fun keyCodeToString(keyCode: Int): String? { + return NON_CHARACTER_KEY_LABELS[keyCode] } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt new file mode 100644 index 0000000000..43885b2719 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt @@ -0,0 +1,579 @@ +package io.github.sds100.keymapper.base.utils + +import io.github.sds100.keymapper.system.inputevents.Scancode + +object ScancodeStrings { + private val SCANCODE_LABELS = mapOf( + // Number keys + Scancode.KEY_1 to "1", + Scancode.KEY_2 to "2", + Scancode.KEY_3 to "3", + Scancode.KEY_4 to "4", + Scancode.KEY_5 to "5", + Scancode.KEY_6 to "6", + Scancode.KEY_7 to "7", + Scancode.KEY_8 to "8", + Scancode.KEY_9 to "9", + Scancode.KEY_0 to "0", + + // Special characters + Scancode.KEY_MINUS to "-", + Scancode.KEY_EQUAL to "=", + Scancode.KEY_LEFTBRACE to "[", + Scancode.KEY_RIGHTBRACE to "]", + Scancode.KEY_SEMICOLON to ";", + Scancode.KEY_APOSTROPHE to "'", + Scancode.KEY_GRAVE to "`", + Scancode.KEY_BACKSLASH to "\\", + Scancode.KEY_COMMA to ",", + Scancode.KEY_DOT to ".", + Scancode.KEY_SLASH to "/", + + // Letter keys + Scancode.KEY_Q to "Q", + Scancode.KEY_W to "W", + Scancode.KEY_E to "E", + Scancode.KEY_R to "R", + Scancode.KEY_T to "T", + Scancode.KEY_Y to "Y", + Scancode.KEY_U to "U", + Scancode.KEY_I to "I", + Scancode.KEY_O to "O", + Scancode.KEY_P to "P", + Scancode.KEY_A to "A", + Scancode.KEY_S to "S", + Scancode.KEY_D to "D", + Scancode.KEY_F to "F", + Scancode.KEY_G to "G", + Scancode.KEY_H to "H", + Scancode.KEY_J to "J", + Scancode.KEY_K to "K", + Scancode.KEY_L to "L", + Scancode.KEY_Z to "Z", + Scancode.KEY_X to "X", + Scancode.KEY_C to "C", + Scancode.KEY_V to "V", + Scancode.KEY_B to "B", + Scancode.KEY_N to "N", + Scancode.KEY_M to "M", + + // Function keys + Scancode.KEY_F1 to "F1", + Scancode.KEY_F2 to "F2", + Scancode.KEY_F3 to "F3", + Scancode.KEY_F4 to "F4", + Scancode.KEY_F5 to "F5", + Scancode.KEY_F6 to "F6", + Scancode.KEY_F7 to "F7", + Scancode.KEY_F8 to "F8", + Scancode.KEY_F9 to "F9", + Scancode.KEY_F10 to "F10", + Scancode.KEY_F11 to "F11", + Scancode.KEY_F12 to "F12", + Scancode.KEY_F13 to "F13", + Scancode.KEY_F14 to "F14", + Scancode.KEY_F15 to "F15", + Scancode.KEY_F16 to "F16", + Scancode.KEY_F17 to "F17", + Scancode.KEY_F18 to "F18", + Scancode.KEY_F19 to "F19", + Scancode.KEY_F20 to "F20", + Scancode.KEY_F21 to "F21", + Scancode.KEY_F22 to "F22", + Scancode.KEY_F23 to "F23", + Scancode.KEY_F24 to "F24", + + // Control keys + Scancode.KEY_BACKSPACE to "Backspace", + Scancode.KEY_TAB to "Tab", + Scancode.KEY_ENTER to "Enter", + Scancode.KEY_LEFTCTRL to "Left Ctrl", + Scancode.KEY_RIGHTCTRL to "Right Ctrl", + Scancode.KEY_LEFTSHIFT to "Left Shift", + Scancode.KEY_RIGHTSHIFT to "Right Shift", + Scancode.KEY_LEFTALT to "Left Alt", + Scancode.KEY_RIGHTALT to "Right Alt", + Scancode.KEY_SPACE to "Space", + Scancode.KEY_CAPSLOCK to "Caps Lock", + Scancode.KEY_NUMLOCK to "Num Lock", + Scancode.KEY_SCROLLLOCK to "Scroll Lock", + Scancode.KEY_LEFTMETA to "Left Meta", + Scancode.KEY_RIGHTMETA to "Right Meta", + Scancode.KEY_COMPOSE to "Compose", + + // Navigation keys + Scancode.KEY_HOME to "Home", + Scancode.KEY_END to "End", + Scancode.KEY_UP to "Up", + Scancode.KEY_DOWN to "Down", + Scancode.KEY_LEFT to "Left", + Scancode.KEY_RIGHT to "Right", + Scancode.KEY_PAGEUP to "Page Up", + Scancode.KEY_PAGEDOWN to "Page Down", + Scancode.KEY_INSERT to "Insert", + Scancode.KEY_DELETE to "Delete", + + // Keypad keys + Scancode.KEY_KP0 to "Keypad 0", + Scancode.KEY_KP1 to "Keypad 1", + Scancode.KEY_KP2 to "Keypad 2", + Scancode.KEY_KP3 to "Keypad 3", + Scancode.KEY_KP4 to "Keypad 4", + Scancode.KEY_KP5 to "Keypad 5", + Scancode.KEY_KP6 to "Keypad 6", + Scancode.KEY_KP7 to "Keypad 7", + Scancode.KEY_KP8 to "Keypad 8", + Scancode.KEY_KP9 to "Keypad 9", + Scancode.KEY_KPDOT to "Keypad .", + Scancode.KEY_KPPLUS to "Keypad +", + Scancode.KEY_KPMINUS to "Keypad -", + Scancode.KEY_KPASTERISK to "Keypad *", + Scancode.KEY_KPSLASH to "Keypad /", + Scancode.KEY_KPENTER to "Keypad Enter", + Scancode.KEY_KPEQUAL to "Keypad =", + Scancode.KEY_KPPLUSMINUS to "Keypad +/-", + Scancode.KEY_KPCOMMA to "Keypad ,", + Scancode.KEY_KPLEFTPAREN to "Keypad (", + Scancode.KEY_KPRIGHTPAREN to "Keypad )", + Scancode.KEY_KPJPCOMMA to "Keypad JP Comma", + + // Media keys + Scancode.KEY_MUTE to "Mute", + Scancode.KEY_VOLUMEDOWN to "Volume Down", + Scancode.KEY_VOLUMEUP to "Volume Up", + Scancode.KEY_NEXTSONG to "Next Song", + Scancode.KEY_PREVIOUSSONG to "Previous Song", + Scancode.KEY_PLAYPAUSE to "Play/Pause", + Scancode.KEY_STOPCD to "Stop", + Scancode.KEY_RECORD to "Record", + Scancode.KEY_REWIND to "Rewind", + Scancode.KEY_FASTFORWARD to "Fast Forward", + Scancode.KEY_EJECTCD to "Eject", + Scancode.KEY_CLOSECD to "Close CD", + Scancode.KEY_EJECTCLOSECD to "Eject/Close CD", + Scancode.KEY_PLAYCD to "Play CD", + Scancode.KEY_PAUSECD to "Pause CD", + Scancode.KEY_PLAY to "Play", + + // System keys + Scancode.KEY_POWER to "Power", + Scancode.KEY_SLEEP to "Sleep", + Scancode.KEY_WAKEUP to "Wake Up", + Scancode.KEY_SUSPEND to "Suspend", + Scancode.KEY_PAUSE to "Pause", + Scancode.KEY_SYSRQ to "SysRq", + Scancode.KEY_LINEFEED to "Line Feed", + Scancode.KEY_MACRO to "Macro", + Scancode.KEY_SCALE to "Scale", + + // Brightness keys + Scancode.KEY_BRIGHTNESSDOWN to "Brightness Down", + Scancode.KEY_BRIGHTNESSUP to "Brightness Up", + Scancode.KEY_BRIGHTNESS_CYCLE to "Brightness Cycle", + Scancode.KEY_BRIGHTNESS_AUTO to "Brightness Auto", + Scancode.KEY_BRIGHTNESS_MIN to "Brightness Min", + Scancode.KEY_BRIGHTNESS_MAX to "Brightness Max", + Scancode.KEY_DISPLAY_OFF to "Display Off", + Scancode.KEY_SWITCHVIDEOMODE to "Switch Video Mode", + Scancode.KEY_KBDILLUMTOGGLE to "Keyboard Illumination Toggle", + Scancode.KEY_KBDILLUMDOWN to "Keyboard Illumination Down", + Scancode.KEY_KBDILLUMUP to "Keyboard Illumination Up", + + // International keys + Scancode.KEY_ZENKAKUHANKAKU to "Zenkaku/Hankaku", + Scancode.KEY_102ND to "102nd Key", + Scancode.KEY_RO to "Ro", + Scancode.KEY_KATAKANA to "Katakana", + Scancode.KEY_HIRAGANA to "Hiragana", + Scancode.KEY_HENKAN to "Henkan", + Scancode.KEY_KATAKANAHIRAGANA to "Katakana/Hiragana", + Scancode.KEY_MUHENKAN to "Muhenkan", + Scancode.KEY_HANGEUL to "Hangeul", + Scancode.KEY_HANJA to "Hanja", + Scancode.KEY_YEN to "Yen", + + // Application keys + Scancode.KEY_STOP to "Stop", + Scancode.KEY_AGAIN to "Again", + Scancode.KEY_PROPS to "Properties", + Scancode.KEY_UNDO to "Undo", + Scancode.KEY_FRONT to "Front", + Scancode.KEY_COPY to "Copy", + Scancode.KEY_OPEN to "Open", + Scancode.KEY_PASTE to "Paste", + Scancode.KEY_FIND to "Find", + Scancode.KEY_CUT to "Cut", + Scancode.KEY_HELP to "Help", + Scancode.KEY_MENU to "Menu", + Scancode.KEY_CALC to "Calculator", + Scancode.KEY_SETUP to "Setup", + Scancode.KEY_FILE to "File", + Scancode.KEY_SENDFILE to "Send File", + Scancode.KEY_DELETEFILE to "Delete File", + Scancode.KEY_XFER to "Transfer", + Scancode.KEY_PROG1 to "Program 1", + Scancode.KEY_PROG2 to "Program 2", + Scancode.KEY_PROG3 to "Program 3", + Scancode.KEY_PROG4 to "Program 4", + + // Web/Internet keys + Scancode.KEY_WWW to "WWW", + Scancode.KEY_MSDOS to "MS-DOS", + Scancode.KEY_COFFEE to "Coffee", + Scancode.KEY_ROTATE_DISPLAY to "Rotate Display", + Scancode.KEY_CYCLEWINDOWS to "Cycle Windows", + Scancode.KEY_MAIL to "Mail", + Scancode.KEY_BOOKMARKS to "Bookmarks", + Scancode.KEY_COMPUTER to "Computer", + Scancode.KEY_BACK to "Back", + Scancode.KEY_FORWARD to "Forward", + Scancode.KEY_HOMEPAGE to "Homepage", + Scancode.KEY_REFRESH to "Refresh", + Scancode.KEY_EXIT to "Exit", + Scancode.KEY_MOVE to "Move", + Scancode.KEY_EDIT to "Edit", + Scancode.KEY_SCROLLUP to "Scroll Up", + Scancode.KEY_SCROLLDOWN to "Scroll Down", + Scancode.KEY_NEW to "New", + Scancode.KEY_REDO to "Redo", + + // Multimedia keys + Scancode.KEY_BASSBOOST to "Bass Boost", + Scancode.KEY_PRINT to "Print", + Scancode.KEY_HP to "HP", + Scancode.KEY_CAMERA to "Camera", + Scancode.KEY_SOUND to "Sound", + Scancode.KEY_QUESTION to "Question", + Scancode.KEY_EMAIL to "Email", + Scancode.KEY_CHAT to "Chat", + Scancode.KEY_SEARCH to "Search", + Scancode.KEY_CONNECT to "Connect", + Scancode.KEY_FINANCE to "Finance", + Scancode.KEY_SPORT to "Sport", + Scancode.KEY_SHOP to "Shop", + Scancode.KEY_ALTERASE to "Alt Erase", + Scancode.KEY_CANCEL to "Cancel", + Scancode.KEY_MEDIA to "Media", + + // Wireless keys + Scancode.KEY_BLUETOOTH to "Bluetooth", + Scancode.KEY_WLAN to "WLAN", + Scancode.KEY_UWB to "UWB", + Scancode.KEY_UNKNOWN to "Unknown", + Scancode.KEY_WWAN to "WWAN", + Scancode.KEY_RFKILL to "RF Kill", + Scancode.KEY_MICMUTE to "Mic Mute", + + // Video keys + Scancode.KEY_VIDEO_NEXT to "Video Next", + Scancode.KEY_VIDEO_PREV to "Video Previous", + + // Battery and document keys + Scancode.KEY_BATTERY to "Battery", + Scancode.KEY_DOCUMENTS to "Documents", + Scancode.KEY_SEND to "Send", + Scancode.KEY_REPLY to "Reply", + Scancode.KEY_FORWARDMAIL to "Forward Mail", + Scancode.KEY_SAVE to "Save", + + // Mouse buttons + Scancode.BTN_LEFT to "Left Mouse Button", + Scancode.BTN_RIGHT to "Right Mouse Button", + Scancode.BTN_MIDDLE to "Middle Mouse Button", + Scancode.BTN_SIDE to "Side Mouse Button", + Scancode.BTN_EXTRA to "Extra Mouse Button", + Scancode.BTN_FORWARD to "Forward Mouse Button", + Scancode.BTN_BACK to "Back Mouse Button", + Scancode.BTN_TASK to "Task Mouse Button", + + // Generic buttons + Scancode.BTN_0 to "Button 0", + Scancode.BTN_1 to "Button 1", + Scancode.BTN_2 to "Button 2", + Scancode.BTN_3 to "Button 3", + Scancode.BTN_4 to "Button 4", + Scancode.BTN_5 to "Button 5", + Scancode.BTN_6 to "Button 6", + Scancode.BTN_7 to "Button 7", + Scancode.BTN_8 to "Button 8", + Scancode.BTN_9 to "Button 9", + + // Joystick buttons + Scancode.BTN_TRIGGER to "Trigger", + Scancode.BTN_THUMB to "Thumb", + Scancode.BTN_THUMB2 to "Thumb 2", + Scancode.BTN_TOP to "Top", + Scancode.BTN_TOP2 to "Top 2", + Scancode.BTN_PINKIE to "Pinkie", + Scancode.BTN_BASE to "Base", + Scancode.BTN_BASE2 to "Base 2", + Scancode.BTN_BASE3 to "Base 3", + Scancode.BTN_BASE4 to "Base 4", + Scancode.BTN_BASE5 to "Base 5", + Scancode.BTN_BASE6 to "Base 6", + Scancode.BTN_DEAD to "Dead", + + // Gamepad buttons + Scancode.BTN_SOUTH to "South Button", + Scancode.BTN_EAST to "East Button", + Scancode.BTN_C to "C Button", + Scancode.BTN_NORTH to "North Button", + Scancode.BTN_WEST to "West Button", + Scancode.BTN_Z to "Z Button", + Scancode.BTN_TL to "Top Left", + Scancode.BTN_TR to "Top Right", + Scancode.BTN_TL2 to "Top Left 2", + Scancode.BTN_TR2 to "Top Right 2", + Scancode.BTN_SELECT to "Select", + Scancode.BTN_START to "Start", + Scancode.BTN_MODE to "Mode", + Scancode.BTN_THUMBL to "Left Thumb", + Scancode.BTN_THUMBR to "Right Thumb", + + // Digital pen buttons + Scancode.BTN_TOOL_PEN to "Pen Tool", + Scancode.BTN_TOOL_RUBBER to "Rubber Tool", + Scancode.BTN_TOOL_BRUSH to "Brush Tool", + Scancode.BTN_TOOL_PENCIL to "Pencil Tool", + Scancode.BTN_TOOL_AIRBRUSH to "Airbrush Tool", + Scancode.BTN_TOOL_FINGER to "Finger Tool", + Scancode.BTN_TOOL_MOUSE to "Mouse Tool", + Scancode.BTN_TOOL_LENS to "Lens Tool", + Scancode.BTN_TOOL_QUINTTAP to "Quint Tap Tool", + Scancode.BTN_STYLUS3 to "Stylus 3", + Scancode.BTN_TOUCH to "Touch", + Scancode.BTN_STYLUS to "Stylus", + Scancode.BTN_STYLUS2 to "Stylus 2", + Scancode.BTN_TOOL_DOUBLETAP to "Double Tap Tool", + Scancode.BTN_TOOL_TRIPLETAP to "Triple Tap Tool", + Scancode.BTN_TOOL_QUADTAP to "Quad Tap Tool", + + // Wheel buttons + Scancode.BTN_GEAR_DOWN to "Gear Down", + Scancode.BTN_GEAR_UP to "Gear Up", + + // Remote control keys + Scancode.KEY_OK to "OK", + Scancode.KEY_SELECT to "Select", + Scancode.KEY_GOTO to "Goto", + Scancode.KEY_CLEAR to "Clear", + Scancode.KEY_POWER2 to "Power 2", + Scancode.KEY_OPTION to "Option", + Scancode.KEY_INFO to "Info", + Scancode.KEY_TIME to "Time", + Scancode.KEY_VENDOR to "Vendor", + Scancode.KEY_ARCHIVE to "Archive", + Scancode.KEY_PROGRAM to "Program", + Scancode.KEY_CHANNEL to "Channel", + Scancode.KEY_FAVORITES to "Favorites", + Scancode.KEY_EPG to "EPG", + Scancode.KEY_PVR to "PVR", + Scancode.KEY_MHP to "MHP", + Scancode.KEY_LANGUAGE to "Language", + Scancode.KEY_TITLE to "Title", + Scancode.KEY_SUBTITLE to "Subtitle", + Scancode.KEY_ANGLE to "Angle", + Scancode.KEY_ZOOM to "Zoom", + Scancode.KEY_MODE to "Mode", + Scancode.KEY_KEYBOARD to "Keyboard", + Scancode.KEY_SCREEN to "Screen", + Scancode.KEY_PC to "PC", + Scancode.KEY_TV to "TV", + Scancode.KEY_TV2 to "TV 2", + Scancode.KEY_VCR to "VCR", + Scancode.KEY_VCR2 to "VCR 2", + Scancode.KEY_SAT to "Satellite", + Scancode.KEY_SAT2 to "Satellite 2", + Scancode.KEY_CD to "CD", + Scancode.KEY_TAPE to "Tape", + Scancode.KEY_RADIO to "Radio", + Scancode.KEY_TUNER to "Tuner", + Scancode.KEY_PLAYER to "Player", + Scancode.KEY_TEXT to "Text", + Scancode.KEY_DVD to "DVD", + Scancode.KEY_AUX to "Aux", + Scancode.KEY_MP3 to "MP3", + Scancode.KEY_AUDIO to "Audio", + Scancode.KEY_VIDEO to "Video", + Scancode.KEY_DIRECTORY to "Directory", + Scancode.KEY_LIST to "List", + Scancode.KEY_MEMO to "Memo", + Scancode.KEY_CALENDAR to "Calendar", + Scancode.KEY_RED to "Red", + Scancode.KEY_GREEN to "Green", + Scancode.KEY_YELLOW to "Yellow", + Scancode.KEY_BLUE to "Blue", + Scancode.KEY_CHANNELUP to "Channel Up", + Scancode.KEY_CHANNELDOWN to "Channel Down", + Scancode.KEY_FIRST to "First", + Scancode.KEY_LAST to "Last", + Scancode.KEY_AB to "A-B", + Scancode.KEY_NEXT to "Next", + Scancode.KEY_RESTART to "Restart", + Scancode.KEY_SLOW to "Slow", + Scancode.KEY_SHUFFLE to "Shuffle", + Scancode.KEY_BREAK to "Break", + Scancode.KEY_PREVIOUS to "Previous", + Scancode.KEY_DIGITS to "Digits", + Scancode.KEY_TEEN to "Teen", + Scancode.KEY_TWEN to "Twen", + + // Phone keys + Scancode.KEY_PHONE to "Phone", + Scancode.KEY_VIDEOPHONE to "Video Phone", + Scancode.KEY_PICKUP_PHONE to "Pick Up Phone", + Scancode.KEY_HANGUP_PHONE to "Hang Up Phone", + + // Application keys + Scancode.KEY_GAMES to "Games", + Scancode.KEY_ZOOMIN to "Zoom In", + Scancode.KEY_ZOOMOUT to "Zoom Out", + Scancode.KEY_ZOOMRESET to "Zoom Reset", + Scancode.KEY_WORDPROCESSOR to "Word Processor", + Scancode.KEY_EDITOR to "Editor", + Scancode.KEY_SPREADSHEET to "Spreadsheet", + Scancode.KEY_GRAPHICSEDITOR to "Graphics Editor", + Scancode.KEY_PRESENTATION to "Presentation", + Scancode.KEY_DATABASE to "Database", + Scancode.KEY_NEWS to "News", + Scancode.KEY_VOICEMAIL to "Voicemail", + Scancode.KEY_ADDRESSBOOK to "Address Book", + Scancode.KEY_MESSENGER to "Messenger", + Scancode.KEY_DISPLAYTOGGLE to "Display Toggle", + Scancode.KEY_SPELLCHECK to "Spell Check", + Scancode.KEY_LOGOFF to "Log Off", + + // Currency keys + Scancode.KEY_DOLLAR to "Dollar", + Scancode.KEY_EURO to "Euro", + + // Media control keys + Scancode.KEY_FRAMEBACK to "Frame Back", + Scancode.KEY_FRAMEFORWARD to "Frame Forward", + Scancode.KEY_CONTEXT_MENU to "Context Menu", + Scancode.KEY_MEDIA_REPEAT to "Media Repeat", + Scancode.KEY_10CHANNELSUP to "10 Channels Up", + Scancode.KEY_10CHANNELSDOWN to "10 Channels Down", + Scancode.KEY_IMAGES to "Images", + Scancode.KEY_NOTIFICATION_CENTER to "Notification Center", + + // Numeric keypad + Scancode.KEY_NUMERIC_0 to "Numeric 0", + Scancode.KEY_NUMERIC_1 to "Numeric 1", + Scancode.KEY_NUMERIC_2 to "Numeric 2", + Scancode.KEY_NUMERIC_3 to "Numeric 3", + Scancode.KEY_NUMERIC_4 to "Numeric 4", + Scancode.KEY_NUMERIC_5 to "Numeric 5", + Scancode.KEY_NUMERIC_6 to "Numeric 6", + Scancode.KEY_NUMERIC_7 to "Numeric 7", + Scancode.KEY_NUMERIC_8 to "Numeric 8", + Scancode.KEY_NUMERIC_9 to "Numeric 9", + Scancode.KEY_NUMERIC_STAR to "Numeric *", + Scancode.KEY_NUMERIC_POUND to "Numeric #", + Scancode.KEY_NUMERIC_A to "Numeric A", + Scancode.KEY_NUMERIC_B to "Numeric B", + Scancode.KEY_NUMERIC_C to "Numeric C", + Scancode.KEY_NUMERIC_D to "Numeric D", + Scancode.KEY_NUMERIC_11 to "Numeric 11", + Scancode.KEY_NUMERIC_12 to "Numeric 12", + + // System control keys + Scancode.KEY_BUTTONCONFIG to "Button Config", + Scancode.KEY_TASKMANAGER to "Task Manager", + Scancode.KEY_JOURNAL to "Journal", + Scancode.KEY_CONTROLPANEL to "Control Panel", + Scancode.KEY_APPSELECT to "App Select", + Scancode.KEY_SCREENSAVER to "Screen Saver", + Scancode.KEY_VOICECOMMAND to "Voice Command", + Scancode.KEY_ASSISTANT to "Assistant", + Scancode.KEY_KBD_LAYOUT_NEXT to "Keyboard Layout Next", + Scancode.KEY_EMOJI_PICKER to "Emoji Picker", + Scancode.KEY_DICTATE to "Dictate", + + // D-pad buttons + Scancode.BTN_DPAD_UP to "D-pad Up", + Scancode.BTN_DPAD_DOWN to "D-pad Down", + Scancode.BTN_DPAD_LEFT to "D-pad Left", + Scancode.BTN_DPAD_RIGHT to "D-pad Right", + + // Text editing keys + Scancode.KEY_DEL_EOL to "Delete End of Line", + Scancode.KEY_DEL_EOS to "Delete End of Screen", + Scancode.KEY_INS_LINE to "Insert Line", + Scancode.KEY_DEL_LINE to "Delete Line", + + // Function modifier keys + Scancode.KEY_FN to "Fn", + Scancode.KEY_FN_ESC to "Fn+Esc", + Scancode.KEY_FN_F1 to "Fn+F1", + Scancode.KEY_FN_F2 to "Fn+F2", + Scancode.KEY_FN_F3 to "Fn+F3", + Scancode.KEY_FN_F4 to "Fn+F4", + Scancode.KEY_FN_F5 to "Fn+F5", + Scancode.KEY_FN_F6 to "Fn+F6", + Scancode.KEY_FN_F7 to "Fn+F7", + Scancode.KEY_FN_F8 to "Fn+F8", + Scancode.KEY_FN_F9 to "Fn+F9", + Scancode.KEY_FN_F10 to "Fn+F10", + Scancode.KEY_FN_F11 to "Fn+F11", + Scancode.KEY_FN_F12 to "Fn+F12", + Scancode.KEY_FN_1 to "Fn+1", + Scancode.KEY_FN_2 to "Fn+2", + Scancode.KEY_FN_D to "Fn+D", + Scancode.KEY_FN_E to "Fn+E", + Scancode.KEY_FN_F to "Fn+F", + Scancode.KEY_FN_S to "Fn+S", + Scancode.KEY_FN_B to "Fn+B", + Scancode.KEY_FN_RIGHT_SHIFT to "Fn+Right Shift", + + // Braille keys + Scancode.KEY_BRL_DOT1 to "Braille Dot 1", + Scancode.KEY_BRL_DOT2 to "Braille Dot 2", + Scancode.KEY_BRL_DOT3 to "Braille Dot 3", + Scancode.KEY_BRL_DOT4 to "Braille Dot 4", + Scancode.KEY_BRL_DOT5 to "Braille Dot 5", + Scancode.KEY_BRL_DOT6 to "Braille Dot 6", + Scancode.KEY_BRL_DOT7 to "Braille Dot 7", + Scancode.KEY_BRL_DOT8 to "Braille Dot 8", + Scancode.KEY_BRL_DOT9 to "Braille Dot 9", + Scancode.KEY_BRL_DOT10 to "Braille Dot 10", + + // Camera keys + Scancode.KEY_CAMERA_FOCUS to "Camera Focus", + Scancode.KEY_CAMERA_ZOOMIN to "Camera Zoom In", + Scancode.KEY_CAMERA_ZOOMOUT to "Camera Zoom Out", + Scancode.KEY_CAMERA_UP to "Camera Up", + Scancode.KEY_CAMERA_DOWN to "Camera Down", + Scancode.KEY_CAMERA_LEFT to "Camera Left", + Scancode.KEY_CAMERA_RIGHT to "Camera Right", + Scancode.KEY_CAMERA_ACCESS_ENABLE to "Camera Access Enable", + Scancode.KEY_CAMERA_ACCESS_DISABLE to "Camera Access Disable", + Scancode.KEY_CAMERA_ACCESS_TOGGLE to "Camera Access Toggle", + + // Wireless and touchpad keys + Scancode.KEY_WPS_BUTTON to "WPS Button", + Scancode.KEY_TOUCHPAD_TOGGLE to "Touchpad Toggle", + Scancode.KEY_TOUCHPAD_ON to "Touchpad On", + Scancode.KEY_TOUCHPAD_OFF to "Touchpad Off", + + // Sensor keys + Scancode.KEY_ALS_TOGGLE to "ALS Toggle", + Scancode.KEY_ROTATE_LOCK_TOGGLE to "Rotate Lock Toggle", + + // Macro keys (first 10) + Scancode.KEY_MACRO1 to "Macro 1", + Scancode.KEY_MACRO2 to "Macro 2", + Scancode.KEY_MACRO3 to "Macro 3", + Scancode.KEY_MACRO4 to "Macro 4", + Scancode.KEY_MACRO5 to "Macro 5", + Scancode.KEY_MACRO6 to "Macro 6", + Scancode.KEY_MACRO7 to "Macro 7", + Scancode.KEY_MACRO8 to "Macro 8", + Scancode.KEY_MACRO9 to "Macro 9", + Scancode.KEY_MACRO10 to "Macro 10" + ) + + fun getScancodeLabel(scancode: Int): String? { + return SCANCODE_LABELS[scancode] + } +} \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index c20924e98b..4f61bdd366 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1515,6 +1515,8 @@ Swipe left fingerprint reader Swipe right fingerprint reader Advanced triggers + Key code %d + Scan code %d Remove diff --git a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt index 7cec55bf22..5ab6e8f6c0 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt @@ -23,7 +23,7 @@ import io.github.sds100.keymapper.base.utils.triggerKey import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.dataOrNull -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -758,7 +758,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `add modifier key event action, enable hold down option and disable repeat option`() = runTest(testDispatcher) { - InputEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> + KeyEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> useCase.keyMap.value = State.Data(KeyMap()) useCase.addAction(ActionData.InputKeyEvent(keyCode)) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt new file mode 100644 index 0000000000..26216f6913 --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt @@ -0,0 +1,70 @@ +package io.github.sds100.keymapper.base.trigger + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.system.inputevents.Scancode +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.Test + +class TriggerKeyTest { + @Test + fun `detect with scan code if key code is unknown and user setting enabled`() { + val triggerKey = KeyEventTriggerKey( + keyCode = 0, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ) + assertThat(triggerKey.detectWithScancode(), `is`(true)) + } + + @Test + fun `detect with scan code if key code is unknown and user setting disabled`() { + val triggerKey = KeyEventTriggerKey( + keyCode = 0, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + assertThat(triggerKey.detectWithScancode(), `is`(true)) + } + + @Test + fun `detect with scan code if user setting enabled and scan code non null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ) + assertThat(triggerKey.detectWithScancode(), `is`(true)) + } + + @Test + fun `detect with key code if user setting enabled and scan code is null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = null, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ) + assertThat(triggerKey.detectWithScancode(), `is`(false)) + } + + @Test + fun `detect with key code if user setting false and key code is known`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + assertThat(triggerKey.detectWithScancode(), `is`(false)) + } +} \ No newline at end of file diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt index ca94033015..b493223af8 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt @@ -6,23 +6,6 @@ import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method object InputDeviceUtils { - val SOURCE_NAMES: Map = mapOf( - InputDevice.SOURCE_DPAD to "DPAD", - InputDevice.SOURCE_GAMEPAD to "GAMEPAD", - InputDevice.SOURCE_JOYSTICK to "JOYSTICK", - InputDevice.SOURCE_KEYBOARD to "KEYBOARD", - InputDevice.SOURCE_MOUSE to "MOUSE", - InputDevice.SOURCE_TOUCHSCREEN to "TOUCHSCREEN", - InputDevice.SOURCE_TOUCHPAD to "TOUCHPAD", - InputDevice.SOURCE_TRACKBALL to "TRACKBALL", - InputDevice.SOURCE_CLASS_BUTTON to "BUTTON", - InputDevice.SOURCE_CLASS_JOYSTICK to "JOYSTICK", - InputDevice.SOURCE_CLASS_POINTER to "POINTER", - InputDevice.SOURCE_CLASS_POSITION to "POSITION", - InputDevice.SOURCE_CLASS_TRACKBALL to "TRACKBALL", - - ) - fun appendDeviceDescriptorToName(descriptor: String, name: String): String = "$name ${descriptor.substring(0..4)}" @@ -53,30 +36,4 @@ val InputDevice.isExternalCompat: Boolean e.printStackTrace() false } - } - -fun InputDevice.getBluetoothAddress(): String? { - return try { - val m: Method = InputDevice::class.java.getMethod("getBluetoothAddress") - (m.invoke(this) as String?) - } catch (e: NoSuchMethodException) { - null - } catch (e: IllegalAccessException) { - null - } catch (e: InvocationTargetException) { - null - } -} - -fun InputDevice.getDeviceBus(): Int { - return try { - val m: Method = InputDevice::class.java.getMethod("getDeviceBus") - (m.invoke(this) as Int) - } catch (e: NoSuchMethodException) { - -1 - } catch (e: IllegalAccessException) { - -1 - } catch (e: InvocationTargetException) { - -1 - } -} + } \ No newline at end of file diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt index 789313a55a..e8974e17ea 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt @@ -47,5 +47,6 @@ data class EvdevTriggerKeyEntity( const val NAME_FLAGS = "flags" const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1 + const val FLAG_DETECT_WITH_SCAN_CODE = 2 } } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt index f962abf064..210bb61ec6 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt @@ -44,5 +44,6 @@ data class KeyEventTriggerKeyEntity( const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1 const val FLAG_DETECTION_SOURCE_INPUT_METHOD = 2 + const val FLAG_DETECT_WITH_SCAN_CODE = 4 } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt similarity index 93% rename from system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt rename to system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt index 638ac9dd47..6e781f8fd1 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt @@ -3,8 +3,7 @@ package io.github.sds100.keymapper.system.inputevents import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.withFlag -object InputEventUtils { - +object KeyEventUtils { private val KEYCODES: IntArray = intArrayOf( KeyEvent.KEYCODE_SOFT_LEFT, KeyEvent.KEYCODE_SOFT_RIGHT, @@ -326,32 +325,6 @@ object InputEventUtils { KeyEvent.KEYCODE_SCREENSHOT, ) - /** - * These are key code maps for the getevent command. These names aren't the same as the - * KeyEvent key codes in the Android SDK so these have to be manually whitelisted - * as people need. - */ - val GET_EVENT_LABEL_TO_KEYCODE: List> = listOf( - "KEY_VOLUMEDOWN" to KeyEvent.KEYCODE_VOLUME_DOWN, - "KEY_VOLUMEUP" to KeyEvent.KEYCODE_VOLUME_UP, - "KEY_MEDIA" to KeyEvent.KEYCODE_HEADSETHOOK, - "KEY_HEADSETHOOK" to KeyEvent.KEYCODE_HEADSETHOOK, - "KEY_CAMERA_FOCUS" to KeyEvent.KEYCODE_FOCUS, - "02fe" to KeyEvent.KEYCODE_CAMERA, - "00fa" to KeyEvent.KEYCODE_CAMERA, - - // This kernel key event code seems to be the Bixby button - // but different ROMs have different key maps and so - // it is reported as different Android key codes. - "02bf" to KeyEvent.KEYCODE_MENU, - "02bf" to KeyEvent.KEYCODE_ASSIST, - - "KEY_SEARCH" to KeyEvent.KEYCODE_SEARCH, - ) - - fun canDetectKeyWhenScreenOff(keyCode: Int): Boolean = - GET_EVENT_LABEL_TO_KEYCODE.any { it.second == keyCode } - val MODIFIER_KEYCODES: Set get() = setOf( KeyEvent.KEYCODE_SHIFT_LEFT, @@ -367,11 +340,6 @@ object InputEventUtils { KeyEvent.KEYCODE_FUNCTION, ) - /** - * Used for keyCode to scanCode fallback to go past possible keyCode values - */ - const val KEYCODE_TO_SCANCODE_OFFSET: Int = 1000 - fun isModifierKey(keyCode: Int): Boolean = keyCode in MODIFIER_KEYCODES fun isGamepadKeyCode(keyCode: Int): Boolean { @@ -494,4 +462,10 @@ object InputEventUtils { else -> false } } + + fun isKeyCodeUnknown(keyCode: Int): Boolean { + // The lowest key code is 1 (KEYCODE_SOFT_LEFT) + return keyCode > KeyEvent.getMaxKeyCode() || keyCode < 1 + } } + diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt new file mode 100644 index 0000000000..bc2d90fcd3 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt @@ -0,0 +1,635 @@ +package io.github.sds100.keymapper.system.inputevents + +object Scancode { + const val KEY_1 = 2 + const val KEY_2 = 3 + const val KEY_3 = 4 + const val KEY_4 = 5 + const val KEY_5 = 6 + const val KEY_6 = 7 + const val KEY_7 = 8 + const val KEY_8 = 9 + const val KEY_9 = 10 + const val KEY_0 = 11 + const val KEY_MINUS = 12 + const val KEY_EQUAL = 13 + const val KEY_BACKSPACE = 14 + const val KEY_TAB = 15 + const val KEY_Q = 16 + const val KEY_W = 17 + const val KEY_E = 18 + const val KEY_R = 19 + const val KEY_T = 20 + const val KEY_Y = 21 + const val KEY_U = 22 + const val KEY_I = 23 + const val KEY_O = 24 + const val KEY_P = 25 + const val KEY_LEFTBRACE = 26 + const val KEY_RIGHTBRACE = 27 + const val KEY_ENTER = 28 + const val KEY_LEFTCTRL = 29 + const val KEY_A = 30 + const val KEY_S = 31 + const val KEY_D = 32 + const val KEY_F = 33 + const val KEY_G = 34 + const val KEY_H = 35 + const val KEY_J = 36 + const val KEY_K = 37 + const val KEY_L = 38 + const val KEY_SEMICOLON = 39 + const val KEY_APOSTROPHE = 40 + const val KEY_GRAVE = 41 + const val KEY_LEFTSHIFT = 42 + const val KEY_BACKSLASH = 43 + const val KEY_Z = 44 + const val KEY_X = 45 + const val KEY_C = 46 + const val KEY_V = 47 + const val KEY_B = 48 + const val KEY_N = 49 + const val KEY_M = 50 + const val KEY_COMMA = 51 + const val KEY_DOT = 52 + const val KEY_SLASH = 53 + const val KEY_RIGHTSHIFT = 54 + const val KEY_KPASTERISK = 55 + const val KEY_LEFTALT = 56 + const val KEY_SPACE = 57 + const val KEY_CAPSLOCK = 58 + const val KEY_F1 = 59 + const val KEY_F2 = 60 + const val KEY_F3 = 61 + const val KEY_F4 = 62 + const val KEY_F5 = 63 + const val KEY_F6 = 64 + const val KEY_F7 = 65 + const val KEY_F8 = 66 + const val KEY_F9 = 67 + const val KEY_F10 = 68 + const val KEY_NUMLOCK = 69 + const val KEY_SCROLLLOCK = 70 + const val KEY_KP7 = 71 + const val KEY_KP8 = 72 + const val KEY_KP9 = 73 + const val KEY_KPMINUS = 74 + const val KEY_KP4 = 75 + const val KEY_KP5 = 76 + const val KEY_KP6 = 77 + const val KEY_KPPLUS = 78 + const val KEY_KP1 = 79 + const val KEY_KP2 = 80 + const val KEY_KP3 = 81 + const val KEY_KP0 = 82 + const val KEY_KPDOT = 83 + const val KEY_ZENKAKUHANKAKU = 85 + const val KEY_102ND = 86 + const val KEY_F11 = 87 + const val KEY_F12 = 88 + const val KEY_RO = 89 + const val KEY_KATAKANA = 90 + const val KEY_HIRAGANA = 91 + const val KEY_HENKAN = 92 + const val KEY_KATAKANAHIRAGANA = 93 + const val KEY_MUHENKAN = 94 + const val KEY_KPJPCOMMA = 95 + const val KEY_KPENTER = 96 + const val KEY_RIGHTCTRL = 97 + const val KEY_KPSLASH = 98 + const val KEY_SYSRQ = 99 + const val KEY_RIGHTALT = 100 + const val KEY_LINEFEED = 101 + const val KEY_HOME = 102 + const val KEY_UP = 103 + const val KEY_PAGEUP = 104 + const val KEY_LEFT = 105 + const val KEY_RIGHT = 106 + const val KEY_END = 107 + const val KEY_DOWN = 108 + const val KEY_PAGEDOWN = 109 + const val KEY_INSERT = 110 + const val KEY_DELETE = 111 + const val KEY_MACRO = 112 + const val KEY_MUTE = 113 + const val KEY_VOLUMEDOWN = 114 + const val KEY_VOLUMEUP = 115 + const val KEY_POWER = 116 + const val KEY_KPEQUAL = 117 + const val KEY_KPPLUSMINUS = 118 + const val KEY_PAUSE = 119 + const val KEY_SCALE = 120 + const val KEY_KPCOMMA = 121 + const val KEY_HANGEUL = 122 + const val KEY_HANGUEL = KEY_HANGEUL + const val KEY_HANJA = 123 + const val KEY_YEN = 124 + const val KEY_LEFTMETA = 125 + const val KEY_RIGHTMETA = 126 + const val KEY_COMPOSE = 127 + const val KEY_STOP = 128 + const val KEY_AGAIN = 129 + const val KEY_PROPS = 130 + const val KEY_UNDO = 131 + const val KEY_FRONT = 132 + const val KEY_COPY = 133 + const val KEY_OPEN = 134 + const val KEY_PASTE = 135 + const val KEY_FIND = 136 + const val KEY_CUT = 137 + const val KEY_HELP = 138 + const val KEY_MENU = 139 + const val KEY_CALC = 140 + const val KEY_SETUP = 141 + const val KEY_SLEEP = 142 + const val KEY_WAKEUP = 143 + const val KEY_FILE = 144 + const val KEY_SENDFILE = 145 + const val KEY_DELETEFILE = 146 + const val KEY_XFER = 147 + const val KEY_PROG1 = 148 + const val KEY_PROG2 = 149 + const val KEY_WWW = 150 + const val KEY_MSDOS = 151 + const val KEY_COFFEE = 152 + const val KEY_SCREENLOCK = KEY_COFFEE + const val KEY_ROTATE_DISPLAY = 153 + const val KEY_DIRECTION = KEY_ROTATE_DISPLAY + const val KEY_CYCLEWINDOWS = 154 + const val KEY_MAIL = 155 + const val KEY_BOOKMARKS = 156 + const val KEY_COMPUTER = 157 + const val KEY_BACK = 158 + const val KEY_FORWARD = 159 + const val KEY_CLOSECD = 160 + const val KEY_EJECTCD = 161 + const val KEY_EJECTCLOSECD = 162 + const val KEY_NEXTSONG = 163 + const val KEY_PLAYPAUSE = 164 + const val KEY_PREVIOUSSONG = 165 + const val KEY_STOPCD = 166 + const val KEY_RECORD = 167 + const val KEY_REWIND = 168 + const val KEY_PHONE = 169 + const val KEY_ISO = 170 + const val KEY_CONFIG = 171 + const val KEY_HOMEPAGE = 172 + const val KEY_REFRESH = 173 + const val KEY_EXIT = 174 + const val KEY_MOVE = 175 + const val KEY_EDIT = 176 + const val KEY_SCROLLUP = 177 + const val KEY_SCROLLDOWN = 178 + const val KEY_KPLEFTPAREN = 179 + const val KEY_KPRIGHTPAREN = 180 + const val KEY_NEW = 181 + const val KEY_REDO = 182 + const val KEY_F13 = 183 + const val KEY_F14 = 184 + const val KEY_F15 = 185 + const val KEY_F16 = 186 + const val KEY_F17 = 187 + const val KEY_F18 = 188 + const val KEY_F19 = 189 + const val KEY_F20 = 190 + const val KEY_F21 = 191 + const val KEY_F22 = 192 + const val KEY_F23 = 193 + const val KEY_F24 = 194 + const val KEY_PLAYCD = 200 + const val KEY_PAUSECD = 201 + const val KEY_PROG3 = 202 + const val KEY_PROG4 = 203 + const val KEY_ALL_APPLICATIONS = 204 + const val KEY_DASHBOARD = KEY_ALL_APPLICATIONS + const val KEY_SUSPEND = 205 + const val KEY_CLOSE = 206 + const val KEY_PLAY = 207 + const val KEY_FASTFORWARD = 208 + const val KEY_BASSBOOST = 209 + const val KEY_PRINT = 210 + const val KEY_HP = 211 + const val KEY_CAMERA = 212 + const val KEY_SOUND = 213 + const val KEY_QUESTION = 214 + const val KEY_EMAIL = 215 + const val KEY_CHAT = 216 + const val KEY_SEARCH = 217 + const val KEY_CONNECT = 218 + const val KEY_FINANCE = 219 + const val KEY_SPORT = 220 + const val KEY_SHOP = 221 + const val KEY_ALTERASE = 222 + const val KEY_CANCEL = 223 + const val KEY_BRIGHTNESSDOWN = 224 + const val KEY_BRIGHTNESSUP = 225 + const val KEY_MEDIA = 226 + const val KEY_SWITCHVIDEOMODE = 227 + const val KEY_KBDILLUMTOGGLE = 228 + const val KEY_KBDILLUMDOWN = 229 + const val KEY_KBDILLUMUP = 230 + const val KEY_SEND = 231 + const val KEY_REPLY = 232 + const val KEY_FORWARDMAIL = 233 + const val KEY_SAVE = 234 + const val KEY_DOCUMENTS = 235 + const val KEY_BATTERY = 236 + const val KEY_BLUETOOTH = 237 + const val KEY_WLAN = 238 + const val KEY_UWB = 239 + const val KEY_UNKNOWN = 240 + const val KEY_VIDEO_NEXT = 241 + const val KEY_VIDEO_PREV = 242 + const val KEY_BRIGHTNESS_CYCLE = 243 + const val KEY_BRIGHTNESS_AUTO = 244 + const val KEY_BRIGHTNESS_ZERO = KEY_BRIGHTNESS_AUTO + const val KEY_DISPLAY_OFF = 245 + const val KEY_WWAN = 246 + const val KEY_WIMAX = KEY_WWAN + const val KEY_RFKILL = 247 + const val KEY_MICMUTE = 248 + const val BTN_MISC = 0x100 + const val BTN_0 = 0x100 + const val BTN_1 = 0x101 + const val BTN_2 = 0x102 + const val BTN_3 = 0x103 + const val BTN_4 = 0x104 + const val BTN_5 = 0x105 + const val BTN_6 = 0x106 + const val BTN_7 = 0x107 + const val BTN_8 = 0x108 + const val BTN_9 = 0x109 + const val BTN_MOUSE = 0x110 + const val BTN_LEFT = 0x110 + const val BTN_RIGHT = 0x111 + const val BTN_MIDDLE = 0x112 + const val BTN_SIDE = 0x113 + const val BTN_EXTRA = 0x114 + const val BTN_FORWARD = 0x115 + const val BTN_BACK = 0x116 + const val BTN_TASK = 0x117 + const val BTN_JOYSTICK = 0x120 + const val BTN_TRIGGER = 0x120 + const val BTN_THUMB = 0x121 + const val BTN_THUMB2 = 0x122 + const val BTN_TOP = 0x123 + const val BTN_TOP2 = 0x124 + const val BTN_PINKIE = 0x125 + const val BTN_BASE = 0x126 + const val BTN_BASE2 = 0x127 + const val BTN_BASE3 = 0x128 + const val BTN_BASE4 = 0x129 + const val BTN_BASE5 = 0x12a + const val BTN_BASE6 = 0x12b + const val BTN_DEAD = 0x12f + const val BTN_GAMEPAD = 0x130 + const val BTN_SOUTH = 0x130 + const val BTN_A = BTN_SOUTH + const val BTN_EAST = 0x131 + const val BTN_B = BTN_EAST + const val BTN_C = 0x132 + const val BTN_NORTH = 0x133 + const val BTN_X = BTN_NORTH + const val BTN_WEST = 0x134 + const val BTN_Y = BTN_WEST + const val BTN_Z = 0x135 + const val BTN_TL = 0x136 + const val BTN_TR = 0x137 + const val BTN_TL2 = 0x138 + const val BTN_TR2 = 0x139 + const val BTN_SELECT = 0x13a + const val BTN_START = 0x13b + const val BTN_MODE = 0x13c + const val BTN_THUMBL = 0x13d + const val BTN_THUMBR = 0x13e + const val BTN_DIGI = 0x140 + const val BTN_TOOL_PEN = 0x140 + const val BTN_TOOL_RUBBER = 0x141 + const val BTN_TOOL_BRUSH = 0x142 + const val BTN_TOOL_PENCIL = 0x143 + const val BTN_TOOL_AIRBRUSH = 0x144 + const val BTN_TOOL_FINGER = 0x145 + const val BTN_TOOL_MOUSE = 0x146 + const val BTN_TOOL_LENS = 0x147 + const val BTN_TOOL_QUINTTAP = 0x148 + const val BTN_STYLUS3 = 0x149 + const val BTN_TOUCH = 0x14a + const val BTN_STYLUS = 0x14b + const val BTN_STYLUS2 = 0x14c + const val BTN_TOOL_DOUBLETAP = 0x14d + const val BTN_TOOL_TRIPLETAP = 0x14e + const val BTN_TOOL_QUADTAP = 0x14f + const val BTN_WHEEL = 0x150 + const val BTN_GEAR_DOWN = 0x150 + const val BTN_GEAR_UP = 0x151 + const val KEY_OK = 0x160 + const val KEY_SELECT = 0x161 + const val KEY_GOTO = 0x162 + const val KEY_CLEAR = 0x163 + const val KEY_POWER2 = 0x164 + const val KEY_OPTION = 0x165 + const val KEY_INFO = 0x166 + const val KEY_TIME = 0x167 + const val KEY_VENDOR = 0x168 + const val KEY_ARCHIVE = 0x169 + const val KEY_PROGRAM = 0x16a + const val KEY_CHANNEL = 0x16b + const val KEY_FAVORITES = 0x16c + const val KEY_EPG = 0x16d + const val KEY_PVR = 0x16e + const val KEY_MHP = 0x16f + const val KEY_LANGUAGE = 0x170 + const val KEY_TITLE = 0x171 + const val KEY_SUBTITLE = 0x172 + const val KEY_ANGLE = 0x173 + const val KEY_FULL_SCREEN = 0x174 + const val KEY_ZOOM = KEY_FULL_SCREEN + const val KEY_MODE = 0x175 + const val KEY_KEYBOARD = 0x176 + const val KEY_ASPECT_RATIO = 0x177 + const val KEY_SCREEN = KEY_ASPECT_RATIO + const val KEY_PC = 0x178 + const val KEY_TV = 0x179 + const val KEY_TV2 = 0x17a + const val KEY_VCR = 0x17b + const val KEY_VCR2 = 0x17c + const val KEY_SAT = 0x17d + const val KEY_SAT2 = 0x17e + const val KEY_CD = 0x17f + const val KEY_TAPE = 0x180 + const val KEY_RADIO = 0x181 + const val KEY_TUNER = 0x182 + const val KEY_PLAYER = 0x183 + const val KEY_TEXT = 0x184 + const val KEY_DVD = 0x185 + const val KEY_AUX = 0x186 + const val KEY_MP3 = 0x187 + const val KEY_AUDIO = 0x188 + const val KEY_VIDEO = 0x189 + const val KEY_DIRECTORY = 0x18a + const val KEY_LIST = 0x18b + const val KEY_MEMO = 0x18c + const val KEY_CALENDAR = 0x18d + const val KEY_RED = 0x18e + const val KEY_GREEN = 0x18f + const val KEY_YELLOW = 0x190 + const val KEY_BLUE = 0x191 + const val KEY_CHANNELUP = 0x192 + const val KEY_CHANNELDOWN = 0x193 + const val KEY_FIRST = 0x194 + const val KEY_LAST = 0x195 + const val KEY_AB = 0x196 + const val KEY_NEXT = 0x197 + const val KEY_RESTART = 0x198 + const val KEY_SLOW = 0x199 + const val KEY_SHUFFLE = 0x19a + const val KEY_BREAK = 0x19b + const val KEY_PREVIOUS = 0x19c + const val KEY_DIGITS = 0x19d + const val KEY_TEEN = 0x19e + const val KEY_TWEN = 0x19f + const val KEY_VIDEOPHONE = 0x1a0 + const val KEY_GAMES = 0x1a1 + const val KEY_ZOOMIN = 0x1a2 + const val KEY_ZOOMOUT = 0x1a3 + const val KEY_ZOOMRESET = 0x1a4 + const val KEY_WORDPROCESSOR = 0x1a5 + const val KEY_EDITOR = 0x1a6 + const val KEY_SPREADSHEET = 0x1a7 + const val KEY_GRAPHICSEDITOR = 0x1a8 + const val KEY_PRESENTATION = 0x1a9 + const val KEY_DATABASE = 0x1aa + const val KEY_NEWS = 0x1ab + const val KEY_VOICEMAIL = 0x1ac + const val KEY_ADDRESSBOOK = 0x1ad + const val KEY_MESSENGER = 0x1ae + const val KEY_DISPLAYTOGGLE = 0x1af + const val KEY_BRIGHTNESS_TOGGLE = KEY_DISPLAYTOGGLE + const val KEY_SPELLCHECK = 0x1b0 + const val KEY_LOGOFF = 0x1b1 + const val KEY_DOLLAR = 0x1b2 + const val KEY_EURO = 0x1b3 + const val KEY_FRAMEBACK = 0x1b4 + const val KEY_FRAMEFORWARD = 0x1b5 + const val KEY_CONTEXT_MENU = 0x1b6 + const val KEY_MEDIA_REPEAT = 0x1b7 + const val KEY_10CHANNELSUP = 0x1b8 + const val KEY_10CHANNELSDOWN = 0x1b9 + const val KEY_IMAGES = 0x1ba + const val KEY_NOTIFICATION_CENTER = 0x1bc + const val KEY_PICKUP_PHONE = 0x1bd + const val KEY_HANGUP_PHONE = 0x1be + const val KEY_DEL_EOL = 0x1c0 + const val KEY_DEL_EOS = 0x1c1 + const val KEY_INS_LINE = 0x1c2 + const val KEY_DEL_LINE = 0x1c3 + const val KEY_FN = 0x1d0 + const val KEY_FN_ESC = 0x1d1 + const val KEY_FN_F1 = 0x1d2 + const val KEY_FN_F2 = 0x1d3 + const val KEY_FN_F3 = 0x1d4 + const val KEY_FN_F4 = 0x1d5 + const val KEY_FN_F5 = 0x1d6 + const val KEY_FN_F6 = 0x1d7 + const val KEY_FN_F7 = 0x1d8 + const val KEY_FN_F8 = 0x1d9 + const val KEY_FN_F9 = 0x1da + const val KEY_FN_F10 = 0x1db + const val KEY_FN_F11 = 0x1dc + const val KEY_FN_F12 = 0x1dd + const val KEY_FN_1 = 0x1de + const val KEY_FN_2 = 0x1df + const val KEY_FN_D = 0x1e0 + const val KEY_FN_E = 0x1e1 + const val KEY_FN_F = 0x1e2 + const val KEY_FN_S = 0x1e3 + const val KEY_FN_B = 0x1e4 + const val KEY_FN_RIGHT_SHIFT = 0x1e5 + const val KEY_BRL_DOT1 = 0x1f1 + const val KEY_BRL_DOT2 = 0x1f2 + const val KEY_BRL_DOT3 = 0x1f3 + const val KEY_BRL_DOT4 = 0x1f4 + const val KEY_BRL_DOT5 = 0x1f5 + const val KEY_BRL_DOT6 = 0x1f6 + const val KEY_BRL_DOT7 = 0x1f7 + const val KEY_BRL_DOT8 = 0x1f8 + const val KEY_BRL_DOT9 = 0x1f9 + const val KEY_BRL_DOT10 = 0x1fa + const val KEY_NUMERIC_0 = 0x200 + const val KEY_NUMERIC_1 = 0x201 + const val KEY_NUMERIC_2 = 0x202 + const val KEY_NUMERIC_3 = 0x203 + const val KEY_NUMERIC_4 = 0x204 + const val KEY_NUMERIC_5 = 0x205 + const val KEY_NUMERIC_6 = 0x206 + const val KEY_NUMERIC_7 = 0x207 + const val KEY_NUMERIC_8 = 0x208 + const val KEY_NUMERIC_9 = 0x209 + const val KEY_NUMERIC_STAR = 0x20a + const val KEY_NUMERIC_POUND = 0x20b + const val KEY_NUMERIC_A = 0x20c + const val KEY_NUMERIC_B = 0x20d + const val KEY_NUMERIC_C = 0x20e + const val KEY_NUMERIC_D = 0x20f + const val KEY_CAMERA_FOCUS = 0x210 + const val KEY_WPS_BUTTON = 0x211 + const val KEY_TOUCHPAD_TOGGLE = 0x212 + const val KEY_TOUCHPAD_ON = 0x213 + const val KEY_TOUCHPAD_OFF = 0x214 + const val KEY_CAMERA_ZOOMIN = 0x215 + const val KEY_CAMERA_ZOOMOUT = 0x216 + const val KEY_CAMERA_UP = 0x217 + const val KEY_CAMERA_DOWN = 0x218 + const val KEY_CAMERA_LEFT = 0x219 + const val KEY_CAMERA_RIGHT = 0x21a + const val KEY_ATTENDANT_ON = 0x21b + const val KEY_ATTENDANT_OFF = 0x21c + const val KEY_ATTENDANT_TOGGLE = 0x21d + const val KEY_LIGHTS_TOGGLE = 0x21e + const val BTN_DPAD_UP = 0x220 + const val BTN_DPAD_DOWN = 0x221 + const val BTN_DPAD_LEFT = 0x222 + const val BTN_DPAD_RIGHT = 0x223 + const val KEY_ALS_TOGGLE = 0x230 + const val KEY_ROTATE_LOCK_TOGGLE = 0x231 + const val KEY_BUTTONCONFIG = 0x240 + const val KEY_TASKMANAGER = 0x241 + const val KEY_JOURNAL = 0x242 + const val KEY_CONTROLPANEL = 0x243 + const val KEY_APPSELECT = 0x244 + const val KEY_SCREENSAVER = 0x245 + const val KEY_VOICECOMMAND = 0x246 + const val KEY_ASSISTANT = 0x247 + const val KEY_KBD_LAYOUT_NEXT = 0x248 + const val KEY_EMOJI_PICKER = 0x249 + const val KEY_DICTATE = 0x24a + const val KEY_CAMERA_ACCESS_ENABLE = 0x24b + const val KEY_CAMERA_ACCESS_DISABLE = 0x24c + const val KEY_CAMERA_ACCESS_TOGGLE = 0x24d + const val KEY_BRIGHTNESS_MIN = 0x250 + const val KEY_BRIGHTNESS_MAX = 0x251 + const val KEY_KBDINPUTASSIST_PREV = 0x260 + const val KEY_KBDINPUTASSIST_NEXT = 0x261 + const val KEY_KBDINPUTASSIST_PREVGROUP = 0x262 + const val KEY_KBDINPUTASSIST_NEXTGROUP = 0x263 + const val KEY_KBDINPUTASSIST_ACCEPT = 0x264 + const val KEY_KBDINPUTASSIST_CANCEL = 0x265 + const val KEY_RIGHT_UP = 0x266 + const val KEY_RIGHT_DOWN = 0x267 + const val KEY_LEFT_UP = 0x268 + const val KEY_LEFT_DOWN = 0x269 + const val KEY_ROOT_MENU = 0x26a + const val KEY_MEDIA_TOP_MENU = 0x26b + const val KEY_NUMERIC_11 = 0x26c + const val KEY_NUMERIC_12 = 0x26d + const val KEY_AUDIO_DESC = 0x26e + const val KEY_3D_MODE = 0x26f + const val KEY_NEXT_FAVORITE = 0x270 + const val KEY_STOP_RECORD = 0x271 + const val KEY_PAUSE_RECORD = 0x272 + const val KEY_VOD = 0x273 + const val KEY_UNMUTE = 0x274 + const val KEY_FASTREVERSE = 0x275 + const val KEY_SLOWREVERSE = 0x276 + const val KEY_DATA = 0x277 + const val KEY_ONSCREEN_KEYBOARD = 0x278 + const val KEY_PRIVACY_SCREEN_TOGGLE = 0x279 + const val KEY_SELECTIVE_SCREENSHOT = 0x27a + const val KEY_NEXT_ELEMENT = 0x27b + const val KEY_PREVIOUS_ELEMENT = 0x27c + const val KEY_AUTOPILOT_ENGAGE_TOGGLE = 0x27d + const val KEY_MARK_WAYPOINT = 0x27e + const val KEY_SOS = 0x27f + const val KEY_NAV_CHART = 0x280 + const val KEY_FISHING_CHART = 0x281 + const val KEY_SINGLE_RANGE_RADAR = 0x282 + const val KEY_DUAL_RANGE_RADAR = 0x283 + const val KEY_RADAR_OVERLAY = 0x284 + const val KEY_TRADITIONAL_SONAR = 0x285 + const val KEY_CLEARVU_SONAR = 0x286 + const val KEY_SIDEVU_SONAR = 0x287 + const val KEY_NAV_INFO = 0x288 + const val KEY_BRIGHTNESS_MENU = 0x289 + const val KEY_MACRO1 = 0x290 + const val KEY_MACRO2 = 0x291 + const val KEY_MACRO3 = 0x292 + const val KEY_MACRO4 = 0x293 + const val KEY_MACRO5 = 0x294 + const val KEY_MACRO6 = 0x295 + const val KEY_MACRO7 = 0x296 + const val KEY_MACRO8 = 0x297 + const val KEY_MACRO9 = 0x298 + const val KEY_MACRO10 = 0x299 + const val KEY_MACRO11 = 0x29a + const val KEY_MACRO12 = 0x29b + const val KEY_MACRO13 = 0x29c + const val KEY_MACRO14 = 0x29d + const val KEY_MACRO15 = 0x29e + const val KEY_MACRO16 = 0x29f + const val KEY_MACRO17 = 0x2a0 + const val KEY_MACRO18 = 0x2a1 + const val KEY_MACRO19 = 0x2a2 + const val KEY_MACRO20 = 0x2a3 + const val KEY_MACRO21 = 0x2a4 + const val KEY_MACRO22 = 0x2a5 + const val KEY_MACRO23 = 0x2a6 + const val KEY_MACRO24 = 0x2a7 + const val KEY_MACRO25 = 0x2a8 + const val KEY_MACRO26 = 0x2a9 + const val KEY_MACRO27 = 0x2aa + const val KEY_MACRO28 = 0x2ab + const val KEY_MACRO29 = 0x2ac + const val KEY_MACRO30 = 0x2ad + const val KEY_MACRO_RECORD_START = 0x2b0 + const val KEY_MACRO_RECORD_STOP = 0x2b1 + const val KEY_MACRO_PRESET_CYCLE = 0x2b2 + const val KEY_MACRO_PRESET1 = 0x2b3 + const val KEY_MACRO_PRESET2 = 0x2b4 + const val KEY_MACRO_PRESET3 = 0x2b5 + const val KEY_KBD_LCD_MENU1 = 0x2b8 + const val KEY_KBD_LCD_MENU2 = 0x2b9 + const val KEY_KBD_LCD_MENU3 = 0x2ba + const val KEY_KBD_LCD_MENU4 = 0x2bb + const val KEY_KBD_LCD_MENU5 = 0x2bc + const val BTN_TRIGGER_HAPPY = 0x2c0 + const val BTN_TRIGGER_HAPPY1 = 0x2c0 + const val BTN_TRIGGER_HAPPY2 = 0x2c1 + const val BTN_TRIGGER_HAPPY3 = 0x2c2 + const val BTN_TRIGGER_HAPPY4 = 0x2c3 + const val BTN_TRIGGER_HAPPY5 = 0x2c4 + const val BTN_TRIGGER_HAPPY6 = 0x2c5 + const val BTN_TRIGGER_HAPPY7 = 0x2c6 + const val BTN_TRIGGER_HAPPY8 = 0x2c7 + const val BTN_TRIGGER_HAPPY9 = 0x2c8 + const val BTN_TRIGGER_HAPPY10 = 0x2c9 + const val BTN_TRIGGER_HAPPY11 = 0x2ca + const val BTN_TRIGGER_HAPPY12 = 0x2cb + const val BTN_TRIGGER_HAPPY13 = 0x2cc + const val BTN_TRIGGER_HAPPY14 = 0x2cd + const val BTN_TRIGGER_HAPPY15 = 0x2ce + const val BTN_TRIGGER_HAPPY16 = 0x2cf + const val BTN_TRIGGER_HAPPY17 = 0x2d0 + const val BTN_TRIGGER_HAPPY18 = 0x2d1 + const val BTN_TRIGGER_HAPPY19 = 0x2d2 + const val BTN_TRIGGER_HAPPY20 = 0x2d3 + const val BTN_TRIGGER_HAPPY21 = 0x2d4 + const val BTN_TRIGGER_HAPPY22 = 0x2d5 + const val BTN_TRIGGER_HAPPY23 = 0x2d6 + const val BTN_TRIGGER_HAPPY24 = 0x2d7 + const val BTN_TRIGGER_HAPPY25 = 0x2d8 + const val BTN_TRIGGER_HAPPY26 = 0x2d9 + const val BTN_TRIGGER_HAPPY27 = 0x2da + const val BTN_TRIGGER_HAPPY28 = 0x2db + const val BTN_TRIGGER_HAPPY29 = 0x2dc + const val BTN_TRIGGER_HAPPY30 = 0x2dd + const val BTN_TRIGGER_HAPPY31 = 0x2de + const val BTN_TRIGGER_HAPPY32 = 0x2df + const val BTN_TRIGGER_HAPPY33 = 0x2e0 + const val BTN_TRIGGER_HAPPY34 = 0x2e1 + const val BTN_TRIGGER_HAPPY35 = 0x2e2 + const val BTN_TRIGGER_HAPPY36 = 0x2e3 + const val BTN_TRIGGER_HAPPY37 = 0x2e4 + const val BTN_TRIGGER_HAPPY38 = 0x2e5 + const val BTN_TRIGGER_HAPPY39 = 0x2e6 + const val BTN_TRIGGER_HAPPY40 = 0x2e7 +} \ No newline at end of file From d850759198fbeb7c2bb507f74982e6ae0fbd9d7b Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 18:16:42 +0100 Subject: [PATCH 092/215] refactor: move key map related files to separate packages --- .../github/sds100/keymapper/home/HomeViewModel.kt | 2 +- .../keymapper/keymaps/ConfigKeyMapViewModel.kt | 2 +- .../AccessibilityServiceController.kt | 2 +- .../keymapper/trigger/ConfigTriggerViewModel.kt | 2 +- base/src/main/AndroidManifest.xml | 2 +- .../keymapper/base/BaseViewModelHiltModule.kt | 8 ++++---- .../{keymaps => }/detection/DetectKeyMapModel.kt | 2 +- .../detection/DetectKeyMapsUseCase.kt | 2 +- .../DetectScreenOffKeyEventsController.kt | 2 +- .../detection/DpadMotionEventTracker.kt | 2 +- .../{keymaps => }/detection/KeyMapAlgorithm.kt | 2 +- .../detection/KeyMapDetectionController.kt | 2 +- .../{keymaps => }/detection/KeyPressedCallback.kt | 2 +- .../detection/ParallelTriggerActionPerformer.kt | 2 +- .../detection/SequenceTriggerActionPerformer.kt | 2 +- .../SimpleMappingController.kt | 14 +++++++------- .../TriggerKeyMapFromOtherAppsController.kt | 4 ++-- .../keymapper/base/home/BaseHomeViewModel.kt | 2 -- .../keymapper/base/home/HomeKeyMapListScreen.kt | 3 --- .../base/{keymaps => home}/KeyMapAppBarState.kt | 4 +--- .../base/{keymaps => home}/KeyMapGroup.kt | 3 ++- .../sds100/keymapper/base/home/KeyMapListAppBar.kt | 1 - .../{keymaps => home}/KeyMapListItemCreator.kt | 5 ++++- .../base/{keymaps => home}/KeyMapListScreen.kt | 2 +- .../base/{keymaps => home}/KeyMapListState.kt | 2 +- .../base/{keymaps => home}/KeyMapListViewModel.kt | 7 +++---- .../base/{keymaps => home}/ListKeyMapsUseCase.kt | 5 ++++- .../base/keymaps/ConfigKeyMapOptionsViewModel.kt | 2 ++ .../keymapper/base/keymaps/ConfigKeyMapUseCase.kt | 3 ++- .../github/sds100/keymapper/base/keymaps/KeyMap.kt | 2 +- .../CreateKeyMapShortcutActivity.kt | 2 +- .../CreateKeyMapShortcutScreen.kt | 5 ++++- .../CreateKeyMapShortcutUseCase.kt | 2 +- .../CreateKeyMapShortcutViewModel.kt | 8 +++++++- .../BaseAccessibilityServiceController.kt | 6 +++--- .../base/trigger/BaseConfigTriggerViewModel.kt | 2 +- .../base/trigger/RecordTriggerController.kt | 2 +- .../keymapper/base/utils/ui/compose/CompactChip.kt | 2 +- .../base/keymaps/DpadMotionEventTrackerTest.kt | 2 +- .../keymapper/base/keymaps/KeyMapAlgorithmTest.kt | 6 +++--- .../keymaps/ProcessKeyMapGroupsForDetectionTest.kt | 4 ++-- .../TriggerKeyMapFromOtherAppsControllerTest.kt | 4 ++-- 42 files changed, 75 insertions(+), 65 deletions(-) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/DetectKeyMapModel.kt (80%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/DetectKeyMapsUseCase.kt (99%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/DetectScreenOffKeyEventsController.kt (95%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/DpadMotionEventTracker.kt (98%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/KeyMapAlgorithm.kt (99%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/KeyMapDetectionController.kt (98%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/KeyPressedCallback.kt (62%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/ParallelTriggerActionPerformer.kt (99%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/SequenceTriggerActionPerformer.kt (95%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => detection}/SimpleMappingController.kt (95%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => }/detection/TriggerKeyMapFromOtherAppsController.kt (91%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => home}/KeyMapAppBarState.kt (86%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => home}/KeyMapGroup.kt (72%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => home}/KeyMapListItemCreator.kt (98%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => home}/KeyMapListScreen.kt (99%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => home}/KeyMapListState.kt (86%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => home}/KeyMapListViewModel.kt (99%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => home}/ListKeyMapsUseCase.kt (98%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => shortcuts}/CreateKeyMapShortcutActivity.kt (98%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => shortcuts}/CreateKeyMapShortcutScreen.kt (98%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => shortcuts}/CreateKeyMapShortcutUseCase.kt (98%) rename base/src/main/java/io/github/sds100/keymapper/base/{keymaps => shortcuts}/CreateKeyMapShortcutViewModel.kt (95%) 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 0c2f3dc906..5e9d90ee09 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 @@ -4,7 +4,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCase import io.github.sds100.keymapper.base.home.BaseHomeViewModel import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase -import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCase +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase diff --git a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt index 5f3c3e5473..7453ad616c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt @@ -8,7 +8,7 @@ import io.github.sds100.keymapper.base.actions.TestActionUseCase import io.github.sds100.keymapper.base.constraints.ConfigConstraintsViewModel import io.github.sds100.keymapper.base.keymaps.BaseConfigKeyMapViewModel import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index a59b75e914..aa34bd7212 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -8,7 +8,7 @@ import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCaseImpl import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeRecorder import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController diff --git a/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt index 80370bb382..f6b8ba1781 100644 --- a/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt @@ -1,7 +1,7 @@ package io.github.sds100.keymapper.trigger import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index b14e766002..bb55b6b0fe 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -48,7 +48,7 @@ diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt index 1d2befbf9c..fb9e67c239 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt @@ -19,12 +19,12 @@ import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCase import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCaseImpl import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCaseImpl -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCase -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCaseImpl +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCaseImpl import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCaseImpl -import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCaseImpl +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.logging.DisplayLogUseCase import io.github.sds100.keymapper.base.logging.DisplayLogUseCaseImpl import io.github.sds100.keymapper.base.promode.ProModeSetupUseCase diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapModel.kt similarity index 80% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapModel.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapModel.kt index 127f2021dd..321bce0664 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.keymaps.KeyMap diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt similarity index 99% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt index 44c129a77f..a4225691e0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import android.accessibilityservice.AccessibilityService import android.os.SystemClock diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectScreenOffKeyEventsController.kt similarity index 95% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/DetectScreenOffKeyEventsController.kt index 6d37ac98ff..8356d6f665 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectScreenOffKeyEventsController.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.KMKeyEvent diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DpadMotionEventTracker.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/DpadMotionEventTracker.kt index d03e31b301..0f51a065a9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DpadMotionEventTracker.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import android.view.InputDevice import android.view.KeyEvent diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt similarity index 99% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index ca5616c05e..5a2976ae66 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import android.view.KeyEvent import androidx.collection.SparseArrayCompat diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt index a6eac5f71f..a84443cc4a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapDetectionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyPressedCallback.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyPressedCallback.kt similarity index 62% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyPressedCallback.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/KeyPressedCallback.kt index cada80083c..3b8377b56a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyPressedCallback.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyPressedCallback.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection interface KeyPressedCallback { fun onDownEvent(button: T) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt similarity index 99% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt index 532ada958b..228b9a038f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.ActionData diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt similarity index 95% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt index 92227fec03..dc7d40816b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.PerformActionsUseCase diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt similarity index 95% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt index aa15212ed8..7d84d5c76d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt @@ -1,11 +1,11 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.actions.RepeatMode import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.constraints.isSatisfied -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.data.PreferenceDefaults import kotlinx.coroutines.CoroutineScope @@ -30,27 +30,27 @@ abstract class SimpleMappingController( private val defaultRepeatRate: StateFlow = performActionsUseCase.defaultRepeatRate.stateIn( coroutineScope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, PreferenceDefaults.REPEAT_RATE.toLong(), ) private val forceVibrate: StateFlow = detectMappingUseCase.forceVibrate.stateIn( coroutineScope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, PreferenceDefaults.FORCE_VIBRATE, ) private val defaultHoldDownDuration: StateFlow = performActionsUseCase.defaultHoldDownDuration.stateIn( coroutineScope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, PreferenceDefaults.HOLD_DOWN_DURATION.toLong(), ) private val defaultVibrateDuration: StateFlow = detectMappingUseCase.defaultVibrateDuration.stateIn( coroutineScope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, PreferenceDefaults.VIBRATION_DURATION.toLong(), ) @@ -194,4 +194,4 @@ abstract class SimpleMappingController( } private class RepeatJob(val actionUid: String, launch: () -> Job) : Job by launch.invoke() -} +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt similarity index 91% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt index c9d04b1f0d..1f76b46fa8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt @@ -1,9 +1,9 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.keymaps.KeyMap -import io.github.sds100.keymapper.base.keymaps.SimpleMappingController +import io.github.sds100.keymapper.base.detection.SimpleMappingController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/BaseHomeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/BaseHomeViewModel.kt index 47e7d65a73..a1cc19ffde 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/BaseHomeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/BaseHomeViewModel.kt @@ -4,8 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCase -import io.github.sds100.keymapper.base.keymaps.KeyMapListViewModel -import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt index 1b4175e7c8..a739247f72 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt @@ -59,9 +59,6 @@ import io.github.sds100.keymapper.base.backup.RestoreType import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.groups.GroupListItemModel -import io.github.sds100.keymapper.base.keymaps.KeyMapAppBarState -import io.github.sds100.keymapper.base.keymaps.KeyMapList -import io.github.sds100.keymapper.base.keymaps.KeyMapListViewModel import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.sorting.SortBottomSheet import io.github.sds100.keymapper.base.trigger.DpadTriggerSetupBottomSheet diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapAppBarState.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapAppBarState.kt similarity index 86% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapAppBarState.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapAppBarState.kt index d2b56debf0..3d61ec8460 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapAppBarState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapAppBarState.kt @@ -1,9 +1,7 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.groups.GroupListItemModel -import io.github.sds100.keymapper.base.home.HomeWarningListItem -import io.github.sds100.keymapper.base.home.SelectedKeyMapsEnabled import io.github.sds100.keymapper.base.utils.ui.compose.ComposeChipModel sealed class KeyMapAppBarState { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapGroup.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapGroup.kt similarity index 72% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapGroup.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapGroup.kt index 8ddf927570..79d33b8888 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapGroup.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapGroup.kt @@ -1,6 +1,7 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import io.github.sds100.keymapper.base.groups.Group +import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.common.utils.State data class KeyMapGroup( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt index b85c8e1c4e..23b20e240a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt @@ -99,7 +99,6 @@ import io.github.sds100.keymapper.base.groups.GroupBreadcrumbRow import io.github.sds100.keymapper.base.groups.GroupConstraintRow import io.github.sds100.keymapper.base.groups.GroupListItemModel import io.github.sds100.keymapper.base.groups.GroupRow -import io.github.sds100.keymapper.base.keymaps.KeyMapAppBarState import io.github.sds100.keymapper.base.utils.ui.compose.ComposeChipModel import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt index 572f70bcd9..a2f242f8bd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowForward @@ -9,6 +9,9 @@ import io.github.sds100.keymapper.base.actions.ActionUiHelper import io.github.sds100.keymapper.base.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt similarity index 99% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListScreen.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt index c18452592d..daa157dcda 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListState.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListState.kt similarity index 86% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListState.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListState.kt index 16189cadba..6c5425e0a6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListState.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.common.utils.State diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt similarity index 99% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt index 90c49ecea1..e25e99118c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -14,9 +14,8 @@ import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper import io.github.sds100.keymapper.base.groups.Group import io.github.sds100.keymapper.base.groups.GroupFamily import io.github.sds100.keymapper.base.groups.GroupListItemModel -import io.github.sds100.keymapper.base.home.HomeWarningListItem -import io.github.sds100.keymapper.base.home.SelectedKeyMapsEnabled -import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ListKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/ListKeyMapsUseCase.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt index d1dc791061..2ff0ebd8dd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ListKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import android.database.sqlite.SQLiteConstraintException import io.github.sds100.keymapper.base.R @@ -12,6 +12,9 @@ import io.github.sds100.keymapper.base.constraints.ConstraintModeEntityMapper import io.github.sds100.keymapper.base.groups.Group import io.github.sds100.keymapper.base.groups.GroupEntityMapper import io.github.sds100.keymapper.base.groups.GroupFamily +import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.keymaps.KeyMapEntityMapper import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt index 9067eabe14..81df0fada8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt @@ -4,6 +4,7 @@ import android.graphics.Color import android.graphics.drawable.Drawable import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.ActionUiHelper +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.DialogModel import io.github.sds100.keymapper.base.utils.ui.DialogProvider @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlin.collections.first class ConfigKeyMapOptionsViewModel( private val coroutineScope: CoroutineScope, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt index 60ca8f1574..5e71b2573d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt @@ -56,6 +56,7 @@ import kotlinx.serialization.json.Json import java.util.LinkedList import javax.inject.Inject import javax.inject.Singleton +import kotlin.collections.map @Singleton class ConfigKeyMapUseCaseController @Inject constructor( @@ -370,7 +371,7 @@ class ConfigKeyMapUseCaseController @Inject constructor( val containsKey = trigger.keys .filterIsInstance() .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device?.isSameDevice(device) == true + keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) } var consumeKeyEvent = true diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt index 3105e02eb9..633055dba5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt @@ -8,7 +8,7 @@ import io.github.sds100.keymapper.base.actions.canBeHeldDown import io.github.sds100.keymapper.base.constraints.ConstraintEntityMapper import io.github.sds100.keymapper.base.constraints.ConstraintModeEntityMapper import io.github.sds100.keymapper.base.constraints.ConstraintState -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapAlgorithm +import io.github.sds100.keymapper.base.detection.KeyMapAlgorithm import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerEntityMapper import io.github.sds100.keymapper.base.trigger.TriggerKey diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutActivity.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt rename to base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutActivity.kt index c4fab0338a..7448cc5099 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutActivity.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.shortcuts import android.os.Bundle import androidx.activity.SystemBarStyle diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutScreen.kt rename to base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt index 366566c45a..ed431008fb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.shortcuts import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent @@ -42,6 +42,9 @@ import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.groups.GroupBreadcrumbRow import io.github.sds100.keymapper.base.groups.GroupListItemModel import io.github.sds100.keymapper.base.groups.GroupRow +import io.github.sds100.keymapper.base.home.KeyMapList +import io.github.sds100.keymapper.base.home.KeyMapListState +import io.github.sds100.keymapper.base.home.KeyMapAppBarState import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.TriggerError import io.github.sds100.keymapper.base.utils.ui.UnsavedChangesDialog diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutUseCase.kt rename to base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt index 563b7bff63..d69f15c5c5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.shortcuts import android.content.Intent import android.graphics.drawable.Drawable diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt similarity index 95% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutViewModel.kt rename to base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt index 086e330619..9bbd2825da 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.shortcuts import android.content.Intent import android.graphics.Color @@ -15,6 +15,12 @@ import io.github.sds100.keymapper.base.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper import io.github.sds100.keymapper.base.groups.GroupListItemModel +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase +import io.github.sds100.keymapper.base.home.KeyMapListItemCreator +import io.github.sds100.keymapper.base.home.KeyMapListState +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase +import io.github.sds100.keymapper.base.home.KeyMapAppBarState +import io.github.sds100.keymapper.base.home.KeyMapGroup import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.base.utils.ui.ResourceProvider diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 0bbef14720..2587e35a22 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -18,9 +18,9 @@ import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapDetectionController -import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.base.detection.KeyMapDetectionController +import io.github.sds100.keymapper.base.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.common.utils.firstBlocking diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 3251149d22..55d752f151 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -13,7 +13,7 @@ import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapOptionsViewModel import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.KeyMap diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 76550d7dd1..8a9962d97e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -4,7 +4,7 @@ import android.view.KeyEvent import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.input.InputEventHubCallback -import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker +import io.github.sds100.keymapper.base.detection.DpadMotionEventTracker import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.isError diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt index 226b93841f..c84d864907 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/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.base.keymaps.chipHeight +import io.github.sds100.keymapper.base.home.chipHeight @Composable fun CompactChip( diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt index b91700dec6..0cf0ab5c52 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt @@ -2,7 +2,7 @@ package io.github.sds100.keymapper.base.keymaps import android.view.InputDevice import android.view.KeyEvent -import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker +import io.github.sds100.keymapper.base.detection.DpadMotionEventTracker import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 4e2e68c4f9..de7837091f 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -13,9 +13,9 @@ import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapAlgorithm +import io.github.sds100.keymapper.base.detection.DetectKeyMapModel +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.base.detection.KeyMapAlgorithm import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt index abcd815259..64151f4738 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt @@ -4,8 +4,8 @@ import io.github.sds100.keymapper.base.constraints.Constraint import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.groups.Group -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.base.detection.DetectKeyMapModel +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.junit.Test diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt index 4b95335488..44cce647b7 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt @@ -6,8 +6,8 @@ import io.github.sds100.keymapper.base.actions.ActionErrorSnapshot import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.actions.RepeatMode import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.base.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.utils.TestConstraintSnapshot import io.github.sds100.keymapper.common.utils.KMError From fa72515532dbeaaa4b643e504a4ed2c5244e35e3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 21:35:04 +0100 Subject: [PATCH 093/215] refactor: split up ConfigKeyMapUseCase into separate classes --- .../github/sds100/keymapper/MainFragment.kt | 79 +- .../keymapper/keymaps/ConfigKeyMapScreen.kt | 81 -- .../keymaps/ConfigKeyMapViewModel.kt | 81 -- .../trigger/ConfigTriggerViewModel.kt | 11 +- .../keymapper/base/BaseSingletonHiltModule.kt | 18 +- .../keymapper/base/BaseViewModelHiltModule.kt | 36 +- .../base/actions/ConfigActionsUseCase.kt | 287 +++++ .../base/actions/ConfigActionsViewModel.kt | 40 +- .../constraints/ConfigConstraintsUseCase.kt | 126 ++ .../constraints/ConfigConstraintsViewModel.kt | 29 +- .../base/keymaps/BaseConfigKeyMapScreen.kt | 18 +- .../keymaps/ConfigKeyMapOptionsViewModel.kt | 4 +- .../base/keymaps/ConfigKeyMapScreen.kt | 61 + .../base/keymaps/ConfigKeyMapState.kt | 117 ++ .../base/keymaps/ConfigKeyMapUseCase.kt | 1114 ----------------- ...pViewModel.kt => ConfigKeyMapViewModel.kt} | 43 +- .../keymaps/GetDefaultKeyMapOptionsUseCase.kt | 5 +- .../shortcuts/CreateKeyMapShortcutUseCase.kt | 4 +- .../CreateKeyMapShortcutViewModel.kt | 18 +- .../trigger/BaseConfigTriggerViewModel.kt | 36 +- .../base/trigger/ConfigTriggerUseCase.kt | 629 ++++++++++ .../keymapper/base/ConfigKeyMapUseCaseTest.kt | 6 +- 22 files changed, 1440 insertions(+), 1403 deletions(-) delete mode 100644 app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt delete mode 100644 app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt rename base/src/main/java/io/github/sds100/keymapper/base/keymaps/{BaseConfigKeyMapViewModel.kt => ConfigKeyMapViewModel.kt} (76%) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt b/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt index ccd1e61eee..4405f5e21f 100644 --- a/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt @@ -24,20 +24,26 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.BaseMainNavHost +import io.github.sds100.keymapper.base.actions.ActionsScreen import io.github.sds100.keymapper.base.actions.ChooseActionScreen import io.github.sds100.keymapper.base.actions.ChooseActionViewModel +import io.github.sds100.keymapper.base.actions.ConfigActionsViewModel import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsViewModel +import io.github.sds100.keymapper.base.constraints.ConstraintsScreen import io.github.sds100.keymapper.base.databinding.FragmentComposeBinding import io.github.sds100.keymapper.base.home.HomeKeyMapListScreen +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapScreen +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapViewModel +import io.github.sds100.keymapper.base.keymaps.KeyMapOptionsScreen import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProviderImpl import io.github.sds100.keymapper.base.utils.navigation.SetupNavigation import io.github.sds100.keymapper.base.utils.navigation.handleRouteArgs import io.github.sds100.keymapper.base.utils.navigation.setupFragmentNavigation -import io.github.sds100.keymapper.base.utils.ui.DialogProviderImpl import io.github.sds100.keymapper.home.HomeViewModel -import io.github.sds100.keymapper.keymaps.ConfigKeyMapScreen -import io.github.sds100.keymapper.keymaps.ConfigKeyMapViewModel +import io.github.sds100.keymapper.trigger.ConfigTriggerViewModel +import io.github.sds100.keymapper.trigger.TriggerScreen import javax.inject.Inject @AndroidEntryPoint @@ -46,9 +52,6 @@ class MainFragment : Fragment() { @Inject lateinit var navigationProvider: NavigationProviderImpl - @Inject - lateinit var dialogProvider: DialogProviderImpl - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -107,36 +110,84 @@ class MainFragment : Fragment() { } composable { backStackEntry -> - val viewModel: ConfigKeyMapViewModel = hiltViewModel() + val keyMapViewModel: ConfigKeyMapViewModel = hiltViewModel() + val triggerViewModel: ConfigTriggerViewModel = hiltViewModel() + val actionsViewModel: ConfigActionsViewModel = hiltViewModel() + val constraintsViewModel: ConfigConstraintsViewModel = hiltViewModel() + val snackbarHostState = remember { SnackbarHostState() } backStackEntry.handleRouteArgs { args -> - viewModel.loadNewKeyMap(groupUid = args.groupUid) + keyMapViewModel.loadNewKeyMap(groupUid = args.groupUid) if (args.showAdvancedTriggers) { - viewModel.configTriggerViewModel.showAdvancedTriggersBottomSheet = true + triggerViewModel.showAdvancedTriggersBottomSheet = true } } ConfigKeyMapScreen( modifier = Modifier.fillMaxSize(), - viewModel = viewModel, + snackbarHostState = snackbarHostState, + keyMapViewModel = keyMapViewModel, + triggerScreen = { + TriggerScreen(Modifier.fillMaxSize(), triggerViewModel) + }, + actionsScreen = { + ActionsScreen(Modifier.fillMaxSize(), actionsViewModel) + }, + constraintsScreen = { + ConstraintsScreen( + Modifier.fillMaxSize(), + constraintsViewModel, + snackbarHostState, + ) + }, + optionsScreen = { + KeyMapOptionsScreen( + Modifier.fillMaxSize(), + triggerViewModel.optionsViewModel, + ) + }, ) } composable { backStackEntry -> - val viewModel: ConfigKeyMapViewModel = hiltViewModel() + val keyMapViewModel: ConfigKeyMapViewModel = hiltViewModel() + val triggerViewModel: ConfigTriggerViewModel = hiltViewModel() + val actionsViewModel: ConfigActionsViewModel = hiltViewModel() + val constraintsViewModel: ConfigConstraintsViewModel = hiltViewModel() + val snackbarHostState = remember { SnackbarHostState() } backStackEntry.handleRouteArgs { args -> - viewModel.loadKeyMap(uid = args.keyMapUid) + keyMapViewModel.loadKeyMap(uid = args.keyMapUid) if (args.showAdvancedTriggers) { - viewModel.configTriggerViewModel.showAdvancedTriggersBottomSheet = true + triggerViewModel.showAdvancedTriggersBottomSheet = true } } ConfigKeyMapScreen( modifier = Modifier.fillMaxSize(), - viewModel = viewModel, + snackbarHostState = snackbarHostState, + keyMapViewModel = keyMapViewModel, + triggerScreen = { + TriggerScreen(Modifier.fillMaxSize(), triggerViewModel) + }, + actionsScreen = { + ActionsScreen(Modifier.fillMaxSize(), actionsViewModel) + }, + constraintsScreen = { + ConstraintsScreen( + Modifier.fillMaxSize(), + constraintsViewModel, + snackbarHostState, + ) + }, + optionsScreen = { + KeyMapOptionsScreen( + Modifier.fillMaxSize(), + triggerViewModel.optionsViewModel, + ) + }, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt deleted file mode 100644 index 61e36a3624..0000000000 --- a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.sds100.keymapper.keymaps - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.github.sds100.keymapper.base.actions.ActionsScreen -import io.github.sds100.keymapper.base.constraints.ConstraintsScreen -import io.github.sds100.keymapper.base.keymaps.BaseConfigKeyMapScreen -import io.github.sds100.keymapper.base.keymaps.KeyMapOptionsScreen -import io.github.sds100.keymapper.base.utils.ui.UnsavedChangesDialog -import io.github.sds100.keymapper.trigger.TriggerScreen - -@Composable -fun ConfigKeyMapScreen( - modifier: Modifier = Modifier, - viewModel: ConfigKeyMapViewModel, -) { - 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) } - - if (showBackDialog) { - UnsavedChangesDialog( - onDismiss = { showBackDialog = false }, - onDiscardClick = { - showBackDialog = false - viewModel.onBackClick() - }, - ) - } - - BaseConfigKeyMapScreen( - modifier = modifier, - isKeyMapEnabled = isKeyMapEnabled, - onKeyMapEnabledChange = viewModel::onEnabledChanged, - triggerScreen = { - TriggerScreen(Modifier.fillMaxSize(), viewModel.configTriggerViewModel) - }, - actionScreen = { - ActionsScreen(Modifier.fillMaxSize(), viewModel.configActionsViewModel) - }, - constraintsScreen = { - ConstraintsScreen( - Modifier.fillMaxSize(), - viewModel.configConstraintsViewModel, - snackbarHostState, - ) - }, - optionsScreen = { - KeyMapOptionsScreen( - Modifier.fillMaxSize(), - viewModel.configTriggerViewModel.optionsViewModel, - ) - }, - onBackClick = { - if (viewModel.isKeyMapEdited) { - showBackDialog = true - } else { - viewModel.onBackClick() - } - }, - onDoneClick = viewModel::onDoneClick, - snackbarHostState = snackbarHostState, - showActionTapTarget = showActionTapTarget, - onActionTapTargetCompleted = viewModel::onActionTapTargetCompleted, - showConstraintTapTarget = showConstraintTapTarget, - onConstraintTapTargetCompleted = viewModel::onConstraintTapTargetCompleted, - onSkipTutorialClick = viewModel::onSkipTutorialClick, - ) -} diff --git a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt deleted file mode 100644 index 7453ad616c..0000000000 --- a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.sds100.keymapper.keymaps - -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import io.github.sds100.keymapper.base.actions.ConfigActionsViewModel -import io.github.sds100.keymapper.base.actions.CreateActionUseCase -import io.github.sds100.keymapper.base.actions.TestActionUseCase -import io.github.sds100.keymapper.base.constraints.ConfigConstraintsViewModel -import io.github.sds100.keymapper.base.keymaps.BaseConfigKeyMapViewModel -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase -import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase -import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase -import io.github.sds100.keymapper.base.purchasing.PurchasingManager -import io.github.sds100.keymapper.base.trigger.RecordTriggerController -import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCase -import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider -import io.github.sds100.keymapper.base.utils.ui.DialogProvider -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.trigger.ConfigTriggerViewModel -import javax.inject.Inject - -@HiltViewModel -class ConfigKeyMapViewModel @Inject constructor( - display: DisplayKeyMapUseCase, - config: ConfigKeyMapUseCase, - onboarding: OnboardingUseCase, - createActionUseCase: CreateActionUseCase, - testActionUseCase: TestActionUseCase, - recordTriggerController: RecordTriggerController, - createKeyMapShortcutUseCase: CreateKeyMapShortcutUseCase, - purchasingManager: PurchasingManager, - setupGuiKeyboardUseCase: SetupGuiKeyboardUseCase, - fingerprintGesturesSupportedUseCase: FingerprintGesturesSupportedUseCase, - resourceProvider: ResourceProvider, - navigationProvider: NavigationProvider, - dialogProvider: DialogProvider, -) : BaseConfigKeyMapViewModel( - config = config, - onboarding = onboarding, - navigationProvider = navigationProvider, - dialogProvider = dialogProvider, -) { - override val configActionsViewModel: ConfigActionsViewModel = ConfigActionsViewModel( - coroutineScope = viewModelScope, - displayAction = display, - createAction = createActionUseCase, - testAction = testActionUseCase, - config = config, - onboarding = onboarding, - resourceProvider = resourceProvider, - navigationProvider = navigationProvider, - dialogProvider = dialogProvider, - ) - - override val configTriggerViewModel: ConfigTriggerViewModel = ConfigTriggerViewModel( - coroutineScope = viewModelScope, - onboarding = onboarding, - config = config, - recordTrigger = recordTriggerController, - createKeyMapShortcut = createKeyMapShortcutUseCase, - displayKeyMap = display, - purchasingManager = purchasingManager, - setupGuiKeyboard = setupGuiKeyboardUseCase, - fingerprintGesturesSupported = fingerprintGesturesSupportedUseCase, - resourceProvider = resourceProvider, - navigationProvider = navigationProvider, - dialogProvider = dialogProvider, - ) - - override val configConstraintsViewModel: ConfigConstraintsViewModel = - ConfigConstraintsViewModel( - coroutineScope = viewModelScope, - config = config, - displayConstraint = display, - resourceProvider = resourceProvider, - navigationProvider = navigationProvider, - dialogProvider = dialogProvider, - ) -} diff --git a/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt index f6b8ba1781..b1dd42b073 100644 --- a/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt @@ -1,24 +1,24 @@ package io.github.sds100.keymapper.trigger -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.PurchasingManager +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.trigger.BaseConfigTriggerViewModel +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import kotlinx.coroutines.CoroutineScope import javax.inject.Inject +@HiltViewModel class ConfigTriggerViewModel @Inject constructor( - private val coroutineScope: CoroutineScope, private val onboarding: OnboardingUseCase, - private val config: ConfigKeyMapUseCase, + private val config: ConfigTriggerUseCase, private val recordTrigger: RecordTriggerController, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val displayKeyMap: DisplayKeyMapUseCase, @@ -29,7 +29,6 @@ class ConfigTriggerViewModel @Inject constructor( navigationProvider: NavigationProvider, dialogProvider: DialogProvider, ) : BaseConfigTriggerViewModel( - coroutineScope = coroutineScope, onboarding = onboarding, config = config, recordTrigger = recordTrigger, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt index f3904c84b3..abade43b31 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt @@ -16,10 +16,12 @@ import io.github.sds100.keymapper.base.constraints.GetConstraintErrorUseCase import io.github.sds100.keymapper.base.constraints.GetConstraintErrorUseCaseImpl import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.input.InputEventHubImpl -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCaseController +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCaseImpl +import io.github.sds100.keymapper.base.keymaps.GetDefaultKeyMapOptionsUseCase +import io.github.sds100.keymapper.base.keymaps.GetDefaultKeyMapOptionsUseCaseImpl import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase @@ -107,10 +109,6 @@ abstract class BaseSingletonHiltModule { @Singleton abstract fun bindSoundsManager(impl: SoundsManagerImpl): SoundsManager - @Binds - @Singleton - abstract fun bindConfigKeyMapUseCase(impl: ConfigKeyMapUseCaseController): ConfigKeyMapUseCase - @Binds @Singleton abstract fun bindRecordTriggerUseCase(impl: RecordTriggerControllerImpl): RecordTriggerController @@ -158,4 +156,12 @@ abstract class BaseSingletonHiltModule { @Binds @Singleton abstract fun rerouteKeyEventsUseCase(impl: RerouteKeyEventsUseCaseImpl): RerouteKeyEventsUseCase + + @Binds + @Singleton + abstract fun bindConfigKeyMapState(impl: ConfigKeyMapStateImpl): ConfigKeyMapState + + @Binds + @Singleton + abstract fun bindGetDefaultKeyMapOptionsUseCas(impl: GetDefaultKeyMapOptionsUseCaseImpl): GetDefaultKeyMapOptionsUseCase } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt index fb9e67c239..31f7d1133f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt @@ -5,8 +5,11 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.actions.ConfigActionsUseCase +import io.github.sds100.keymapper.base.actions.ConfigActionsUseCaseImpl import io.github.sds100.keymapper.base.actions.CreateActionUseCase import io.github.sds100.keymapper.base.actions.CreateActionUseCaseImpl +import io.github.sds100.keymapper.base.actions.DisplayActionUseCase import io.github.sds100.keymapper.base.actions.TestActionUseCase import io.github.sds100.keymapper.base.actions.TestActionUseCaseImpl import io.github.sds100.keymapper.base.actions.keyevent.ConfigKeyEventUseCase @@ -15,22 +18,25 @@ import io.github.sds100.keymapper.base.actions.sound.ChooseSoundFileUseCase import io.github.sds100.keymapper.base.actions.sound.ChooseSoundFileUseCaseImpl import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCase import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCaseImpl +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCase +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCaseImpl import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCase import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCaseImpl +import io.github.sds100.keymapper.base.constraints.DisplayConstraintUseCase +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCaseImpl -import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase -import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCaseImpl import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCaseImpl -import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase -import io.github.sds100.keymapper.base.home.ListKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.logging.DisplayLogUseCase import io.github.sds100.keymapper.base.logging.DisplayLogUseCaseImpl import io.github.sds100.keymapper.base.promode.ProModeSetupUseCase import io.github.sds100.keymapper.base.promode.ProModeSetupUseCaseImpl import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCase import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCaseImpl +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCaseImpl import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.system.apps.DisplayAppShortcutsUseCase @@ -39,6 +45,8 @@ import io.github.sds100.keymapper.base.system.apps.DisplayAppsUseCase import io.github.sds100.keymapper.base.system.apps.DisplayAppsUseCaseImpl import io.github.sds100.keymapper.base.system.bluetooth.ChooseBluetoothDeviceUseCase import io.github.sds100.keymapper.base.system.bluetooth.ChooseBluetoothDeviceUseCaseImpl +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCaseImpl import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCaseImpl @@ -49,6 +57,14 @@ abstract class BaseViewModelHiltModule { @ViewModelScoped abstract fun bindDisplayKeyMapUseCase(impl: DisplayKeyMapUseCaseImpl): DisplayKeyMapUseCase + @Binds + @ViewModelScoped + abstract fun bindDisplayActionUseCase(impl: DisplayKeyMapUseCaseImpl): DisplayActionUseCase + + @Binds + @ViewModelScoped + abstract fun bindDisplayConstraintUseCase(impl: DisplayKeyMapUseCaseImpl): DisplayConstraintUseCase + @Binds @ViewModelScoped abstract fun bindListKeyMapsUseCase(impl: ListKeyMapsUseCaseImpl): ListKeyMapsUseCase @@ -116,4 +132,16 @@ abstract class BaseViewModelHiltModule { @Binds @ViewModelScoped abstract fun bindProModeSetupUseCase(impl: ProModeSetupUseCaseImpl): ProModeSetupUseCase + + @Binds + @ViewModelScoped + abstract fun bindConfigConstraintsUseCase(impl: ConfigConstraintsUseCaseImpl): ConfigConstraintsUseCase + + @Binds + @ViewModelScoped + abstract fun bindConfigActionsUseCase(impl: ConfigActionsUseCaseImpl): ConfigActionsUseCase + + @Binds + @ViewModelScoped + abstract fun bindConfigTriggerUseCase(impl: ConfigTriggerUseCaseImpl): ConfigTriggerUseCase } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt new file mode 100644 index 0000000000..cf0362d682 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt @@ -0,0 +1,287 @@ +package io.github.sds100.keymapper.base.actions + +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCase +import io.github.sds100.keymapper.base.constraints.Constraint +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.keymaps.GetDefaultKeyMapOptionsUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.moveElement +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.util.LinkedList +import javax.inject.Inject + +@ViewModelScoped +class ConfigActionsUseCaseImpl @Inject constructor( + private val state: ConfigKeyMapState, + private val preferenceRepository: PreferenceRepository, + private val configConstraints: ConfigConstraintsUseCase, + defaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase +) : ConfigActionsUseCase, GetDefaultKeyMapOptionsUseCase by defaultKeyMapOptionsUseCase { + + override val keyMap: StateFlow> = state.keyMap + + /** + * The most recently used is first. + */ + override val recentlyUsedActions: Flow> = + preferenceRepository.get(Keys.recentlyUsedActions) + .map(::getActionShortcuts) + .map { it.take(5) } + + override fun addAction(data: ActionData) { + state.update { keyMap -> + val newActionList = keyMap.actionList.toMutableList().apply { + add(createAction(keyMap, data)) + } + + preferenceRepository.update( + Keys.recentlyUsedActions, + { old -> + val oldList: List = if (old == null) { + emptyList() + } else { + Json.decodeFromString>(old) + } + + val newShortcuts = LinkedList(oldList) + .also { it.addFirst(data) } + .distinct() + + Json.encodeToString(newShortcuts) + }, + ) + + keyMap.copy(actionList = newActionList) + } + + } + + override fun moveAction(fromIndex: Int, toIndex: Int) { + updateActionList { actionList -> + actionList.toMutableList().apply { + moveElement(fromIndex, toIndex) + } + } + } + + override fun removeAction(uid: String) { + updateActionList { actionList -> + actionList.toMutableList().apply { + removeAll { it.uid == uid } + } + } + } + + override fun setActionData(uid: String, data: ActionData) { + updateActionList { actionList -> + actionList.map { action -> + if (action.uid == uid) { + action.copy(data = data) + } else { + action + } + } + } + } + + override fun setActionRepeatEnabled(uid: String, repeat: Boolean) { + setActionOption(uid) { action -> action.copy(repeat = repeat) } + } + + override fun setActionRepeatRate(uid: String, repeatRate: Int) { + setActionOption(uid) { action -> + if (repeatRate == defaultRepeatRate.value) { + action.copy(repeatRate = null) + } else { + action.copy(repeatRate = repeatRate) + } + } + } + + override fun setActionRepeatDelay(uid: String, repeatDelay: Int) { + setActionOption(uid) { action -> + if (repeatDelay == defaultRepeatDelay.value) { + action.copy(repeatDelay = null) + } else { + action.copy(repeatDelay = repeatDelay) + } + } + } + + override fun setActionRepeatLimit(uid: String, repeatLimit: Int) { + setActionOption(uid) { action -> + if (action.repeatMode == RepeatMode.LIMIT_REACHED) { + if (repeatLimit == 1) { + action.copy(repeatLimit = null) + } else { + action.copy(repeatLimit = repeatLimit) + } + } else { + if (repeatLimit == Int.MAX_VALUE) { + action.copy(repeatLimit = null) + } else { + action.copy(repeatLimit = repeatLimit) + } + } + } + } + + override fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) = + setActionOption(uid) { it.copy(holdDown = holdDown) } + + override fun setActionHoldDownDuration(uid: String, holdDownDuration: Int) { + setActionOption(uid) { action -> + if (holdDownDuration == defaultHoldDownDuration.value) { + action.copy(holdDownDuration = null) + } else { + action.copy(holdDownDuration = holdDownDuration) + } + } + } + + override fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) = + setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN) } + + override fun setActionStopRepeatingWhenLimitReached(uid: String) = + setActionOption(uid) { it.copy(repeatMode = RepeatMode.LIMIT_REACHED) } + + override fun setActionStopRepeatingWhenTriggerReleased(uid: String) = + setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_RELEASED) } + + override fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) = + setActionOption(uid) { it.copy(stopHoldDownWhenTriggerPressedAgain = enabled) } + + override fun setActionMultiplier(uid: String, multiplier: Int) { + setActionOption(uid) { action -> + if (multiplier == 1) { + action.copy(multiplier = null) + } else { + action.copy(multiplier = multiplier) + } + } + } + + override fun setDelayBeforeNextAction(uid: String, delay: Int) { + setActionOption(uid) { action -> + if (delay == 0) { + action.copy(delayBeforeNextAction = null) + } else { + action.copy(delayBeforeNextAction = delay) + } + } + } + + private suspend fun getActionShortcuts(json: String?): List { + if (json == null) { + return emptyList() + } + + try { + return withContext(Dispatchers.Default) { + val list = Json.decodeFromString>(json) + + list.distinct() + } + } catch (_: Exception) { + preferenceRepository.set(Keys.recentlyUsedActions, null) + return emptyList() + } + } + + private fun createAction(keyMap: KeyMap, data: ActionData): Action { + var holdDown = false + var repeat = false + + if (data is ActionData.InputKeyEvent) { + val containsDpadKey: Boolean = + keyMap.trigger.keys + .mapNotNull { it as? KeyEventTriggerKey } + .any { KeyEventUtils.isDpadKeyCode(it.keyCode) } + + if (KeyEventUtils.isModifierKey(data.keyCode) || containsDpadKey) { + holdDown = true + repeat = false + } else { + repeat = true + } + } + + if (data is ActionData.Volume.Down || data is ActionData.Volume.Up || data is ActionData.Volume.Stream) { + repeat = true + } + + if (data is ActionData.AnswerCall) { + configConstraints.addConstraint(Constraint.PhoneRinging()) + } + + if (data is ActionData.EndCall) { + configConstraints.addConstraint(Constraint.InPhoneCall()) + } + + return Action( + data = data, + repeat = repeat, + holdDown = holdDown, + ) + } + + private fun updateActionList(block: (actionList: List) -> List) { + state.update { it.copy(actionList = block(it.actionList)) } + } + + private fun setActionOption( + uid: String, + block: (action: Action) -> Action, + ) { + state.update { keyMap -> + val newActionList = keyMap.actionList.map { action -> + if (action.uid == uid) { + block.invoke(action) + } else { + action + } + } + + keyMap.copy( + actionList = newActionList, + ) + } + } + +} + +interface ConfigActionsUseCase : GetDefaultKeyMapOptionsUseCase { + val keyMap: StateFlow> + + fun addAction(data: ActionData) + fun moveAction(fromIndex: Int, toIndex: Int) + fun removeAction(uid: String) + + val recentlyUsedActions: Flow> + fun setActionData(uid: String, data: ActionData) + fun setActionMultiplier(uid: String, multiplier: Int) + fun setDelayBeforeNextAction(uid: String, delay: Int) + fun setActionRepeatRate(uid: String, repeatRate: Int) + fun setActionRepeatLimit(uid: String, repeatLimit: Int) + fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) + fun setActionStopRepeatingWhenLimitReached(uid: String) + fun setActionRepeatEnabled(uid: String, repeat: Boolean) + fun setActionRepeatDelay(uid: String, repeatDelay: Int) + fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) + fun setActionHoldDownDuration(uid: String, holdDownDuration: Int) + fun setActionStopRepeatingWhenTriggerReleased(uid: String) + + fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) + +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index 0e1a4ca5d2..be2a3d94e6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -1,7 +1,9 @@ package io.github.sds100.keymapper.base.actions +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.keymaps.ShortcutModel import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase @@ -26,7 +28,6 @@ import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -39,24 +40,25 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import javax.inject.Inject -class ConfigActionsViewModel( - private val coroutineScope: CoroutineScope, +@HiltViewModel +class ConfigActionsViewModel @Inject constructor( private val displayAction: DisplayActionUseCase, private val createAction: CreateActionUseCase, private val testAction: TestActionUseCase, - private val config: ConfigKeyMapUseCase, + private val config: ConfigActionsUseCase, private val onboarding: OnboardingUseCase, resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, -) : ActionOptionsBottomSheetCallback, +) : ViewModel(), ActionOptionsBottomSheetCallback, ResourceProvider by resourceProvider, DialogProvider by dialogProvider, NavigationProvider by navigationProvider { val createActionDelegate = - CreateActionDelegate(coroutineScope, createAction, this, this, this) + CreateActionDelegate(viewModelScope, createAction, this, this, this) private val uiHelper = ActionUiHelper(displayAction, resourceProvider) private val _state = MutableStateFlow>(State.Loading) @@ -65,15 +67,15 @@ class ConfigActionsViewModel( private val shortcuts: StateFlow>> = config.recentlyUsedActions.map { actions -> actions.map(::buildShortcut).toSet() - }.stateIn(coroutineScope, SharingStarted.Lazily, emptySet()) + }.stateIn(viewModelScope, SharingStarted.Lazily, emptySet()) val actionOptionsUid = MutableStateFlow(null) val actionOptionsState: StateFlow = combine(config.keyMap, actionOptionsUid, transform = ::buildOptionsState) - .stateIn(coroutineScope, SharingStarted.Lazily, null) + .stateIn(viewModelScope, SharingStarted.Lazily, null) private val actionErrorSnapshot: StateFlow = - displayAction.actionErrorSnapshot.stateIn(coroutineScope, SharingStarted.Lazily, null) + displayAction.actionErrorSnapshot.stateIn(viewModelScope, SharingStarted.Lazily, null) init { combine( @@ -85,9 +87,9 @@ class ConfigActionsViewModel( _state.value = keyMapState.mapData { keyMap -> buildState(keyMap, shortcuts, errorSnapshot, showDeviceDescriptors) } - }.launchIn(coroutineScope) + }.launchIn(viewModelScope) - coroutineScope.launch { + viewModelScope.launch { createActionDelegate.actionResult.filterNotNull().collect { action -> val actionUid = actionOptionsUid.value ?: return@collect config.setActionData(actionUid, action) @@ -101,19 +103,19 @@ class ConfigActionsViewModel( } fun onClickShortcut(action: ActionData) { - coroutineScope.launch { + viewModelScope.launch { config.addAction(action) } } fun onFixError(actionUid: String) { - coroutineScope.launch { + viewModelScope.launch { val actionData = getActionData(actionUid) ?: return@launch val error = actionErrorSnapshot.filterNotNull().first().getError(actionData) ?: return@launch if (error == SystemError.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY)) { - coroutineScope.launch { + viewModelScope.launch { ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( resourceProvider = this@ConfigActionsViewModel, dialogProvider = this@ConfigActionsViewModel, @@ -134,7 +136,7 @@ class ConfigActionsViewModel( } fun onAddActionClick() { - coroutineScope.launch { + viewModelScope.launch { val actionData = navigate("add_action", NavDestination.ChooseAction) ?: return@launch val showInstallShizukuPrompt = onboarding.showInstallShizukuPrompt(actionData) @@ -165,7 +167,7 @@ class ConfigActionsViewModel( } fun onTestClick(actionUid: String) { - coroutineScope.launch { + viewModelScope.launch { val actionData = getActionData(actionUid) ?: return@launch attemptTestAction(actionData) } @@ -173,7 +175,7 @@ class ConfigActionsViewModel( override fun onEditClick() { val actionUid = actionOptionsUid.value ?: return - coroutineScope.launch { + viewModelScope.launch { val keyMap = config.keyMap.first().dataOrNull() ?: return@launch val oldAction = keyMap.actionList.find { it.uid == actionUid } ?: return@launch @@ -183,7 +185,7 @@ class ConfigActionsViewModel( override fun onReplaceClick() { val actionUid = actionOptionsUid.value ?: return - coroutineScope.launch { + viewModelScope.launch { val newActionData = navigate("replace_action", NavDestination.ChooseAction) ?: return@launch diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt new file mode 100644 index 0000000000..449597251e --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt @@ -0,0 +1,126 @@ +package io.github.sds100.keymapper.base.constraints + +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.util.LinkedList +import javax.inject.Inject + +@ViewModelScoped +class ConfigConstraintsUseCaseImpl @Inject constructor( + private val state: ConfigKeyMapState, + private val preferenceRepository: PreferenceRepository +) : ConfigConstraintsUseCase { + + override val keyMap: StateFlow> = state.keyMap + + /** + * The most recently used is first. + */ + override val recentlyUsedConstraints: Flow> = + combine( + preferenceRepository.get(Keys.recentlyUsedConstraints).map(::getConstraintShortcuts), + keyMap.filterIsInstance>(), + ) { shortcuts, keyMap -> + + // Do not include constraints that the key map already contains. + shortcuts + .filter { !keyMap.data.constraintState.constraints.contains(it) } + .take(5) + } + + override fun addConstraint(constraint: Constraint): Boolean { + var containsConstraint = false + + updateConstraintState { oldState -> + containsConstraint = oldState.constraints.contains(constraint) + oldState.copy(constraints = oldState.constraints.plus(constraint)) + } + + preferenceRepository.update( + Keys.recentlyUsedConstraints, + { old -> + val oldList: List = if (old == null) { + emptyList() + } else { + Json.decodeFromString>(old) + } + + val newShortcuts = LinkedList(oldList) + .also { it.addFirst(constraint) } + .distinct() + + Json.encodeToString(newShortcuts) + }, + ) + + return !containsConstraint + } + + override fun removeConstraint(id: String) { + updateConstraintState { oldState -> + val newList = oldState.constraints.toMutableSet().apply { + removeAll { it.uid == id } + } + oldState.copy(constraints = newList) + } + } + + override fun setAndMode() { + updateConstraintState { oldState -> + oldState.copy(mode = ConstraintMode.AND) + } + } + + override fun setOrMode() { + updateConstraintState { oldState -> + oldState.copy(mode = ConstraintMode.OR) + } + } + + private fun updateConstraintState(block: (ConstraintState) -> ConstraintState) { + state.update { keyMap -> + keyMap.copy(constraintState = block(keyMap.constraintState)) + } + } + + + private suspend fun getConstraintShortcuts(json: String?): List { + if (json == null) { + return emptyList() + } + + try { + return withContext(Dispatchers.Default) { + val list = Json.decodeFromString>(json) + + list.distinct() + } + } catch (_: Exception) { + preferenceRepository.set(Keys.recentlyUsedConstraints, null) + return emptyList() + } + } + +} + +interface ConfigConstraintsUseCase { + val keyMap: StateFlow> + + val recentlyUsedConstraints: Flow> + fun addConstraint(constraint: Constraint): Boolean + fun removeConstraint(id: String) + fun setAndMode() + fun setOrMode() +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt index a2d941bbbd..85c0b0abbc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt @@ -3,7 +3,9 @@ package io.github.sds100.keymapper.base.constraints import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.keymaps.ShortcutModel import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.isFixable @@ -20,7 +22,6 @@ import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -33,15 +34,17 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject -class ConfigConstraintsViewModel( - private val coroutineScope: CoroutineScope, - private val config: ConfigKeyMapUseCase, + +@HiltViewModel +class ConfigConstraintsViewModel @Inject constructor( + private val config: ConfigConstraintsUseCase, private val displayConstraint: DisplayConstraintUseCase, resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, -) : ResourceProvider by resourceProvider, +) : ViewModel(), ResourceProvider by resourceProvider, DialogProvider by dialogProvider, NavigationProvider by navigationProvider { @@ -54,11 +57,11 @@ class ConfigConstraintsViewModel( private val shortcuts: StateFlow>> = config.recentlyUsedConstraints.map { actions -> actions.map(::buildShortcut).toSet() - }.stateIn(coroutineScope, SharingStarted.Lazily, emptySet()) + }.stateIn(viewModelScope, SharingStarted.Lazily, emptySet()) private val constraintErrorSnapshot: StateFlow = displayConstraint.constraintErrorSnapshot.stateIn( - coroutineScope, + viewModelScope, SharingStarted.Lazily, null, ) @@ -74,11 +77,11 @@ class ConfigConstraintsViewModel( _state.value = keyMapState.mapData { keyMap -> buildState(keyMap.constraintState, shortcuts, errorSnapshot) } - }.launchIn(coroutineScope) + }.launchIn(viewModelScope) } fun onClickShortcut(constraint: Constraint) { - coroutineScope.launch { + viewModelScope.launch { config.addConstraint(constraint) } } @@ -93,7 +96,7 @@ class ConfigConstraintsViewModel( } fun onFixError(constraintUid: String) { - coroutineScope.launch { + viewModelScope.launch { val constraint = config.keyMap .firstOrNull() ?.dataOrNull() @@ -106,7 +109,7 @@ class ConfigConstraintsViewModel( ?: return@launch if (error == SystemError.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY)) { - coroutineScope.launch { + viewModelScope.launch { ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( resourceProvider = this@ConfigConstraintsViewModel, dialogProvider = this@ConfigConstraintsViewModel, @@ -127,7 +130,7 @@ class ConfigConstraintsViewModel( } fun addConstraint() { - coroutineScope.launch { + viewModelScope.launch { val constraint = navigate("add_constraint", NavDestination.ChooseConstraint) ?: return@launch diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt index 06517591d7..abb4d7dbf6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt @@ -66,7 +66,7 @@ fun BaseConfigKeyMapScreen( isKeyMapEnabled: Boolean, onKeyMapEnabledChange: (Boolean) -> Unit = {}, triggerScreen: @Composable () -> Unit, - actionScreen: @Composable () -> Unit, + actionsScreen: @Composable () -> Unit, constraintsScreen: @Composable () -> Unit, optionsScreen: @Composable () -> Unit, onBackClick: () -> Unit = {}, @@ -202,7 +202,7 @@ fun BaseConfigKeyMapScreen( ) { pageIndex -> when (tabs[pageIndex]) { ConfigKeyMapTab.TRIGGER -> triggerScreen() - ConfigKeyMapTab.ACTIONS -> actionScreen() + ConfigKeyMapTab.ACTIONS -> actionsScreen() ConfigKeyMapTab.CONSTRAINTS -> constraintsScreen() ConfigKeyMapTab.OPTIONS -> optionsScreen() ConfigKeyMapTab.TRIGGER_AND_ACTIONS -> { @@ -213,7 +213,7 @@ fun BaseConfigKeyMapScreen( topScreen = triggerScreen, bottomTitle = stringResource(R.string.tab_actions), bottomHelpUrl = actionsHelpUrl, - bottomScreen = actionScreen, + bottomScreen = actionsScreen, ) } else { HorizontalTwoScreens( @@ -222,7 +222,7 @@ fun BaseConfigKeyMapScreen( leftScreen = triggerScreen, rightTitle = stringResource(R.string.tab_actions), rightHelpUrl = actionsHelpUrl, - rightScreen = actionScreen, + rightScreen = actionsScreen, ) } } @@ -255,7 +255,7 @@ fun BaseConfigKeyMapScreen( topLeftScreen = triggerScreen, topRightTitle = stringResource(R.string.tab_actions), topRightHelpUrl = actionsHelpUrl, - topRightScreen = actionScreen, + topRightScreen = actionsScreen, bottomLeftTitle = stringResource(R.string.tab_constraints), bottomLeftHelpUrl = constraintsHelpUrl, bottomLeftScreen = constraintsScreen, @@ -557,7 +557,7 @@ private fun SmallScreenPreview() { modifier = Modifier.fillMaxSize(), isKeyMapEnabled = false, triggerScreen = {}, - actionScreen = {}, + actionsScreen = {}, constraintsScreen = {}, optionsScreen = {}, ) @@ -572,7 +572,7 @@ private fun MediumScreenPreview() { modifier = Modifier.fillMaxSize(), isKeyMapEnabled = true, triggerScreen = {}, - actionScreen = {}, + actionsScreen = {}, constraintsScreen = {}, optionsScreen = {}, ) @@ -587,7 +587,7 @@ private fun MediumScreenLandscapePreview() { modifier = Modifier.fillMaxSize(), isKeyMapEnabled = true, triggerScreen = {}, - actionScreen = {}, + actionsScreen = {}, constraintsScreen = {}, optionsScreen = {}, ) @@ -602,7 +602,7 @@ private fun LargeScreenPreview() { modifier = Modifier.fillMaxSize(), isKeyMapEnabled = true, triggerScreen = {}, - actionScreen = {}, + actionsScreen = {}, constraintsScreen = {}, optionsScreen = {}, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt index 81df0fada8..a96848f03a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt @@ -5,6 +5,7 @@ import android.graphics.drawable.Drawable import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.ActionUiHelper import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.DialogModel import io.github.sds100.keymapper.base.utils.ui.DialogProvider @@ -23,11 +24,10 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlin.collections.first class ConfigKeyMapOptionsViewModel( private val coroutineScope: CoroutineScope, - private val config: ConfigKeyMapUseCase, + private val config: ConfigTriggerUseCase, private val displayUseCase: DisplayKeyMapUseCase, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val dialogProvider: DialogProvider, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt new file mode 100644 index 0000000000..8a49420851 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt @@ -0,0 +1,61 @@ +package io.github.sds100.keymapper.base.keymaps + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.utils.ui.UnsavedChangesDialog + +@Composable +fun ConfigKeyMapScreen( + modifier: Modifier = Modifier, + snackbarHostState: SnackbarHostState, + keyMapViewModel: ConfigKeyMapViewModel, + triggerScreen: @Composable () -> Unit, + actionsScreen: @Composable () -> Unit, + constraintsScreen: @Composable () -> Unit, + optionsScreen: @Composable () -> Unit, +) { + val isKeyMapEnabled by keyMapViewModel.isEnabled.collectAsStateWithLifecycle() + val showActionTapTarget by keyMapViewModel.showActionsTapTarget.collectAsStateWithLifecycle() + val showConstraintTapTarget by keyMapViewModel.showConstraintsTapTarget.collectAsStateWithLifecycle() + var showBackDialog by rememberSaveable { mutableStateOf(false) } + + if (showBackDialog) { + UnsavedChangesDialog( + onDismiss = { showBackDialog = false }, + onDiscardClick = { + showBackDialog = false + keyMapViewModel.onBackClick() + }, + ) + } + + BaseConfigKeyMapScreen( + modifier = modifier, + isKeyMapEnabled = isKeyMapEnabled, + onKeyMapEnabledChange = keyMapViewModel::onEnabledChanged, + triggerScreen = triggerScreen, + actionsScreen = actionsScreen, + constraintsScreen = constraintsScreen, + optionsScreen = optionsScreen, + onBackClick = { + if (keyMapViewModel.isKeyMapEdited) { + showBackDialog = true + } else { + keyMapViewModel.onBackClick() + } + }, + onDoneClick = keyMapViewModel::onDoneClick, + snackbarHostState = snackbarHostState, + showActionTapTarget = showActionTapTarget, + onActionTapTargetCompleted = keyMapViewModel::onActionTapTargetCompleted, + showConstraintTapTarget = showConstraintTapTarget, + onConstraintTapTargetCompleted = keyMapViewModel::onConstraintTapTargetCompleted, + onSkipTutorialClick = keyMapViewModel::onSkipTutorialClick, + ) +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt new file mode 100644 index 0000000000..4c93035b13 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt @@ -0,0 +1,117 @@ +package io.github.sds100.keymapper.base.keymaps + +import android.database.sqlite.SQLiteConstraintException +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.dataOrNull +import io.github.sds100.keymapper.common.utils.mapData +import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout +import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository +import io.github.sds100.keymapper.data.repositories.KeyMapRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ConfigKeyMapStateImpl @Inject constructor( + private val coroutineScope: CoroutineScope, + private val keyMapRepository: KeyMapRepository, + private val floatingButtonRepository: FloatingButtonRepository +) : ConfigKeyMapState { + private var originalKeyMap: KeyMap? = null + + private val _keyMap: MutableStateFlow> = MutableStateFlow(State.Loading) + override val keyMap: StateFlow> = _keyMap.asStateFlow() + + override val floatingButtonToUse: MutableStateFlow = MutableStateFlow(null) + + init { + // Update button data in the key map whenever the floating buttons changes. + coroutineScope.launch { + floatingButtonRepository.buttonsList + .filterIsInstance>>() + .map { it.data } + .collectLatest(::updateFloatingButtonTriggerKeys) + } + } + + private fun updateFloatingButtonTriggerKeys(buttons: List) { + update { keyMap -> + keyMap.copy(trigger = keyMap.trigger.updateFloatingButtonData(buttons)) + } + } + + /** + * Whether any changes were made to the key map. + */ + override val isEdited: Boolean + get() = when (val keyMap = keyMap.value) { + is State.Data -> originalKeyMap?.let { it != keyMap.data } ?: false + State.Loading -> false + } + + override suspend fun loadKeyMap(uid: String) { + _keyMap.update { State.Loading } + val entity = keyMapRepository.get(uid) ?: return + val floatingButtons = floatingButtonRepository.buttonsList + .filterIsInstance>>() + .map { it.data } + .first() + + val keyMap = KeyMapEntityMapper.fromEntity(entity, floatingButtons) + _keyMap.update { State.Data(keyMap) } + originalKeyMap = keyMap + } + + override fun loadNewKeyMap(groupUid: String?) { + val keyMap = KeyMap(groupUid = groupUid) + _keyMap.update { State.Data(keyMap) } + originalKeyMap = keyMap + } + + override fun save() { + val keyMap = keyMap.value.dataOrNull() ?: return + + if (keyMap.dbId == null) { + val entity = KeyMapEntityMapper.toEntity(keyMap, 0) + try { + keyMapRepository.insert(entity) + } catch (e: SQLiteConstraintException) { + keyMapRepository.update(entity) + } + } else { + keyMapRepository.update(KeyMapEntityMapper.toEntity(keyMap, keyMap.dbId)) + } + } + + override fun restoreState(keyMap: KeyMap) { + _keyMap.update { State.Data(keyMap) } + } + + override fun update(block: (keyMap: KeyMap) -> KeyMap) { + _keyMap.update { value -> value.mapData { block.invoke(it) } } + } + +} + +interface ConfigKeyMapState { + val keyMap: StateFlow> + val isEdited: Boolean + + fun update(block: (keyMap: KeyMap) -> KeyMap) + fun save() + + fun restoreState(keyMap: KeyMap) + suspend fun loadKeyMap(uid: String) + fun loadNewKeyMap(groupUid: String?) + + val floatingButtonToUse: MutableStateFlow +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt deleted file mode 100644 index 5e71b2573d..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt +++ /dev/null @@ -1,1114 +0,0 @@ -package io.github.sds100.keymapper.base.keymaps - -import android.database.sqlite.SQLiteConstraintException -import io.github.sds100.keymapper.base.actions.Action -import io.github.sds100.keymapper.base.actions.ActionData -import io.github.sds100.keymapper.base.actions.RepeatMode -import io.github.sds100.keymapper.base.constraints.Constraint -import io.github.sds100.keymapper.base.constraints.ConstraintMode -import io.github.sds100.keymapper.base.constraints.ConstraintState -import io.github.sds100.keymapper.base.floating.FloatingButtonEntityMapper -import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey -import io.github.sds100.keymapper.base.trigger.AssistantTriggerType -import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey -import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey -import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice -import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey -import io.github.sds100.keymapper.base.trigger.Trigger -import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerMode -import io.github.sds100.keymapper.common.models.EvdevDeviceInfo -import io.github.sds100.keymapper.common.utils.InputDeviceUtils -import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.dataOrNull -import io.github.sds100.keymapper.common.utils.firstBlocking -import io.github.sds100.keymapper.common.utils.ifIsData -import io.github.sds100.keymapper.common.utils.moveElement -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout -import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository -import io.github.sds100.keymapper.data.repositories.FloatingLayoutRepository -import io.github.sds100.keymapper.data.repositories.KeyMapRepository -import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter -import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.inputevents.KeyEventUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import java.util.LinkedList -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.collections.map - -@Singleton -class ConfigKeyMapUseCaseController @Inject constructor( - private val coroutineScope: CoroutineScope, - private val keyMapRepository: KeyMapRepository, - private val devicesAdapter: DevicesAdapter, - private val preferenceRepository: PreferenceRepository, - private val floatingLayoutRepository: FloatingLayoutRepository, - private val floatingButtonRepository: FloatingButtonRepository, - private val serviceAdapter: AccessibilityServiceAdapter, -) : ConfigKeyMapUseCase, - GetDefaultKeyMapOptionsUseCase by GetDefaultKeyMapOptionsUseCaseImpl( - coroutineScope, - preferenceRepository, - ) { - - private var originalKeyMap: KeyMap? = null - override val keyMap = MutableStateFlow>(State.Loading) - - override val floatingButtonToUse: MutableStateFlow = MutableStateFlow(null) - - private val showDeviceDescriptors: Flow = - preferenceRepository.get(Keys.showDeviceDescriptors).map { it == true } - - /** - * The most recently used is first. - */ - override val recentlyUsedActions: StateFlow> = - preferenceRepository.get(Keys.recentlyUsedActions) - .map(::getActionShortcuts) - .map { it.take(5) } - .stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), - emptyList(), - ) - - /** - * The most recently used is first. - */ - override val recentlyUsedConstraints: StateFlow> = - combine( - preferenceRepository.get(Keys.recentlyUsedConstraints).map(::getConstraintShortcuts), - keyMap.filterIsInstance>(), - ) { shortcuts, keyMap -> - - // Do not include constraints that the key map already contains. - shortcuts - .filter { !keyMap.data.constraintState.constraints.contains(it) } - .take(5) - }.stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), - emptyList(), - ) - - /** - * Whether any changes were made to the key map. - */ - override val isEdited: Boolean - get() = when (val keyMap = keyMap.value) { - is State.Data -> originalKeyMap?.let { it != keyMap.data } ?: false - State.Loading -> false - } - - init { - // Update button data in the key map whenever the floating buttons changes. - coroutineScope.launch { - floatingButtonRepository.buttonsList - .filterIsInstance>>() - .map { it.data } - .collectLatest(::updateFloatingButtonTriggerKeys) - } - } - - private fun updateFloatingButtonTriggerKeys(buttons: List) { - keyMap.update { keyMapState -> - if (keyMapState is State.Data) { - val trigger = keyMapState.data.trigger - val newKeyMap = - keyMapState.data.copy(trigger = trigger.updateFloatingButtonData(buttons)) - - State.Data(newKeyMap) - } else { - keyMapState - } - } - } - - override fun addConstraint(constraint: Constraint): Boolean { - var containsConstraint = false - - keyMap.value.ifIsData { mapping -> - val oldState = mapping.constraintState - - containsConstraint = oldState.constraints.contains(constraint) - val newState = oldState.copy(constraints = oldState.constraints.plus(constraint)) - - setConstraintState(newState) - } - - preferenceRepository.update( - Keys.recentlyUsedConstraints, - { old -> - val oldList: List = if (old == null) { - emptyList() - } else { - Json.decodeFromString>(old) - } - - val newShortcuts = LinkedList(oldList) - .also { it.addFirst(constraint) } - .distinct() - - Json.encodeToString(newShortcuts as List) - }, - ) - - return !containsConstraint - } - - override fun removeConstraint(id: String) { - keyMap.value.ifIsData { mapping -> - val newList = mapping.constraintState.constraints.toMutableSet().apply { - removeAll { it.uid == id } - } - - setConstraintState(mapping.constraintState.copy(constraints = newList)) - } - } - - override fun setAndMode() { - keyMap.value.ifIsData { mapping -> - setConstraintState(mapping.constraintState.copy(mode = ConstraintMode.AND)) - } - } - - override fun setOrMode() { - keyMap.value.ifIsData { mapping -> - setConstraintState(mapping.constraintState.copy(mode = ConstraintMode.OR)) - } - } - - override fun addAction(data: ActionData) = keyMap.value.ifIsData { mapping -> - mapping.actionList.toMutableList().apply { - add(createAction(data)) - setActionList(this) - } - - preferenceRepository.update( - Keys.recentlyUsedActions, - { old -> - val oldList: List = if (old == null) { - emptyList() - } else { - Json.decodeFromString>(old) - } - - val newShortcuts = LinkedList(oldList) - .also { it.addFirst(data) } - .distinct() - - Json.encodeToString(newShortcuts as List) - }, - ) - } - - override fun moveAction(fromIndex: Int, toIndex: Int) { - keyMap.value.ifIsData { mapping -> - mapping.actionList.toMutableList().apply { - moveElement(fromIndex, toIndex) - setActionList(this) - } - } - } - - override fun removeAction(uid: String) { - keyMap.value.ifIsData { mapping -> - mapping.actionList.toMutableList().apply { - removeAll { it.uid == uid } - setActionList(this) - } - } - } - - override suspend fun addFloatingButtonTriggerKey(buttonUid: String) { - floatingButtonToUse.update { null } - - editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> keyToCompare.buttonUid == buttonUid } - - val button = floatingButtonRepository.get(buttonUid) - ?.let { entity -> - FloatingButtonEntityMapper.fromEntity( - entity.button, - entity.layout.name, - ) - } - - val triggerKey = FloatingButtonKey( - buttonUid = buttonUid, - button = button, - clickType = clickType, - ) - - var newKeys = trigger.keys.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - } - - override fun addAssistantTriggerKey(type: AssistantTriggerType) = editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsAssistantKey = trigger.keys.any { it is AssistantTriggerKey } - - val triggerKey = AssistantTriggerKey(type = type, clickType = clickType) - - val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsAssistantKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys. - - It must be a short press because long pressing the assistant key isn't supported. - */ - !containsAssistantKey -> TriggerMode.Parallel(ClickType.SHORT_PRESS) - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun addFingerprintGesture(type: FingerprintGestureType) = editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsFingerprintGesture = trigger.keys.any { it is FingerprintTriggerKey } - - val triggerKey = FingerprintTriggerKey(type = type, clickType = clickType) - - val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsFingerprintGesture -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys. - - It must be a short press because long pressing the assistant key isn't supported. - */ - !containsFingerprintGesture -> TriggerMode.Parallel(ClickType.SHORT_PRESS) - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun addKeyEventTriggerKey( - keyCode: Int, - scanCode: Int, - device: KeyEventTriggerDevice, - requiresIme: Boolean, - ) = editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) - } - - var consumeKeyEvent = true - - // Issue #753 - if (KeyEventUtils.isModifierKey(keyCode)) { - consumeKeyEvent = false - } - - val triggerKey = KeyEventTriggerKey( - keyCode = keyCode, - device = device, - clickType = clickType, - consumeEvent = consumeKeyEvent, - requiresIme = requiresIme, - ) - - var newKeys = trigger.keys.filter { it !is EvdevTriggerKey }.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun addEvdevTriggerKey( - keyCode: Int, - scanCode: Int, - device: EvdevDeviceInfo, - ) = editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device == device - } - - val triggerKey = EvdevTriggerKey( - keyCode = keyCode, - scanCode = scanCode, - device = device, - clickType = clickType, - consumeEvent = true, - ) - - var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey }.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun removeTriggerKey(uid: String) = editTrigger { trigger -> - val newKeys = trigger.keys.toMutableList().apply { - removeAll { it.uid == uid } - } - - val newMode = when { - newKeys.size <= 1 -> TriggerMode.Undefined - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun moveTriggerKey(fromIndex: Int, toIndex: Int) = editTrigger { trigger -> - trigger.copy( - keys = trigger.keys.toMutableList().apply { - add(toIndex, removeAt(fromIndex)) - }, - ) - } - - override fun getTriggerKey(uid: String): TriggerKey? { - return keyMap.value.dataOrNull()?.trigger?.keys?.find { it.uid == uid } - } - - override fun setParallelTriggerMode() = editTrigger { trigger -> - if (trigger.mode is TriggerMode.Parallel) { - return@editTrigger trigger - } - - // undefined mode only allowed if one or no keys - if (trigger.keys.size <= 1) { - return@editTrigger trigger.copy(mode = TriggerMode.Undefined) - } - - val oldKeys = trigger.keys - var newKeys = oldKeys - - // set all the keys to a short press if coming from a non-parallel trigger - // because they must all be the same click type and can't all be double pressed - newKeys = newKeys - .map { key -> key.setClickType(clickType = ClickType.SHORT_PRESS) } - // remove duplicates of keys that have the same keycode and device id - .distinctBy { key -> - when (key) { - // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time - is AssistantTriggerKey, is FingerprintTriggerKey -> 0 - is KeyEventTriggerKey -> Pair( - key.keyCode, - key.device, - ) - - is FloatingButtonKey -> key.buttonUid - is EvdevTriggerKey -> Pair( - key.keyCode, - key.device, - ) - } - } - - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(newKeys[0].clickType) - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun setSequenceTriggerMode() = editTrigger { trigger -> - if (trigger.mode == TriggerMode.Sequence) return@editTrigger trigger - // undefined mode only allowed if one or no keys - if (trigger.keys.size <= 1) { - return@editTrigger trigger.copy(mode = TriggerMode.Undefined) - } - - trigger.copy(mode = TriggerMode.Sequence) - } - - override fun setUndefinedTriggerMode() = editTrigger { trigger -> - if (trigger.mode == TriggerMode.Undefined) return@editTrigger trigger - - // undefined mode only allowed if one or no keys - if (trigger.keys.size > 1) { - return@editTrigger trigger - } - - trigger.copy(mode = TriggerMode.Undefined) - } - - override fun setTriggerShortPress() { - editTrigger { oldTrigger -> - if (oldTrigger.mode == TriggerMode.Sequence) { - return@editTrigger oldTrigger - } - - val newKeys = oldTrigger.keys.map { it.setClickType(clickType = ClickType.SHORT_PRESS) } - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(ClickType.SHORT_PRESS) - } - oldTrigger.copy(keys = newKeys, mode = newMode) - } - } - - override fun setTriggerLongPress() { - editTrigger { trigger -> - if (trigger.mode == TriggerMode.Sequence) { - return@editTrigger trigger - } - - // You can't set the trigger to a long press if it contains a key - // that isn't detected with key codes. This is because there aren't - // separate key events for the up and down press that can be timed. - if (trigger.keys.any { !it.allowedLongPress }) { - return@editTrigger trigger - } - - val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.LONG_PRESS) } - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(ClickType.LONG_PRESS) - } - - trigger.copy(keys = newKeys, mode = newMode) - } - } - - override fun setTriggerDoublePress() { - editTrigger { trigger -> - if (trigger.mode != TriggerMode.Undefined) { - return@editTrigger trigger - } - - if (trigger.keys.any { !it.allowedDoublePress }) { - return@editTrigger trigger - } - - val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) } - val newMode = TriggerMode.Undefined - - trigger.copy(keys = newKeys, mode = newMode) - } - } - - override fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) { - editTriggerKey(keyUid) { key -> - key.setClickType(clickType = clickType) - } - } - - override fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) { - editTriggerKey(keyUid) { key -> - if (key !is KeyEventTriggerKey) { - throw IllegalArgumentException("You can not set the device for non KeyEventTriggerKeys.") - } - - key.copy(device = device) - } - } - - override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) { - editTriggerKey(keyUid) { key -> - when (key) { - is KeyEventTriggerKey -> { - key.copy(consumeEvent = consumeKeyEvent) - } - - is EvdevTriggerKey -> { - key.copy(consumeEvent = consumeKeyEvent) - } - - else -> { - key - } - } - } - } - - override fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) { - editTriggerKey(keyUid) { key -> - if (key is AssistantTriggerKey) { - key.copy(type = type) - } else { - key - } - } - } - - override fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) { - editTriggerKey(keyUid) { key -> - if (key is FingerprintTriggerKey) { - key.copy(type = type) - } else { - key - } - } - } - - override fun setVibrateEnabled(enabled: Boolean) = editTrigger { it.copy(vibrate = enabled) } - - override fun setVibrationDuration(duration: Int) = editTrigger { trigger -> - if (duration == defaultVibrateDuration.value) { - trigger.copy(vibrateDuration = null) - } else { - trigger.copy(vibrateDuration = duration) - } - } - - override fun setLongPressDelay(delay: Int) = editTrigger { trigger -> - if (delay == defaultLongPressDelay.value) { - trigger.copy(longPressDelay = null) - } else { - trigger.copy(longPressDelay = delay) - } - } - - override fun setDoublePressDelay(delay: Int) { - editTrigger { trigger -> - if (delay == defaultDoublePressDelay.value) { - trigger.copy(doublePressDelay = null) - } else { - trigger.copy(doublePressDelay = delay) - } - } - } - - override fun setSequenceTriggerTimeout(delay: Int) { - editTrigger { trigger -> - if (delay == defaultSequenceTriggerTimeout.value) { - trigger.copy(sequenceTriggerTimeout = null) - } else { - trigger.copy(sequenceTriggerTimeout = delay) - } - } - } - - override fun setLongPressDoubleVibrationEnabled(enabled: Boolean) { - editTrigger { it.copy(longPressDoubleVibration = enabled) } - } - - override fun setTriggerWhenScreenOff(enabled: Boolean) { - editTrigger { it.copy(screenOffTrigger = enabled) } - } - - override fun setTriggerFromOtherAppsEnabled(enabled: Boolean) { - editTrigger { it.copy(triggerFromOtherApps = enabled) } - } - - override fun setShowToastEnabled(enabled: Boolean) { - editTrigger { it.copy(showToast = enabled) } - } - - override fun getAvailableTriggerKeyDevices(): List { - val externalKeyEventTriggerDevices = sequence { - val inputDevices = - devicesAdapter.connectedInputDevices.value.dataOrNull() ?: emptyList() - - val showDeviceDescriptors = showDeviceDescriptors.firstBlocking() - - inputDevices.forEach { device -> - - if (device.isExternal) { - val name = if (showDeviceDescriptors) { - InputDeviceUtils.appendDeviceDescriptorToName( - device.descriptor, - device.name, - ) - } else { - device.name - } - - yield(KeyEventTriggerDevice.External(device.descriptor, name)) - } - } - } - - return sequence { - yield(KeyEventTriggerDevice.Internal) - yield(KeyEventTriggerDevice.Any) - yieldAll(externalKeyEventTriggerDevices) - }.toList() - } - - override fun setEnabled(enabled: Boolean) { - editKeyMap { it.copy(isEnabled = enabled) } - } - - override fun setActionData(uid: String, data: ActionData) { - editKeyMap { keyMap -> - val newActionList = keyMap.actionList.map { action -> - if (action.uid == uid) { - action.copy(data = data) - } else { - action - } - } - - keyMap.copy( - actionList = newActionList, - ) - } - } - - override fun setActionRepeatEnabled(uid: String, repeat: Boolean) { - setActionOption(uid) { action -> action.copy(repeat = repeat) } - } - - override fun setActionRepeatRate(uid: String, repeatRate: Int) { - setActionOption(uid) { action -> - if (repeatRate == defaultRepeatRate.value) { - action.copy(repeatRate = null) - } else { - action.copy(repeatRate = repeatRate) - } - } - } - - override fun setActionRepeatDelay(uid: String, repeatDelay: Int) { - setActionOption(uid) { action -> - if (repeatDelay == defaultRepeatDelay.value) { - action.copy(repeatDelay = null) - } else { - action.copy(repeatDelay = repeatDelay) - } - } - } - - override fun setActionRepeatLimit(uid: String, repeatLimit: Int) { - setActionOption(uid) { action -> - if (action.repeatMode == RepeatMode.LIMIT_REACHED) { - if (repeatLimit == 1) { - action.copy(repeatLimit = null) - } else { - action.copy(repeatLimit = repeatLimit) - } - } else { - if (repeatLimit == Int.MAX_VALUE) { - action.copy(repeatLimit = null) - } else { - action.copy(repeatLimit = repeatLimit) - } - } - } - } - - override fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) = - setActionOption(uid) { it.copy(holdDown = holdDown) } - - override fun setActionHoldDownDuration(uid: String, holdDownDuration: Int) { - setActionOption(uid) { action -> - if (holdDownDuration == defaultHoldDownDuration.value) { - action.copy(holdDownDuration = null) - } else { - action.copy(holdDownDuration = holdDownDuration) - } - } - } - - override fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN) } - - override fun setActionStopRepeatingWhenLimitReached(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.LIMIT_REACHED) } - - override fun setActionStopRepeatingWhenTriggerReleased(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_RELEASED) } - - override fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) = - setActionOption(uid) { it.copy(stopHoldDownWhenTriggerPressedAgain = enabled) } - - override fun setActionMultiplier(uid: String, multiplier: Int) { - setActionOption(uid) { action -> - if (multiplier == 1) { - action.copy(multiplier = null) - } else { - action.copy(multiplier = multiplier) - } - } - } - - override fun setDelayBeforeNextAction(uid: String, delay: Int) { - setActionOption(uid) { action -> - if (delay == 0) { - action.copy(delayBeforeNextAction = null) - } else { - action.copy(delayBeforeNextAction = delay) - } - } - } - - private fun createAction(data: ActionData): Action { - var holdDown = false - var repeat = false - - if (data is ActionData.InputKeyEvent) { - val trigger = keyMap.value.dataOrNull()?.trigger - - val containsDpadKey: Boolean = if (trigger == null) { - false - } else { - trigger.keys - .mapNotNull { it as? KeyEventTriggerKey } - .any { KeyEventUtils.isDpadKeyCode(it.keyCode) } - } - - if (KeyEventUtils.isModifierKey(data.keyCode) || containsDpadKey) { - holdDown = true - repeat = false - } else { - repeat = true - } - } - - if (data is ActionData.Volume.Down || data is ActionData.Volume.Up || data is ActionData.Volume.Stream) { - repeat = true - } - - if (data is ActionData.AnswerCall) { - addConstraint(Constraint.PhoneRinging()) - } - - if (data is ActionData.EndCall) { - addConstraint(Constraint.InPhoneCall()) - } - - return Action( - data = data, - repeat = repeat, - holdDown = holdDown, - ) - } - - private fun setActionList(actionList: List) { - editKeyMap { it.copy(actionList = actionList) } - } - - private fun setConstraintState(constraintState: ConstraintState) { - editKeyMap { it.copy(constraintState = constraintState) } - } - - override suspend fun loadKeyMap(uid: String) { - keyMap.update { State.Loading } - val entity = keyMapRepository.get(uid) ?: return - val floatingButtons = floatingButtonRepository.buttonsList - .filterIsInstance>>() - .map { it.data } - .first() - - val keyMap = KeyMapEntityMapper.fromEntity(entity, floatingButtons) - this.keyMap.update { State.Data(keyMap) } - originalKeyMap = keyMap - } - - override fun loadNewKeyMap(groupUid: String?) { - val keyMap = KeyMap(groupUid = groupUid) - this.keyMap.update { State.Data(keyMap) } - originalKeyMap = keyMap - } - - override fun save() { - val keyMap = keyMap.value.dataOrNull() ?: return - - if (keyMap.dbId == null) { - val entity = KeyMapEntityMapper.toEntity(keyMap, 0) - try { - keyMapRepository.insert(entity) - } catch (e: SQLiteConstraintException) { - keyMapRepository.update(entity) - } - } else { - keyMapRepository.update(KeyMapEntityMapper.toEntity(keyMap, keyMap.dbId)) - } - } - - override fun restoreState(keyMap: KeyMap) { - this.keyMap.value = State.Data(keyMap) - } - - override suspend fun getFloatingLayoutCount(): Int { - return floatingLayoutRepository.count() - } - - override suspend fun sendServiceEvent(event: AccessibilityServiceEvent): KMResult<*> { - return serviceAdapter.send(event) - } - - private fun setActionOption( - uid: String, - block: (action: Action) -> Action, - ) { - editKeyMap { keyMap -> - val newActionList = keyMap.actionList.map { action -> - if (action.uid == uid) { - block.invoke(action) - } else { - action - } - } - - keyMap.copy( - actionList = newActionList, - ) - } - } - - private suspend fun getActionShortcuts(json: String?): List { - if (json == null) { - return emptyList() - } - - try { - return withContext(Dispatchers.Default) { - val list = Json.decodeFromString>(json) - - list.distinct() - } - } catch (_: Exception) { - preferenceRepository.set(Keys.recentlyUsedActions, null) - return emptyList() - } - } - - private suspend fun getConstraintShortcuts(json: String?): List { - if (json == null) { - return emptyList() - } - - try { - return withContext(Dispatchers.Default) { - val list = Json.decodeFromString>(json) - - list.distinct() - } - } catch (_: Exception) { - preferenceRepository.set(Keys.recentlyUsedConstraints, null) - return emptyList() - } - } - - private inline fun editTrigger(block: (trigger: Trigger) -> Trigger) { - editKeyMap { keyMap -> - val newTrigger = block(keyMap.trigger) - - keyMap.copy(trigger = newTrigger) - } - } - - private fun editTriggerKey(uid: String, block: (key: TriggerKey) -> TriggerKey) { - editTrigger { oldTrigger -> - val newKeys = oldTrigger.keys.map { - if (it.uid == uid) { - block.invoke(it) - } else { - it - } - } - - oldTrigger.copy(keys = newKeys) - } - } - - private inline fun editKeyMap(block: (keymap: KeyMap) -> KeyMap) { - keyMap.value.ifIsData { keyMap.value = State.Data(block.invoke(it)) } - } -} - -interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { - val keyMap: Flow> - val isEdited: Boolean - - fun save() - - fun setEnabled(enabled: Boolean) - - fun addAction(data: ActionData) - fun moveAction(fromIndex: Int, toIndex: Int) - fun removeAction(uid: String) - - val recentlyUsedActions: Flow> - fun setActionData(uid: String, data: ActionData) - fun setActionMultiplier(uid: String, multiplier: Int) - fun setDelayBeforeNextAction(uid: String, delay: Int) - fun setActionRepeatRate(uid: String, repeatRate: Int) - fun setActionRepeatLimit(uid: String, repeatLimit: Int) - fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) - fun setActionStopRepeatingWhenLimitReached(uid: String) - fun setActionRepeatEnabled(uid: String, repeat: Boolean) - fun setActionRepeatDelay(uid: String, repeatDelay: Int) - fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) - fun setActionHoldDownDuration(uid: String, holdDownDuration: Int) - fun setActionStopRepeatingWhenTriggerReleased(uid: String) - - fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) - - val recentlyUsedConstraints: Flow> - fun addConstraint(constraint: Constraint): Boolean - fun removeConstraint(id: String) - fun setAndMode() - fun setOrMode() - suspend fun sendServiceEvent(event: AccessibilityServiceEvent): KMResult<*> - - // trigger - fun addKeyEventTriggerKey( - keyCode: Int, - scanCode: Int, - device: KeyEventTriggerDevice, - requiresIme: Boolean, - ) - - suspend fun addFloatingButtonTriggerKey(buttonUid: String) - fun addAssistantTriggerKey(type: AssistantTriggerType) - fun addFingerprintGesture(type: FingerprintGestureType) - fun addEvdevTriggerKey( - keyCode: Int, - scanCode: Int, - device: EvdevDeviceInfo, - ) - - fun removeTriggerKey(uid: String) - fun getTriggerKey(uid: String): TriggerKey? - fun moveTriggerKey(fromIndex: Int, toIndex: Int) - - fun restoreState(keyMap: KeyMap) - suspend fun loadKeyMap(uid: String) - fun loadNewKeyMap(groupUid: String?) - - fun setParallelTriggerMode() - fun setSequenceTriggerMode() - fun setUndefinedTriggerMode() - - fun setTriggerShortPress() - fun setTriggerLongPress() - fun setTriggerDoublePress() - - fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) - fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) - fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) - fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) - fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) - - fun setVibrateEnabled(enabled: Boolean) - fun setVibrationDuration(duration: Int) - fun setLongPressDelay(delay: Int) - fun setDoublePressDelay(delay: Int) - fun setSequenceTriggerTimeout(delay: Int) - fun setLongPressDoubleVibrationEnabled(enabled: Boolean) - fun setTriggerWhenScreenOff(enabled: Boolean) - fun setTriggerFromOtherAppsEnabled(enabled: Boolean) - fun setShowToastEnabled(enabled: Boolean) - - fun getAvailableTriggerKeyDevices(): List - - val floatingButtonToUse: MutableStateFlow - suspend fun getFloatingLayoutCount(): Int -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt similarity index 76% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapViewModel.kt rename to base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt index 7dd8d9d1f4..e9887e95c0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt @@ -2,11 +2,10 @@ package io.github.sds100.keymapper.base.keymaps import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.github.sds100.keymapper.base.actions.ConfigActionsViewModel -import io.github.sds100.keymapper.base.constraints.ConfigConstraintsViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase -import io.github.sds100.keymapper.base.trigger.BaseConfigTriggerViewModel +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.common.utils.dataOrNull @@ -16,9 +15,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject -abstract class BaseConfigKeyMapViewModel( - private val config: ConfigKeyMapUseCase, +@HiltViewModel +class ConfigKeyMapViewModel @Inject constructor( + private val configKeyMapState: ConfigKeyMapState, + private val configTrigger: ConfigTriggerUseCase, private val onboarding: OnboardingUseCase, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, @@ -26,21 +28,17 @@ abstract class BaseConfigKeyMapViewModel( NavigationProvider by navigationProvider, DialogProvider by dialogProvider { - abstract val configActionsViewModel: ConfigActionsViewModel - abstract val configTriggerViewModel: BaseConfigTriggerViewModel - abstract val configConstraintsViewModel: ConfigConstraintsViewModel + val isKeyMapEdited: Boolean + get() = configKeyMapState.isEdited - val isEnabled: StateFlow = config.keyMap + val isEnabled: StateFlow = configTrigger.keyMap .map { state -> state.dataOrNull()?.isEnabled ?: true } .stateIn(viewModelScope, SharingStarted.Eagerly, true) - val isKeyMapEdited: Boolean - get() = config.isEdited - val showActionsTapTarget: StateFlow = combine( onboarding.showTapTarget(OnboardingTapTarget.CHOOSE_ACTION), - config.keyMap, + configKeyMapState.keyMap, ) { showTapTarget, keyMapState -> // Show the choose action tap target if they have recorded a key. showTapTarget && keyMapState.dataOrNull()?.trigger?.keys?.isNotEmpty() ?: false @@ -49,14 +47,14 @@ abstract class BaseConfigKeyMapViewModel( val showConstraintsTapTarget: StateFlow = combine( onboarding.showTapTarget(OnboardingTapTarget.CHOOSE_CONSTRAINT), - config.keyMap, + configKeyMapState.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 onDoneClick() { - config.save() + configKeyMapState.save() viewModelScope.launch { popBackStack() @@ -64,17 +62,17 @@ abstract class BaseConfigKeyMapViewModel( } fun loadNewKeyMap(floatingButtonUid: String? = null, groupUid: String?) { - config.loadNewKeyMap(groupUid) + configKeyMapState.loadNewKeyMap(groupUid) if (floatingButtonUid != null) { viewModelScope.launch { - config.addFloatingButtonTriggerKey(floatingButtonUid) + configTrigger.addFloatingButtonTriggerKey(floatingButtonUid) } } } fun loadKeyMap(uid: String) { viewModelScope.launch { - config.loadKeyMap(uid) + configKeyMapState.loadKeyMap(uid) } } @@ -84,10 +82,6 @@ abstract class BaseConfigKeyMapViewModel( } } - fun onEnabledChanged(enabled: Boolean) { - config.setEnabled(enabled) - } - fun onActionTapTargetCompleted() { onboarding.completedTapTarget(OnboardingTapTarget.CHOOSE_ACTION) } @@ -99,4 +93,9 @@ abstract class BaseConfigKeyMapViewModel( fun onSkipTutorialClick() { onboarding.skipTapTargetOnboarding() } + + fun onEnabledChanged(enabled: Boolean) { + configTrigger.setEnabled(enabled) + } + } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt index 11e3b89199..b7db27f9af 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt @@ -8,8 +8,11 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject +import javax.inject.Singleton -class GetDefaultKeyMapOptionsUseCaseImpl( +@Singleton +class GetDefaultKeyMapOptionsUseCaseImpl @Inject constructor( coroutineScope: CoroutineScope, preferenceRepository: PreferenceRepository, ) : GetDefaultKeyMapOptionsUseCase { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt index d69f15c5c5..5eaf510603 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt @@ -3,15 +3,15 @@ package io.github.sds100.keymapper.base.shortcuts import android.content.Intent import android.graphics.drawable.Drawable import androidx.core.os.bundleOf +import dagger.hilt.android.scopes.ViewModelScoped import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.system.apps.AppShortcutAdapter import javax.inject.Inject +@ViewModelScoped class CreateKeyMapShortcutUseCaseImpl @Inject constructor( private val appShortcutAdapter: AppShortcutAdapter, - private val resourceProvider: ResourceProvider, ) : CreateKeyMapShortcutUseCase { companion object { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt index 9bbd2825da..bb05d142ee 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt @@ -15,12 +15,13 @@ import io.github.sds100.keymapper.base.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper import io.github.sds100.keymapper.base.groups.GroupListItemModel -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase +import io.github.sds100.keymapper.base.home.KeyMapAppBarState +import io.github.sds100.keymapper.base.home.KeyMapGroup import io.github.sds100.keymapper.base.home.KeyMapListItemCreator import io.github.sds100.keymapper.base.home.KeyMapListState import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase -import io.github.sds100.keymapper.base.home.KeyMapAppBarState -import io.github.sds100.keymapper.base.home.KeyMapGroup +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.base.utils.ui.ResourceProvider @@ -41,7 +42,8 @@ import javax.inject.Inject @HiltViewModel class CreateKeyMapShortcutViewModel @Inject constructor( - private val config: ConfigKeyMapUseCase, + private val configKeyMapState: ConfigKeyMapState, + private val configTrigger: ConfigTriggerUseCase, private val listKeyMaps: ListKeyMapsUseCase, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val resourceProvider: ResourceProvider, @@ -162,10 +164,10 @@ class CreateKeyMapShortcutViewModel @Inject constructor( if (state.keyMaps !is State.Data) return@launch - config.loadKeyMap(uid) - config.setTriggerFromOtherAppsEnabled(true) + configKeyMapState.loadKeyMap(uid) + configTrigger.setTriggerFromOtherAppsEnabled(true) - val keyMapState = config.keyMap.first() + val keyMapState = configKeyMapState.keyMap.first() if (keyMapState !is State.Data) return@launch @@ -218,7 +220,7 @@ class CreateKeyMapShortcutViewModel @Inject constructor( icon = icon, ) - config.save() + configKeyMapState.save() _returnIntentResult.emit(intent) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 55d752f151..f470c1ca61 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -8,12 +8,12 @@ import androidx.compose.material.icons.rounded.Fingerprint import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapOptionsViewModel -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.KeyMap @@ -22,6 +22,7 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.ProductId import io.github.sds100.keymapper.base.purchasing.PurchasingManager +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem @@ -42,7 +43,6 @@ import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.ifIsData import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.common.utils.onSuccess -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -62,9 +62,8 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch abstract class BaseConfigTriggerViewModel( - private val coroutineScope: CoroutineScope, private val onboarding: OnboardingUseCase, - private val config: ConfigKeyMapUseCase, + private val config: ConfigTriggerUseCase, private val recordTrigger: RecordTriggerController, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val displayKeyMap: DisplayKeyMapUseCase, @@ -74,7 +73,8 @@ abstract class BaseConfigTriggerViewModel( resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, -) : ResourceProvider by resourceProvider, +) : ViewModel(), + ResourceProvider by resourceProvider, DialogProvider by dialogProvider, NavigationProvider by navigationProvider { @@ -84,7 +84,7 @@ abstract class BaseConfigTriggerViewModel( } val optionsViewModel = ConfigKeyMapOptionsViewModel( - coroutineScope, + viewModelScope, config, displayKeyMap, createKeyMapShortcut, @@ -140,7 +140,7 @@ abstract class BaseConfigTriggerViewModel( val state: StateFlow> = _state.asStateFlow() val recordTriggerState: StateFlow = recordTrigger.state.stateIn( - coroutineScope, + viewModelScope, SharingStarted.Lazily, RecordTriggerState.Idle, ) @@ -160,7 +160,7 @@ abstract class BaseConfigTriggerViewModel( isChosen, ) }.stateIn( - coroutineScope, + viewModelScope, SharingStarted.Lazily, SetupGuiKeyboardState.DEFAULT, ) @@ -168,7 +168,7 @@ abstract class BaseConfigTriggerViewModel( val triggerKeyOptionsUid = MutableStateFlow(null) val triggerKeyOptionsState: StateFlow = combine(config.keyMap, triggerKeyOptionsUid, transform = ::buildKeyOptionsUiState) - .stateIn(coroutineScope, SharingStarted.Lazily, null) + .stateIn(viewModelScope, SharingStarted.Lazily, null) /** * Check whether the user stopped the trigger recording countdown. This @@ -205,9 +205,9 @@ abstract class BaseConfigTriggerViewModel( showTapTargetsPair.second, ) } - }.launchIn(coroutineScope) + }.launchIn(viewModelScope) - coroutineScope.launch { + viewModelScope.launch { recordTrigger.onRecordKey.collect { key -> when (key) { is RecordedKey.EvdevEvent -> onRecordEvdevEvent(key) @@ -216,7 +216,7 @@ abstract class BaseConfigTriggerViewModel( } } - coroutineScope.launch { + viewModelScope.launch { config.keyMap .mapNotNull { it.dataOrNull()?.trigger?.mode } .distinctUntilChanged() @@ -240,12 +240,12 @@ abstract class BaseConfigTriggerViewModel( // reset this field when recording has completed isRecordingCompletionUserInitiated = false - }.launchIn(coroutineScope) + }.launchIn(viewModelScope) } open fun onClickTriggerKeyShortcut(shortcut: TriggerKeyShortcut) { if (shortcut == TriggerKeyShortcut.FINGERPRINT_GESTURE) { - coroutineScope.launch { + viewModelScope.launch { val listItems = listOf( FingerprintGestureType.SWIPE_DOWN to getString(R.string.fingerprint_gesture_down), FingerprintGestureType.SWIPE_UP to getString(R.string.fingerprint_gesture_up), @@ -614,7 +614,7 @@ abstract class BaseConfigTriggerViewModel( } fun onRecordTriggerButtonClick() { - coroutineScope.launch { + viewModelScope.launch { val recordTriggerState = recordTrigger.state.firstOrNull() ?: return@launch val result = when (recordTriggerState) { @@ -652,7 +652,7 @@ abstract class BaseConfigTriggerViewModel( } open fun onTriggerErrorClick(error: TriggerError) { - coroutineScope.launch { + viewModelScope.launch { when (error) { TriggerError.DND_ACCESS_DENIED -> ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( @@ -805,7 +805,7 @@ abstract class BaseConfigTriggerViewModel( } fun onEnableGuiKeyboardClick() { - coroutineScope.launch { + viewModelScope.launch { setupGuiKeyboard.enableInputMethod() } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt new file mode 100644 index 0000000000..2a68d6c42f --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -0,0 +1,629 @@ +package io.github.sds100.keymapper.base.trigger + +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.floating.FloatingButtonEntityMapper +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.keymaps.GetDefaultKeyMapOptionsUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.dataOrNull +import io.github.sds100.keymapper.common.utils.firstBlocking +import io.github.sds100.keymapper.data.Keys +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.system.devices.DevicesAdapter +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ViewModelScoped +class ConfigTriggerUseCaseImpl @Inject constructor( + private val state: ConfigKeyMapState, + private val preferenceRepository: PreferenceRepository, + private val floatingButtonRepository: FloatingButtonRepository, + private val devicesAdapter: DevicesAdapter, + private val floatingLayoutRepository: FloatingLayoutRepository, + private val getDefaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase +) : ConfigTriggerUseCase, GetDefaultKeyMapOptionsUseCase by getDefaultKeyMapOptionsUseCase { + override val keyMap: StateFlow> = state.keyMap + + override val floatingButtonToUse: MutableStateFlow = state.floatingButtonToUse + + private val showDeviceDescriptors: Flow = + preferenceRepository.get(Keys.showDeviceDescriptors).map { it == true } + + override fun setEnabled(enabled: Boolean) { + state.update { it.copy(isEnabled = enabled) } + } + + override suspend fun getFloatingLayoutCount(): Int { + return floatingLayoutRepository.count() + } + + override suspend fun addFloatingButtonTriggerKey(buttonUid: String) { + floatingButtonToUse.update { null } + + val button = floatingButtonRepository.get(buttonUid) + ?.let { entity -> + FloatingButtonEntityMapper.fromEntity( + entity.button, + entity.layout.name, + ) + } + + updateTrigger { trigger -> + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys + .filterIsInstance() + .any { keyToCompare -> keyToCompare.buttonUid == buttonUid } + + val triggerKey = FloatingButtonKey( + buttonUid = buttonUid, + button = button, + clickType = clickType, + ) + + var newKeys = trigger.keys.plus(triggerKey) + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys */ + newKeys.size == 2 && !containsKey -> { + newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } + TriggerMode.Parallel(triggerKey.clickType) + } + + else -> trigger.mode + } + + trigger.copy(keys = newKeys, mode = newMode) + } + } + + override fun addAssistantTriggerKey(type: AssistantTriggerType) = updateTrigger { trigger -> + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsAssistantKey = trigger.keys.any { it is AssistantTriggerKey } + + val triggerKey = AssistantTriggerKey(type = type, clickType = clickType) + + val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsAssistantKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys. + + It must be a short press because long pressing the assistant key isn't supported. + */ + !containsAssistantKey -> TriggerMode.Parallel(ClickType.SHORT_PRESS) + else -> trigger.mode + } + + trigger.copy(keys = newKeys, mode = newMode) + } + + override fun addFingerprintGesture(type: FingerprintGestureType) = updateTrigger { trigger -> + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsFingerprintGesture = trigger.keys.any { it is FingerprintTriggerKey } + + val triggerKey = FingerprintTriggerKey(type = type, clickType = clickType) + + val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsFingerprintGesture -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys. + + It must be a short press because long pressing the assistant key isn't supported. + */ + !containsFingerprintGesture -> TriggerMode.Parallel(ClickType.SHORT_PRESS) + else -> trigger.mode + } + + trigger.copy(keys = newKeys, mode = newMode) + } + + override fun addKeyEventTriggerKey( + keyCode: Int, + scanCode: Int, + device: KeyEventTriggerDevice, + requiresIme: Boolean, + ) = updateTrigger { trigger -> + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys + .filterIsInstance() + .any { keyToCompare -> + keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) + } + + var consumeKeyEvent = true + + // Issue #753 + if (KeyEventUtils.isModifierKey(keyCode)) { + consumeKeyEvent = false + } + + val triggerKey = KeyEventTriggerKey( + keyCode = keyCode, + device = device, + clickType = clickType, + consumeEvent = consumeKeyEvent, + requiresIme = requiresIme, + ) + + var newKeys = trigger.keys.filter { it !is EvdevTriggerKey }.plus(triggerKey) + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys */ + newKeys.size == 2 && !containsKey -> { + newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } + TriggerMode.Parallel(triggerKey.clickType) + } + + else -> trigger.mode + } + + trigger.copy(keys = newKeys, mode = newMode) + } + + override fun addEvdevTriggerKey( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + ) = updateTrigger { trigger -> + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys + .filterIsInstance() + .any { keyToCompare -> + keyToCompare.keyCode == keyCode && keyToCompare.device == device + } + + val triggerKey = EvdevTriggerKey( + keyCode = keyCode, + scanCode = scanCode, + device = device, + clickType = clickType, + consumeEvent = true, + ) + + var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey }.plus(triggerKey) + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys */ + newKeys.size == 2 && !containsKey -> { + newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } + TriggerMode.Parallel(triggerKey.clickType) + } + + else -> trigger.mode + } + + trigger.copy(keys = newKeys, mode = newMode) + } + + override fun removeTriggerKey(uid: String) = updateTrigger { trigger -> + val newKeys = trigger.keys.toMutableList().apply { + removeAll { it.uid == uid } + } + + val newMode = when { + newKeys.size <= 1 -> TriggerMode.Undefined + else -> trigger.mode + } + + trigger.copy(keys = newKeys, mode = newMode) + } + + override fun moveTriggerKey(fromIndex: Int, toIndex: Int) = updateTrigger { trigger -> + trigger.copy( + keys = trigger.keys.toMutableList().apply { + add(toIndex, removeAt(fromIndex)) + }, + ) + } + + override fun getTriggerKey(uid: String): TriggerKey? { + return state.keyMap.value.dataOrNull()?.trigger?.keys?.find { it.uid == uid } + } + + override fun setParallelTriggerMode() = updateTrigger { trigger -> + if (trigger.mode is TriggerMode.Parallel) { + return@updateTrigger trigger + } + + // undefined mode only allowed if one or no keys + if (trigger.keys.size <= 1) { + return@updateTrigger trigger.copy(mode = TriggerMode.Undefined) + } + + val oldKeys = trigger.keys + var newKeys = oldKeys + + // set all the keys to a short press if coming from a non-parallel trigger + // because they must all be the same click type and can't all be double pressed + newKeys = newKeys + .map { key -> key.setClickType(clickType = ClickType.SHORT_PRESS) } + // remove duplicates of keys that have the same keycode and device id + .distinctBy { key -> + when (key) { + // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time + is AssistantTriggerKey, is FingerprintTriggerKey -> 0 + is KeyEventTriggerKey -> Pair( + key.keyCode, + key.device, + ) + + is FloatingButtonKey -> key.buttonUid + is EvdevTriggerKey -> Pair( + key.keyCode, + key.device, + ) + } + } + + val newMode = if (newKeys.size <= 1) { + TriggerMode.Undefined + } else { + TriggerMode.Parallel(newKeys[0].clickType) + } + + trigger.copy(keys = newKeys, mode = newMode) + } + + override fun setSequenceTriggerMode() = updateTrigger { trigger -> + if (trigger.mode == TriggerMode.Sequence) return@updateTrigger trigger + // undefined mode only allowed if one or no keys + if (trigger.keys.size <= 1) { + return@updateTrigger trigger.copy(mode = TriggerMode.Undefined) + } + + trigger.copy(mode = TriggerMode.Sequence) + } + + override fun setUndefinedTriggerMode() = updateTrigger { trigger -> + if (trigger.mode == TriggerMode.Undefined) return@updateTrigger trigger + + // undefined mode only allowed if one or no keys + if (trigger.keys.size > 1) { + return@updateTrigger trigger + } + + trigger.copy(mode = TriggerMode.Undefined) + } + + override fun setTriggerShortPress() { + updateTrigger { oldTrigger -> + if (oldTrigger.mode == TriggerMode.Sequence) { + return@updateTrigger oldTrigger + } + + val newKeys = oldTrigger.keys.map { it.setClickType(clickType = ClickType.SHORT_PRESS) } + val newMode = if (newKeys.size <= 1) { + TriggerMode.Undefined + } else { + TriggerMode.Parallel(ClickType.SHORT_PRESS) + } + oldTrigger.copy(keys = newKeys, mode = newMode) + } + } + + override fun setTriggerLongPress() { + updateTrigger { trigger -> + if (trigger.mode == TriggerMode.Sequence) { + return@updateTrigger trigger + } + + // You can't set the trigger to a long press if it contains a key + // that isn't detected with key codes. This is because there aren't + // separate key events for the up and down press that can be timed. + if (trigger.keys.any { !it.allowedLongPress }) { + return@updateTrigger trigger + } + + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.LONG_PRESS) } + val newMode = if (newKeys.size <= 1) { + TriggerMode.Undefined + } else { + TriggerMode.Parallel(ClickType.LONG_PRESS) + } + + trigger.copy(keys = newKeys, mode = newMode) + } + } + + override fun setTriggerDoublePress() { + updateTrigger { trigger -> + if (trigger.mode != TriggerMode.Undefined) { + return@updateTrigger trigger + } + + if (trigger.keys.any { !it.allowedDoublePress }) { + return@updateTrigger trigger + } + + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) } + val newMode = TriggerMode.Undefined + + trigger.copy(keys = newKeys, mode = newMode) + } + } + + override fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) { + updateTriggerKey(keyUid) { key -> + key.setClickType(clickType = clickType) + } + } + + override fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) { + updateTriggerKey(keyUid) { key -> + if (key !is KeyEventTriggerKey) { + throw IllegalArgumentException("You can not set the device for non KeyEventTriggerKeys.") + } + + key.copy(device = device) + } + } + + override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) { + updateTriggerKey(keyUid) { key -> + when (key) { + is KeyEventTriggerKey -> { + key.copy(consumeEvent = consumeKeyEvent) + } + + is EvdevTriggerKey -> { + key.copy(consumeEvent = consumeKeyEvent) + } + + else -> { + key + } + } + } + } + + override fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) { + updateTriggerKey(keyUid) { key -> + if (key is AssistantTriggerKey) { + key.copy(type = type) + } else { + key + } + } + } + + override fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) { + updateTriggerKey(keyUid) { key -> + if (key is FingerprintTriggerKey) { + key.copy(type = type) + } else { + key + } + } + } + + override fun setVibrateEnabled(enabled: Boolean) = updateTrigger { it.copy(vibrate = enabled) } + + override fun setVibrationDuration(duration: Int) = updateTrigger { trigger -> + if (duration == defaultVibrateDuration.value) { + trigger.copy(vibrateDuration = null) + } else { + trigger.copy(vibrateDuration = duration) + } + } + + override fun setLongPressDelay(delay: Int) = updateTrigger { trigger -> + if (delay == defaultLongPressDelay.value) { + trigger.copy(longPressDelay = null) + } else { + trigger.copy(longPressDelay = delay) + } + } + + override fun setDoublePressDelay(delay: Int) { + updateTrigger { trigger -> + if (delay == defaultDoublePressDelay.value) { + trigger.copy(doublePressDelay = null) + } else { + trigger.copy(doublePressDelay = delay) + } + } + } + + override fun setSequenceTriggerTimeout(delay: Int) { + updateTrigger { trigger -> + if (delay == defaultSequenceTriggerTimeout.value) { + trigger.copy(sequenceTriggerTimeout = null) + } else { + trigger.copy(sequenceTriggerTimeout = delay) + } + } + } + + override fun setLongPressDoubleVibrationEnabled(enabled: Boolean) { + updateTrigger { it.copy(longPressDoubleVibration = enabled) } + } + + override fun setTriggerWhenScreenOff(enabled: Boolean) { + updateTrigger { it.copy(screenOffTrigger = enabled) } + } + + override fun setTriggerFromOtherAppsEnabled(enabled: Boolean) { + updateTrigger { it.copy(triggerFromOtherApps = enabled) } + } + + override fun setShowToastEnabled(enabled: Boolean) { + updateTrigger { it.copy(showToast = enabled) } + } + + override fun getAvailableTriggerKeyDevices(): List { + val externalKeyEventTriggerDevices = sequence { + val inputDevices = + devicesAdapter.connectedInputDevices.value.dataOrNull() ?: emptyList() + + val showDeviceDescriptors = showDeviceDescriptors.firstBlocking() + + inputDevices.forEach { device -> + + if (device.isExternal) { + val name = if (showDeviceDescriptors) { + InputDeviceUtils.appendDeviceDescriptorToName( + device.descriptor, + device.name, + ) + } else { + device.name + } + + yield(KeyEventTriggerDevice.External(device.descriptor, name)) + } + } + } + + return sequence { + yield(KeyEventTriggerDevice.Internal) + yield(KeyEventTriggerDevice.Any) + yieldAll(externalKeyEventTriggerDevices) + }.toList() + } + + private fun updateTrigger(block: (trigger: Trigger) -> Trigger) { + state.update { keyMap -> + val newTrigger = block(keyMap.trigger) + + keyMap.copy(trigger = newTrigger) + } + } + + private fun updateTriggerKey(uid: String, block: (key: TriggerKey) -> TriggerKey) { + updateTrigger { oldTrigger -> + val newKeys = oldTrigger.keys.map { + if (it.uid == uid) { + block.invoke(it) + } else { + it + } + } + + oldTrigger.copy(keys = newKeys) + } + } + +} + +interface ConfigTriggerUseCase : GetDefaultKeyMapOptionsUseCase { + + val keyMap: StateFlow> + + fun setEnabled(enabled: Boolean) + + // trigger + fun addKeyEventTriggerKey( + keyCode: Int, + scanCode: Int, + device: KeyEventTriggerDevice, + requiresIme: Boolean, + ) + + suspend fun addFloatingButtonTriggerKey(buttonUid: String) + fun addAssistantTriggerKey(type: AssistantTriggerType) + fun addFingerprintGesture(type: FingerprintGestureType) + fun addEvdevTriggerKey( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + ) + + fun removeTriggerKey(uid: String) + fun getTriggerKey(uid: String): TriggerKey? + fun moveTriggerKey(fromIndex: Int, toIndex: Int) + + fun setParallelTriggerMode() + fun setSequenceTriggerMode() + fun setUndefinedTriggerMode() + + fun setTriggerShortPress() + fun setTriggerLongPress() + fun setTriggerDoublePress() + + fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) + fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) + fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) + fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) + fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) + + fun setVibrateEnabled(enabled: Boolean) + fun setVibrationDuration(duration: Int) + fun setLongPressDelay(delay: Int) + fun setDoublePressDelay(delay: Int) + fun setSequenceTriggerTimeout(delay: Int) + fun setLongPressDoubleVibrationEnabled(enabled: Boolean) + fun setTriggerWhenScreenOff(enabled: Boolean) + fun setTriggerFromOtherAppsEnabled(enabled: Boolean) + fun setShowToastEnabled(enabled: Boolean) + + fun getAvailableTriggerKeyDevices(): List + + val floatingButtonToUse: MutableStateFlow + suspend fun getFloatingLayoutCount(): Int +} \ No newline at end of file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt index 5ab6e8f6c0..08109e0471 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt @@ -5,7 +5,7 @@ import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.constraints.Constraint import io.github.sds100.keymapper.base.keymaps.ClickType -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCaseController +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapController import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey @@ -43,11 +43,11 @@ class ConfigKeyMapUseCaseTest { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) - private lateinit var useCase: ConfigKeyMapUseCaseController + private lateinit var useCase: ConfigKeyMapController @Before fun init() { - useCase = ConfigKeyMapUseCaseController( + useCase = ConfigKeyMapController( coroutineScope = testScope, devicesAdapter = mock(), keyMapRepository = mock(), From d8bd765f9216707ea177e806906225ea12eed67b Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 22:04:46 +0100 Subject: [PATCH 094/215] #1394 fix tests --- .../base/keymaps/ConfigKeyMapState.kt | 6 + .../base/actions/ConfigActionsUseCaseTest.kt | 154 ++++++++++++++ .../ConfigTriggerUseCaseTest.kt} | 199 ++++-------------- .../keymapper/base/trigger/TriggerKeyTest.kt | 18 ++ 4 files changed, 221 insertions(+), 156 deletions(-) create mode 100644 base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt rename base/src/test/java/io/github/sds100/keymapper/base/{ConfigKeyMapUseCaseTest.kt => trigger/ConfigTriggerUseCaseTest.kt} (78%) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt index 4c93035b13..705a1adc7c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt @@ -77,6 +77,12 @@ class ConfigKeyMapStateImpl @Inject constructor( originalKeyMap = keyMap } + // Useful for testing + fun setKeyMap(keyMap: KeyMap) { + _keyMap.update { State.Data(keyMap) } + originalKeyMap = keyMap + } + override fun save() { val keyMap = keyMap.value.dataOrNull() ?: return diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt new file mode 100644 index 0000000000..364822726d --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt @@ -0,0 +1,154 @@ +package io.github.sds100.keymapper.base.actions + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCase +import io.github.sds100.keymapper.base.constraints.Constraint +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey +import io.github.sds100.keymapper.base.utils.singleKeyTrigger +import io.github.sds100.keymapper.base.utils.triggerKey +import io.github.sds100.keymapper.common.utils.dataOrNull +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class ConfigActionsUseCaseTest { + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var useCase: ConfigActionsUseCaseImpl + private lateinit var configKeyMapState: ConfigKeyMapStateImpl + private lateinit var mockConfigConstraintsUseCase: ConfigConstraintsUseCase + + @Before + fun before() { + configKeyMapState = ConfigKeyMapStateImpl( + testScope, + keyMapRepository = mock(), + floatingButtonRepository = mock() + ) + + mockConfigConstraintsUseCase = mock() + + useCase = ConfigActionsUseCaseImpl( + state = configKeyMapState, + preferenceRepository = mock(), + configConstraints = mockConfigConstraintsUseCase, + defaultKeyMapOptionsUseCase = mock() + ) + } + + @Test + fun `Enable hold down option for key event actions when the trigger is a DPAD button`() = + runTest(testDispatcher) { + configKeyMapState.setKeyMap( + KeyMap( + trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_DPAD_LEFT, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + requiresIme = true + ) + ) + ) + ) + + useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) + + val actionList = useCase.keyMap.value.dataOrNull()!!.actionList + assertThat(actionList[0].holdDown, `is`(true)) + assertThat(actionList[0].repeat, `is`(false)) + } + + /** + * Issue #852. Add a phone ringing constraint when you add an action + * to answer a phone call. + */ + @Test + fun `when add answer phone call action, then add phone ringing constraint`() = + runTest(testDispatcher) { + // GIVEN + configKeyMapState.setKeyMap(KeyMap()) + val action = ActionData.AnswerCall + + // WHEN + useCase.addAction(action) + + // THEN + verify(mockConfigConstraintsUseCase).addConstraint(any()) + } + + /** + * Issue #852. Add a in phone call constraint when you add an action + * to end a phone call. + */ + @Test + fun `when add end phone call action, then add in phone call constraint`() = + runTest(testDispatcher) { + // GIVEN + configKeyMapState.setKeyMap(KeyMap()) + val action = ActionData.EndCall + + // WHEN + useCase.addAction(action) + + // THEN + verify(mockConfigConstraintsUseCase).addConstraint(any()) + } + + /** + * issue #593 + */ + @Test + fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = + runTest(testDispatcher) { + // given + val action = Action( + data = ActionData.TapScreen(100, 100, null), + holdDown = true, + ) + + val keyMap = KeyMap( + 0, + trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_0)), + actionList = listOf(action), + ) + + // when + configKeyMapState.setKeyMap(keyMap) + + // then + assertThat(useCase.keyMap.value.dataOrNull()!!.actionList, `is`(listOf(action))) + } + + @Test + fun `add modifier key event action, enable hold down option and disable repeat option`() = + runTest(testDispatcher) { + KeyEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> + configKeyMapState.setKeyMap(KeyMap()) + + useCase.addAction(ActionData.InputKeyEvent(keyCode)) + + useCase.keyMap.value.dataOrNull()!!.actionList + .single() + .let { + assertThat(it.holdDown, `is`(true)) + assertThat(it.repeat, `is`(false)) + } + } + } + +} \ No newline at end of file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCaseTest.kt similarity index 78% rename from base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt rename to base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCaseTest.kt index 08109e0471..e84e7a0451 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCaseTest.kt @@ -1,35 +1,19 @@ -package io.github.sds100.keymapper.base +package io.github.sds100.keymapper.base.trigger import android.view.KeyEvent -import io.github.sds100.keymapper.base.actions.Action -import io.github.sds100.keymapper.base.actions.ActionData -import io.github.sds100.keymapper.base.constraints.Constraint import io.github.sds100.keymapper.base.keymaps.ClickType -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapController +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey -import io.github.sds100.keymapper.base.trigger.AssistantTriggerType -import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey -import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice -import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey -import io.github.sds100.keymapper.base.trigger.Trigger -import io.github.sds100.keymapper.base.trigger.TriggerMode import io.github.sds100.keymapper.base.utils.parallelTrigger import io.github.sds100.keymapper.base.utils.sequenceTrigger -import io.github.sds100.keymapper.base.utils.singleKeyTrigger import io.github.sds100.keymapper.base.utils.triggerKey import io.github.sds100.keymapper.common.models.EvdevDeviceInfo -import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.dataOrNull -import io.github.sds100.keymapper.system.inputevents.KeyEventUtils -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.instanceOf import org.hamcrest.Matchers.`is` @@ -37,31 +21,37 @@ import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock -@ExperimentalCoroutinesApi -class ConfigKeyMapUseCaseTest { +class ConfigTriggerUseCaseTest { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) - private lateinit var useCase: ConfigKeyMapController + private lateinit var useCase: ConfigTriggerUseCaseImpl + private lateinit var configKeyMapState: ConfigKeyMapStateImpl @Before - fun init() { - useCase = ConfigKeyMapController( - coroutineScope = testScope, - devicesAdapter = mock(), + fun before() { + configKeyMapState = ConfigKeyMapStateImpl( + testScope, keyMapRepository = mock(), + floatingButtonRepository = mock() + ) + + useCase = ConfigTriggerUseCaseImpl( + state = configKeyMapState, preferenceRepository = mock(), - floatingLayoutRepository = mock(), floatingButtonRepository = mock(), - serviceAdapter = mock(), + devicesAdapter = mock(), + floatingLayoutRepository = mock(), + getDefaultKeyMapOptionsUseCase = mock() ) } + @Test fun `Adding a non evdev key deletes all evdev keys in the trigger`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data( + configKeyMapState.setKeyMap( KeyMap( trigger = parallelTrigger( FloatingButtonKey( @@ -118,7 +108,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Adding an evdev key deletes all non evdev keys in the trigger`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data( + configKeyMapState.setKeyMap( KeyMap( trigger = parallelTrigger( FloatingButtonKey( @@ -168,7 +158,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Converting a sequence trigger to parallel trigger removes duplicate evdev keys`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data( + configKeyMapState.setKeyMap( KeyMap( trigger = sequenceTrigger( EvdevTriggerKey( @@ -209,7 +199,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Adding the same evdev trigger key from same device makes the trigger a sequence`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addEvdevTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -240,7 +230,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Adding an evdev trigger key to a sequence trigger keeps it sequence`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data( + configKeyMapState.setKeyMap( KeyMap( trigger = sequenceTrigger( EvdevTriggerKey( @@ -286,7 +276,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Adding the same evdev trigger key code from different devices keeps the trigger parallel`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addEvdevTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -317,7 +307,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Do not allow setting double press for parallel trigger with side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -338,7 +328,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Do not allow setting long press for parallel trigger with side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -358,7 +348,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Do not allow setting double press for side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -371,7 +361,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Do not allow setting long press for side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) @@ -385,7 +375,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Set click type to short press if side key added to double press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -407,7 +397,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Set click type to short press if fingerprint gestures added to double press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -429,7 +419,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Set click type to short press if side key added to long press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -451,7 +441,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Set click type to short press if fingerprint gestures added to long press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -470,31 +460,13 @@ class ConfigKeyMapUseCaseTest { assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) } - @Test - fun `Enable hold down option for key event actions when the trigger is a DPAD button`() = - runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_DPAD_LEFT, - 0, - KeyEventTriggerDevice.Internal, - true, - ) - - useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) - - val actionList = useCase.keyMap.value.dataOrNull()!!.actionList - assertThat(actionList[0].holdDown, `is`(true)) - assertThat(actionList[0].repeat, `is`(false)) - } - /** * This ensures that it isn't possible to have two or more assistant triggers when the mode is parallel. */ @Test fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -518,7 +490,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -542,7 +514,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -567,7 +539,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Set click type to short press when adding assistant key to double press trigger key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -585,7 +557,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `Set click type to short press when adding assistant key to long press trigger key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) useCase.addKeyEventTriggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -616,7 +588,7 @@ class ConfigKeyMapUseCaseTest { ), ) - useCase.keyMap.value = State.Data(keyMap) + configKeyMapState.setKeyMap(keyMap) useCase.setTriggerLongPress() val trigger = useCase.keyMap.value.dataOrNull()!!.trigger @@ -646,7 +618,7 @@ class ConfigKeyMapUseCaseTest { for (modifierKeyCode in modifierKeys) { // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) // WHEN useCase.addKeyEventTriggerKey( @@ -659,7 +631,7 @@ class ConfigKeyMapUseCaseTest { // THEN val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys[0].consumeEvent, `is`(false)) + assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(false)) } } @@ -667,10 +639,10 @@ class ConfigKeyMapUseCaseTest { * Issue #753. */ @Test - fun `when add non-modifier key trigger, do ont enable do not remap option`() = + fun `when add non-modifier key trigger, do not enable do not remap option`() = runTest(testDispatcher) { // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) + configKeyMapState.setKeyMap(KeyMap()) // WHEN useCase.addKeyEventTriggerKey( @@ -683,92 +655,7 @@ class ConfigKeyMapUseCaseTest { // THEN val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys[0].consumeEvent, `is`(true)) - } - - /** - * Issue #852. Add a phone ringing constraint when you add an action - * to answer a phone call. - */ - @Test - fun `when add answer phone call action, then add phone ringing constraint`() = - runTest(testDispatcher) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) - val action = ActionData.AnswerCall - - // WHEN - useCase.addAction(action) - - // THEN - val keyMap = useCase.keyMap.value.dataOrNull()!! - assertThat( - keyMap.constraintState.constraints, - contains(instanceOf(Constraint.PhoneRinging::class.java)), - ) + assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(true)) } - /** - * Issue #852. Add a in phone call constraint when you add an action - * to end a phone call. - */ - @Test - fun `when add end phone call action, then add in phone call constraint`() = - runTest(testDispatcher) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) - val action = ActionData.EndCall - - // WHEN - useCase.addAction(action) - - // THEN - val keyMap = useCase.keyMap.value.dataOrNull()!! - assertThat( - keyMap.constraintState.constraints, - contains(instanceOf(Constraint.InPhoneCall::class.java)), - ) - } - - /** - * issue #593 - */ - @Test - fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = - runTest(testDispatcher) { - // given - val action = Action( - data = ActionData.TapScreen(100, 100, null), - holdDown = true, - ) - - val keyMap = KeyMap( - 0, - trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_0)), - actionList = listOf(action), - ) - - // when - useCase.keyMap.value = State.Data(keyMap) - - // then - assertThat(useCase.keyMap.value.dataOrNull()!!.actionList, `is`(listOf(action))) - } - - @Test - fun `add modifier key event action, enable hold down option and disable repeat option`() = - runTest(testDispatcher) { - KeyEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addAction(ActionData.InputKeyEvent(keyCode)) - - useCase.keyMap.value.dataOrNull()!!.actionList - .single() - .let { - assertThat(it.holdDown, `is`(true)) - assertThat(it.repeat, `is`(false)) - } - } - } -} +} \ No newline at end of file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt index 26216f6913..a3c23989bf 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt @@ -5,9 +5,27 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.system.inputevents.Scancode import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` +import org.junit.After +import org.junit.Before import org.junit.Test +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic class TriggerKeyTest { + + private lateinit var mockedKeyEvent: MockedStatic + + @Before + fun setUp() { + mockedKeyEvent = mockStatic(KeyEvent::class.java) + mockedKeyEvent.`when` { KeyEvent.getMaxKeyCode() }.thenReturn(1000) + } + + @After + fun tearDown() { + mockedKeyEvent.close() + } + @Test fun `detect with scan code if key code is unknown and user setting enabled`() { val triggerKey = KeyEventTriggerKey( From 4b62d03363416b05ab4bd2acc4ebc43b12c46360 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 9 Aug 2025 22:36:58 +0100 Subject: [PATCH 095/215] #1394 extract the core logic for configuring triggers into a separate ConfigTriggerDelegate class --- .../base/trigger/ConfigTriggerDelegate.kt | 518 +++++++++++++ .../base/trigger/ConfigTriggerUseCase.kt | 417 ++--------- .../base/trigger/ConfigTriggerDelegateTest.kt | 686 ++++++++++++++++++ .../base/trigger/ConfigTriggerUseCaseTest.kt | 661 ----------------- .../data/repositories/RoomKeyMapRepository.kt | 3 +- 5 files changed, 1260 insertions(+), 1025 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt create mode 100644 base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt delete mode 100644 base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCaseTest.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt new file mode 100644 index 0000000000..af4379a5d9 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -0,0 +1,518 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.floating.FloatingButtonData +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils + +/** + * This extracts the core logic when configuring a trigger which makes it easier to write tests. + */ +class ConfigTriggerDelegate { + + fun addFloatingButtonTriggerKey( + trigger: Trigger, + buttonUid: String, + button: FloatingButtonData?, + ): Trigger { + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys + .filterIsInstance() + .any { keyToCompare -> keyToCompare.buttonUid == buttonUid } + + val triggerKey = FloatingButtonKey( + buttonUid = buttonUid, + button = button, + clickType = clickType, + ) + + var newKeys = trigger.keys.plus(triggerKey) + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys */ + newKeys.size == 2 && !containsKey -> { + newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } + TriggerMode.Parallel(triggerKey.clickType) + } + + else -> trigger.mode + } + + return trigger.copy(keys = newKeys, mode = newMode) + } + + + fun addAssistantTriggerKey(trigger: Trigger, type: AssistantTriggerType): Trigger { + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsAssistantKey = trigger.keys.any { it is AssistantTriggerKey } + + val triggerKey = AssistantTriggerKey(type = type, clickType = clickType) + + val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsAssistantKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys. + + It must be a short press because long pressing the assistant key isn't supported. + */ + !containsAssistantKey -> TriggerMode.Parallel(ClickType.SHORT_PRESS) + else -> trigger.mode + } + + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun addFingerprintGesture(trigger: Trigger, type: FingerprintGestureType): Trigger { + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsFingerprintGesture = trigger.keys.any { it is FingerprintTriggerKey } + + val triggerKey = FingerprintTriggerKey(type = type, clickType = clickType) + + val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsFingerprintGesture -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys. + + It must be a short press because long pressing the assistant key isn't supported. + */ + !containsFingerprintGesture -> TriggerMode.Parallel(ClickType.SHORT_PRESS) + else -> trigger.mode + } + + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun addKeyEventTriggerKey( + trigger: Trigger, + keyCode: Int, + scanCode: Int, + device: KeyEventTriggerDevice, + requiresIme: Boolean, + ): Trigger { + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys + .filterIsInstance() + .any { keyToCompare -> + keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) + } + + var consumeKeyEvent = true + + // Issue #753 + if (KeyEventUtils.isModifierKey(keyCode)) { + consumeKeyEvent = false + } + + val triggerKey = KeyEventTriggerKey( + keyCode = keyCode, + device = device, + clickType = clickType, + scanCode = scanCode, + consumeEvent = consumeKeyEvent, + requiresIme = requiresIme, + ) + + var newKeys = trigger.keys.filter { it !is EvdevTriggerKey }.plus(triggerKey) + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys */ + newKeys.size == 2 && !containsKey -> { + newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } + TriggerMode.Parallel(triggerKey.clickType) + } + + else -> trigger.mode + } + + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun addEvdevTriggerKey( + trigger: Trigger, + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + ): Trigger { + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys + .filterIsInstance() + .any { keyToCompare -> + keyToCompare.keyCode == keyCode && keyToCompare.device == device + } + + val triggerKey = EvdevTriggerKey( + keyCode = keyCode, + scanCode = scanCode, + device = device, + clickType = clickType, + consumeEvent = true, + ) + + var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey }.plus(triggerKey) + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys */ + newKeys.size == 2 && !containsKey -> { + newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } + TriggerMode.Parallel(triggerKey.clickType) + } + + else -> trigger.mode + } + + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun removeTriggerKey(trigger: Trigger, uid: String): Trigger { + val newKeys = trigger.keys.toMutableList().apply { + removeAll { it.uid == uid } + } + + val newMode = when { + newKeys.size <= 1 -> TriggerMode.Undefined + else -> trigger.mode + } + + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun moveTriggerKey(trigger: Trigger, fromIndex: Int, toIndex: Int): Trigger { + return trigger.copy( + keys = trigger.keys.toMutableList().apply { + add(toIndex, removeAt(fromIndex)) + }, + ) + } + + fun setParallelTriggerMode(trigger: Trigger): Trigger { + if (trigger.mode is TriggerMode.Parallel) { + return trigger + } + + // undefined mode only allowed if one or no keys + if (trigger.keys.size <= 1) { + return trigger.copy(mode = TriggerMode.Undefined) + } + + val oldKeys = trigger.keys + var newKeys = oldKeys + + // set all the keys to a short press if coming from a non-parallel trigger + // because they must all be the same click type and can't all be double pressed + newKeys = newKeys + .map { key -> key.setClickType(clickType = ClickType.SHORT_PRESS) } + // remove duplicates of keys that have the same keycode and device id + .distinctBy { key -> + when (key) { + // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time + is AssistantTriggerKey, is FingerprintTriggerKey -> 0 + is KeyEventTriggerKey -> Pair( + key.keyCode, + key.device, + ) + + is FloatingButtonKey -> key.buttonUid + is EvdevTriggerKey -> Pair( + key.keyCode, + key.device, + ) + } + } + + val newMode = if (newKeys.size <= 1) { + TriggerMode.Undefined + } else { + TriggerMode.Parallel(newKeys[0].clickType) + } + + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun setSequenceTriggerMode(trigger: Trigger): Trigger { + if (trigger.mode == TriggerMode.Sequence) return trigger + // undefined mode only allowed if one or no keys + if (trigger.keys.size <= 1) { + return trigger.copy(mode = TriggerMode.Undefined) + } + + return trigger.copy(mode = TriggerMode.Sequence) + } + + fun setUndefinedTriggerMode(trigger: Trigger): Trigger { + if (trigger.mode == TriggerMode.Undefined) return trigger + + // undefined mode only allowed if one or no keys + if (trigger.keys.size > 1) { + return trigger + } + + return trigger.copy(mode = TriggerMode.Undefined) + } + + fun setTriggerShortPress(trigger: Trigger): Trigger { + if (trigger.mode == TriggerMode.Sequence) { + return trigger + } + + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.SHORT_PRESS) } + val newMode = if (newKeys.size <= 1) { + TriggerMode.Undefined + } else { + TriggerMode.Parallel(ClickType.SHORT_PRESS) + } + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun setTriggerLongPress(trigger: Trigger): Trigger { + if (trigger.mode == TriggerMode.Sequence) { + return trigger + } + + // You can't set the trigger to a long press if it contains a key + // that isn't detected with key codes. This is because there aren't + // separate key events for the up and down press that can be timed. + if (trigger.keys.any { !it.allowedLongPress }) { + return trigger + } + + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.LONG_PRESS) } + val newMode = if (newKeys.size <= 1) { + TriggerMode.Undefined + } else { + TriggerMode.Parallel(ClickType.LONG_PRESS) + } + + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun setTriggerDoublePress(trigger: Trigger): Trigger { + if (trigger.mode != TriggerMode.Undefined) { + return trigger + } + + if (trigger.keys.any { !it.allowedDoublePress }) { + return trigger + } + + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) } + val newMode = TriggerMode.Undefined + + return trigger.copy(keys = newKeys, mode = newMode) + } + + fun setTriggerKeyClickType(trigger: Trigger, keyUid: String, clickType: ClickType): Trigger { + val newKeys = trigger.keys.map { + if (it.uid == keyUid) { + it.setClickType(clickType = clickType) + } else { + it + } + } + + return trigger.copy(keys = newKeys) + } + + fun setTriggerKeyDevice( + trigger: Trigger, + keyUid: String, + device: KeyEventTriggerDevice + ): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid) { + if (key !is KeyEventTriggerKey) { + throw IllegalArgumentException("You can not set the device for non KeyEventTriggerKeys.") + } + + key.copy(device = device) + } else { + key + } + } + + return trigger.copy(keys = newKeys) + } + + fun setTriggerKeyConsumeKeyEvent( + trigger: Trigger, + keyUid: String, + consumeKeyEvent: Boolean + ): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid) { + when (key) { + is KeyEventTriggerKey -> { + key.copy(consumeEvent = consumeKeyEvent) + } + + is EvdevTriggerKey -> { + key.copy(consumeEvent = consumeKeyEvent) + } + + else -> { + key + } + } + } else { + key + } + } + + return trigger.copy(keys = newKeys) + } + + fun setAssistantTriggerKeyType( + trigger: Trigger, + keyUid: String, + type: AssistantTriggerType + ): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid) { + if (key is AssistantTriggerKey) { + key.copy(type = type) + } else { + key + } + } else { + key + } + } + + return trigger.copy(keys = newKeys) + } + + fun setFingerprintGestureType( + trigger: Trigger, + keyUid: String, + type: FingerprintGestureType + ): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid) { + if (key is FingerprintTriggerKey) { + key.copy(type = type) + } else { + key + } + } else { + key + } + } + + return trigger.copy(keys = newKeys) + } + + fun setVibrateEnabled(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(vibrate = enabled) + } + + fun setVibrationDuration( + trigger: Trigger, + duration: Int, + defaultVibrateDuration: Int + ): Trigger { + return if (duration == defaultVibrateDuration) { + trigger.copy(vibrateDuration = null) + } else { + trigger.copy(vibrateDuration = duration) + } + } + + fun setLongPressDelay(trigger: Trigger, delay: Int, defaultLongPressDelay: Int): Trigger { + return if (delay == defaultLongPressDelay) { + trigger.copy(longPressDelay = null) + } else { + trigger.copy(longPressDelay = delay) + } + } + + fun setDoublePressDelay(trigger: Trigger, delay: Int, defaultDoublePressDelay: Int): Trigger { + return if (delay == defaultDoublePressDelay) { + trigger.copy(doublePressDelay = null) + } else { + trigger.copy(doublePressDelay = delay) + } + } + + fun setSequenceTriggerTimeout( + trigger: Trigger, + delay: Int, + defaultSequenceTriggerTimeout: Int + ): Trigger { + return if (delay == defaultSequenceTriggerTimeout) { + trigger.copy(sequenceTriggerTimeout = null) + } else { + trigger.copy(sequenceTriggerTimeout = delay) + } + } + + fun setLongPressDoubleVibrationEnabled(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(longPressDoubleVibration = enabled) + } + + fun setTriggerWhenScreenOff(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(screenOffTrigger = enabled) + } + + fun setTriggerFromOtherAppsEnabled(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(triggerFromOtherApps = enabled) + } + + fun setShowToastEnabled(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(showToast = enabled) + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt index 2a68d6c42f..94abc9d69e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -17,7 +17,6 @@ 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.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -41,6 +40,8 @@ class ConfigTriggerUseCaseImpl @Inject constructor( private val showDeviceDescriptors: Flow = preferenceRepository.get(Keys.showDeviceDescriptors).map { it == true } + private val delegate: ConfigTriggerDelegate = ConfigTriggerDelegate() + override fun setEnabled(enabled: Boolean) { state.update { it.copy(isEnabled = enabled) } } @@ -61,104 +62,16 @@ class ConfigTriggerUseCaseImpl @Inject constructor( } updateTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> keyToCompare.buttonUid == buttonUid } - - val triggerKey = FloatingButtonKey( - buttonUid = buttonUid, - button = button, - clickType = clickType, - ) - - var newKeys = trigger.keys.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) + delegate.addFloatingButtonTriggerKey(trigger, buttonUid, button) } } override fun addAssistantTriggerKey(type: AssistantTriggerType) = updateTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsAssistantKey = trigger.keys.any { it is AssistantTriggerKey } - - val triggerKey = AssistantTriggerKey(type = type, clickType = clickType) - - val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsAssistantKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys. - - It must be a short press because long pressing the assistant key isn't supported. - */ - !containsAssistantKey -> TriggerMode.Parallel(ClickType.SHORT_PRESS) - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) + delegate.addAssistantTriggerKey(trigger, type) } override fun addFingerprintGesture(type: FingerprintGestureType) = updateTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsFingerprintGesture = trigger.keys.any { it is FingerprintTriggerKey } - - val triggerKey = FingerprintTriggerKey(type = type, clickType = clickType) - - val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsFingerprintGesture -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys. - - It must be a short press because long pressing the assistant key isn't supported. - */ - !containsFingerprintGesture -> TriggerMode.Parallel(ClickType.SHORT_PRESS) - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) + delegate.addFingerprintGesture(trigger, type) } override fun addKeyEventTriggerKey( @@ -167,52 +80,13 @@ class ConfigTriggerUseCaseImpl @Inject constructor( device: KeyEventTriggerDevice, requiresIme: Boolean, ) = updateTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) - } - - var consumeKeyEvent = true - - // Issue #753 - if (KeyEventUtils.isModifierKey(keyCode)) { - consumeKeyEvent = false - } - - val triggerKey = KeyEventTriggerKey( - keyCode = keyCode, - device = device, - clickType = clickType, - consumeEvent = consumeKeyEvent, - requiresIme = requiresIme, + delegate.addKeyEventTriggerKey( + trigger, + keyCode, + scanCode, + device, + requiresIme, ) - - var newKeys = trigger.keys.filter { it !is EvdevTriggerKey }.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) } override fun addEvdevTriggerKey( @@ -220,66 +94,20 @@ class ConfigTriggerUseCaseImpl @Inject constructor( scanCode: Int, device: EvdevDeviceInfo, ) = updateTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device == device - } - - val triggerKey = EvdevTriggerKey( - keyCode = keyCode, - scanCode = scanCode, - device = device, - clickType = clickType, - consumeEvent = true, + delegate.addEvdevTriggerKey( + trigger, + keyCode, + scanCode, + device, ) - - var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey }.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) } override fun removeTriggerKey(uid: String) = updateTrigger { trigger -> - val newKeys = trigger.keys.toMutableList().apply { - removeAll { it.uid == uid } - } - - val newMode = when { - newKeys.size <= 1 -> TriggerMode.Undefined - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) + delegate.removeTriggerKey(trigger, uid) } override fun moveTriggerKey(fromIndex: Int, toIndex: Int) = updateTrigger { trigger -> - trigger.copy( - keys = trigger.keys.toMutableList().apply { - add(toIndex, removeAt(fromIndex)) - }, - ) + delegate.moveTriggerKey(trigger, fromIndex, toIndex) } override fun getTriggerKey(uid: String): TriggerKey? { @@ -287,233 +115,111 @@ class ConfigTriggerUseCaseImpl @Inject constructor( } override fun setParallelTriggerMode() = updateTrigger { trigger -> - if (trigger.mode is TriggerMode.Parallel) { - return@updateTrigger trigger - } - - // undefined mode only allowed if one or no keys - if (trigger.keys.size <= 1) { - return@updateTrigger trigger.copy(mode = TriggerMode.Undefined) - } - - val oldKeys = trigger.keys - var newKeys = oldKeys - - // set all the keys to a short press if coming from a non-parallel trigger - // because they must all be the same click type and can't all be double pressed - newKeys = newKeys - .map { key -> key.setClickType(clickType = ClickType.SHORT_PRESS) } - // remove duplicates of keys that have the same keycode and device id - .distinctBy { key -> - when (key) { - // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time - is AssistantTriggerKey, is FingerprintTriggerKey -> 0 - is KeyEventTriggerKey -> Pair( - key.keyCode, - key.device, - ) - - is FloatingButtonKey -> key.buttonUid - is EvdevTriggerKey -> Pair( - key.keyCode, - key.device, - ) - } - } - - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(newKeys[0].clickType) - } - - trigger.copy(keys = newKeys, mode = newMode) + delegate.setParallelTriggerMode(trigger) } override fun setSequenceTriggerMode() = updateTrigger { trigger -> - if (trigger.mode == TriggerMode.Sequence) return@updateTrigger trigger - // undefined mode only allowed if one or no keys - if (trigger.keys.size <= 1) { - return@updateTrigger trigger.copy(mode = TriggerMode.Undefined) - } - - trigger.copy(mode = TriggerMode.Sequence) + delegate.setSequenceTriggerMode(trigger) } override fun setUndefinedTriggerMode() = updateTrigger { trigger -> - if (trigger.mode == TriggerMode.Undefined) return@updateTrigger trigger - - // undefined mode only allowed if one or no keys - if (trigger.keys.size > 1) { - return@updateTrigger trigger - } - - trigger.copy(mode = TriggerMode.Undefined) + delegate.setUndefinedTriggerMode(trigger) } override fun setTriggerShortPress() { - updateTrigger { oldTrigger -> - if (oldTrigger.mode == TriggerMode.Sequence) { - return@updateTrigger oldTrigger - } - - val newKeys = oldTrigger.keys.map { it.setClickType(clickType = ClickType.SHORT_PRESS) } - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(ClickType.SHORT_PRESS) - } - oldTrigger.copy(keys = newKeys, mode = newMode) + updateTrigger { trigger -> + delegate.setTriggerShortPress(trigger) } } override fun setTriggerLongPress() { updateTrigger { trigger -> - if (trigger.mode == TriggerMode.Sequence) { - return@updateTrigger trigger - } - - // You can't set the trigger to a long press if it contains a key - // that isn't detected with key codes. This is because there aren't - // separate key events for the up and down press that can be timed. - if (trigger.keys.any { !it.allowedLongPress }) { - return@updateTrigger trigger - } - - val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.LONG_PRESS) } - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(ClickType.LONG_PRESS) - } - - trigger.copy(keys = newKeys, mode = newMode) + delegate.setTriggerLongPress(trigger) } } override fun setTriggerDoublePress() { updateTrigger { trigger -> - if (trigger.mode != TriggerMode.Undefined) { - return@updateTrigger trigger - } - - if (trigger.keys.any { !it.allowedDoublePress }) { - return@updateTrigger trigger - } - - val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) } - val newMode = TriggerMode.Undefined - - trigger.copy(keys = newKeys, mode = newMode) + delegate.setTriggerDoublePress(trigger) } } override fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) { - updateTriggerKey(keyUid) { key -> - key.setClickType(clickType = clickType) + updateTrigger { trigger -> + delegate.setTriggerKeyClickType(trigger, keyUid, clickType) } } override fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) { - updateTriggerKey(keyUid) { key -> - if (key !is KeyEventTriggerKey) { - throw IllegalArgumentException("You can not set the device for non KeyEventTriggerKeys.") - } - - key.copy(device = device) + updateTrigger { trigger -> + delegate.setTriggerKeyDevice(trigger, keyUid, device) } } override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) { - updateTriggerKey(keyUid) { key -> - when (key) { - is KeyEventTriggerKey -> { - key.copy(consumeEvent = consumeKeyEvent) - } - - is EvdevTriggerKey -> { - key.copy(consumeEvent = consumeKeyEvent) - } - - else -> { - key - } - } + updateTrigger { trigger -> + delegate.setTriggerKeyConsumeKeyEvent(trigger, keyUid, consumeKeyEvent) } } override fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) { - updateTriggerKey(keyUid) { key -> - if (key is AssistantTriggerKey) { - key.copy(type = type) - } else { - key - } + updateTrigger { trigger -> + delegate.setAssistantTriggerKeyType(trigger, keyUid, type) } } override fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) { - updateTriggerKey(keyUid) { key -> - if (key is FingerprintTriggerKey) { - key.copy(type = type) - } else { - key - } + updateTrigger { trigger -> + delegate.setFingerprintGestureType(trigger, keyUid, type) } } - override fun setVibrateEnabled(enabled: Boolean) = updateTrigger { it.copy(vibrate = enabled) } + override fun setVibrateEnabled(enabled: Boolean) = updateTrigger { trigger -> + delegate.setVibrateEnabled(trigger, enabled) + } override fun setVibrationDuration(duration: Int) = updateTrigger { trigger -> - if (duration == defaultVibrateDuration.value) { - trigger.copy(vibrateDuration = null) - } else { - trigger.copy(vibrateDuration = duration) - } + delegate.setVibrationDuration(trigger, duration, defaultVibrateDuration.value) } override fun setLongPressDelay(delay: Int) = updateTrigger { trigger -> - if (delay == defaultLongPressDelay.value) { - trigger.copy(longPressDelay = null) - } else { - trigger.copy(longPressDelay = delay) - } + delegate.setLongPressDelay(trigger, delay, defaultLongPressDelay.value) } override fun setDoublePressDelay(delay: Int) { updateTrigger { trigger -> - if (delay == defaultDoublePressDelay.value) { - trigger.copy(doublePressDelay = null) - } else { - trigger.copy(doublePressDelay = delay) - } + delegate.setDoublePressDelay(trigger, delay, defaultDoublePressDelay.value) } } override fun setSequenceTriggerTimeout(delay: Int) { updateTrigger { trigger -> - if (delay == defaultSequenceTriggerTimeout.value) { - trigger.copy(sequenceTriggerTimeout = null) - } else { - trigger.copy(sequenceTriggerTimeout = delay) - } + delegate.setSequenceTriggerTimeout(trigger, delay, defaultSequenceTriggerTimeout.value) } } override fun setLongPressDoubleVibrationEnabled(enabled: Boolean) { - updateTrigger { it.copy(longPressDoubleVibration = enabled) } + updateTrigger { trigger -> + delegate.setLongPressDoubleVibrationEnabled(trigger, enabled) + } } override fun setTriggerWhenScreenOff(enabled: Boolean) { - updateTrigger { it.copy(screenOffTrigger = enabled) } + updateTrigger { trigger -> + delegate.setTriggerWhenScreenOff(trigger, enabled) + } } override fun setTriggerFromOtherAppsEnabled(enabled: Boolean) { - updateTrigger { it.copy(triggerFromOtherApps = enabled) } + updateTrigger { trigger -> + delegate.setTriggerFromOtherAppsEnabled(trigger, enabled) + } } override fun setShowToastEnabled(enabled: Boolean) { - updateTrigger { it.copy(showToast = enabled) } + updateTrigger { trigger -> + delegate.setShowToastEnabled(trigger, enabled) + } } override fun getAvailableTriggerKeyDevices(): List { @@ -554,21 +260,6 @@ class ConfigTriggerUseCaseImpl @Inject constructor( keyMap.copy(trigger = newTrigger) } } - - private fun updateTriggerKey(uid: String, block: (key: TriggerKey) -> TriggerKey) { - updateTrigger { oldTrigger -> - val newKeys = oldTrigger.keys.map { - if (it.uid == uid) { - block.invoke(it) - } else { - it - } - } - - oldTrigger.copy(keys = newKeys) - } - } - } interface ConfigTriggerUseCase : GetDefaultKeyMapOptionsUseCase { diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt new file mode 100644 index 0000000000..4492721d2d --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt @@ -0,0 +1,686 @@ +package io.github.sds100.keymapper.base.trigger + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.base.utils.parallelTrigger +import io.github.sds100.keymapper.base.utils.sequenceTrigger +import io.github.sds100.keymapper.base.utils.triggerKey +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.hasSize +import org.hamcrest.Matchers.instanceOf +import org.hamcrest.Matchers.`is` +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test + +class ConfigTriggerDelegateTest { + + private lateinit var delegate: ConfigTriggerDelegate + + @Before + fun before() { + delegate = ConfigTriggerDelegate() + } + + /** + * Issue #761 + */ + @Test + fun `Do not enable scan code detection if a key in another key map has the same key code, different scan code and is from a different device`() { + fail() + } + + /** + * Issue #761 + */ + @Test + fun `Do not enable scan code detection if a key in the trigger has the same key code, different scan code and is from a different device`() { + fail() + } + + /** + * Issue #761 + */ + @Test + fun `Enable scan code detection for an evdev trigger if a key in another key map has the same key code but different scan code`() { + fail() + } + + /** + * Issue #761 + */ + @Test + fun `Enable scan code detection for a key event trigger if a key in another key map has the same key code but different scan code`() { + fail() + } + + /** + * Issue #761 + */ + @Test + fun `Enable scan code detection if another key exists in the trigger with the same key code but different scan code`() { + fail() + } + + @Test + fun `Adding a non evdev key deletes all evdev keys in the trigger`() { + val trigger = parallelTrigger( + FloatingButtonKey( + buttonUid = "floating_button", + button = null, + clickType = ClickType.SHORT_PRESS, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ), + ), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 100, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ), + ) + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger, + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false, + ) + + assertThat(newTrigger.keys, hasSize(3)) + assertThat(newTrigger.keys[0], instanceOf(FloatingButtonKey::class.java)) + assertThat(newTrigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + assertThat(newTrigger.keys[2], instanceOf(KeyEventTriggerKey::class.java)) + assertThat( + (newTrigger.keys[2] as KeyEventTriggerKey).requiresIme, + `is`(false), + ) + } + + @Test + fun `Adding an evdev key deletes all non evdev keys in the trigger`() { + val trigger = parallelTrigger( + FloatingButtonKey( + buttonUid = "floating_button", + button = null, + clickType = ClickType.SHORT_PRESS, + ), + triggerKey( + KeyEvent.KEYCODE_VOLUME_UP, + KeyEventTriggerDevice.Internal, + ), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEventTriggerDevice.Internal, + ), + ) + + val evdevDevice = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = evdevDevice, + ) + + assertThat(newTrigger.keys, hasSize(3)) + assertThat(newTrigger.keys[0], instanceOf(FloatingButtonKey::class.java)) + assertThat(newTrigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + assertThat(newTrigger.keys[2], instanceOf(EvdevTriggerKey::class.java)) + assertThat( + (newTrigger.keys[2] as EvdevTriggerKey).device, + `is`(evdevDevice), + ) + } + + @Test + fun `Converting a sequence trigger to parallel trigger removes duplicate evdev keys`() { + val trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ), + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ), + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0], instanceOf(EvdevTriggerKey::class.java)) + assertThat( + (newTrigger.keys[0] as EvdevTriggerKey).keyCode, + `is`(KeyEvent.KEYCODE_VOLUME_DOWN), + ) + } + + @Test + fun `Adding the same evdev trigger key from same device makes the trigger a sequence`() { + val emptyTrigger = Trigger() + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val triggerWithFirstKey = delegate.addEvdevTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device, + ) + + val triggerWithSecondKey = delegate.addEvdevTriggerKey( + trigger = triggerWithFirstKey, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device, + ) + + assertThat(triggerWithSecondKey.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding an evdev trigger key to a sequence trigger keeps it sequence`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device, + ), + ) + + // Add a third key and it should still be a sequence trigger now + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 0, + device = device, + ) + + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding the same evdev trigger key code from different devices keeps the trigger parallel`() { + val emptyTrigger = Trigger() + val device1 = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + val device2 = EvdevDeviceInfo( + name = "Fake Controller", + bus = 1, + vendor = 2, + product = 1, + ) + + val triggerWithFirstKey = delegate.addEvdevTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device1, + ) + + val triggerWithSecondKey = delegate.addEvdevTriggerKey( + trigger = triggerWithFirstKey, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device2, + ) + + assertThat(triggerWithSecondKey.mode, `is`(TriggerMode.Parallel(ClickType.SHORT_PRESS))) + } + + @Test + fun `Do not allow setting double press for parallel trigger with side key`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithKeyEvent, + type = AssistantTriggerType.ANY + ) + + val finalTrigger = delegate.setTriggerDoublePress(triggerWithAssistant) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting long press for parallel trigger with side key`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithKeyEvent, + type = AssistantTriggerType.ANY + ) + + val finalTrigger = delegate.setTriggerLongPress(triggerWithAssistant) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting double press for side key`() { + val emptyTrigger = Trigger() + + val triggerWithAssistant = delegate.addAssistantTriggerKey( + trigger = emptyTrigger, + type = AssistantTriggerType.ANY + ) + + val finalTrigger = delegate.setTriggerDoublePress(triggerWithAssistant) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Undefined)) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting long press for side key`() { + val emptyTrigger = Trigger() + + val triggerWithAssistant = delegate.addAssistantTriggerKey( + trigger = emptyTrigger, + type = AssistantTriggerType.ANY + ) + + val finalTrigger = delegate.setTriggerLongPress(triggerWithAssistant) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Undefined)) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if side key added to double press volume button`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithDoublePress = delegate.setTriggerDoublePress(triggerWithKeyEvent) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithDoublePress, + type = AssistantTriggerType.ANY + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if fingerprint gestures added to double press volume button`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithDoublePress = delegate.setTriggerDoublePress(triggerWithKeyEvent) + + val finalTrigger = delegate.addFingerprintGesture( + trigger = triggerWithDoublePress, + type = FingerprintGestureType.SWIPE_UP + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if side key added to long press volume button`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithLongPress = delegate.setTriggerLongPress(triggerWithKeyEvent) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithLongPress, + type = AssistantTriggerType.ANY + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if fingerprint gestures added to long press volume button`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithLongPress = delegate.setTriggerLongPress(triggerWithKeyEvent) + + val finalTrigger = delegate.addFingerprintGesture( + trigger = triggerWithLongPress, + type = FingerprintGestureType.SWIPE_UP + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + /** + * This ensures that it isn't possible to have two or more assistant triggers when the mode is parallel. + */ + @Test + fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithVoiceAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithKeyEvent, + type = AssistantTriggerType.VOICE + ) + + val triggerWithDeviceAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithVoiceAssistant, + type = AssistantTriggerType.DEVICE + ) + + val finalTrigger = delegate.setParallelTriggerMode(triggerWithDeviceAssistant) + + assertThat(finalTrigger.keys, hasSize(2)) + assertThat( + finalTrigger.keys[0], + instanceOf(KeyEventTriggerKey::class.java), + ) + assertThat(finalTrigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + } + + @Test + fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithDeviceAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithKeyEvent, + type = AssistantTriggerType.DEVICE + ) + + val triggerWithVoiceAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithDeviceAssistant, + type = AssistantTriggerType.VOICE + ) + + val finalTrigger = delegate.setParallelTriggerMode(triggerWithVoiceAssistant) + + assertThat(finalTrigger.keys, hasSize(2)) + assertThat( + finalTrigger.keys[0], + instanceOf(KeyEventTriggerKey::class.java), + ) + assertThat(finalTrigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + } + + @Test + fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() { + val emptyTrigger = Trigger() + + val triggerWithFirstKey = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithSecondKey = delegate.addKeyEventTriggerKey( + trigger = triggerWithFirstKey, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithLongPress = delegate.setTriggerLongPress(triggerWithSecondKey) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithLongPress, + type = AssistantTriggerType.ANY + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } + + @Test + fun `Set click type to short press when adding assistant key to double press trigger key`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithDoublePress = delegate.setTriggerDoublePress(triggerWithKeyEvent) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithDoublePress, + type = AssistantTriggerType.ANY + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } + + @Test + fun `Set click type to short press when adding assistant key to long press trigger key`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithLongPress = delegate.setTriggerLongPress(triggerWithKeyEvent) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithLongPress, + type = AssistantTriggerType.ANY + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } + + @Test + fun `Do not allow long press for parallel trigger with assistant key`() { + val trigger = Trigger( + mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS), + keys = listOf( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), + ), + ) + + val finalTrigger = delegate.setTriggerLongPress(trigger) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } + + /** + * Issue #753. If a modifier key is used as a trigger then it the + * option to not override the default action must be chosen so that the modifier + * key can still be used normally. + */ + @Test + fun `when add modifier key trigger, enable do not remap option`() { + val modifierKeys = setOf( + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_ALT_RIGHT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_CTRL_RIGHT, + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_META_RIGHT, + KeyEvent.KEYCODE_SYM, + KeyEvent.KEYCODE_NUM, + KeyEvent.KEYCODE_FUNCTION, + ) + + for (modifierKeyCode in modifierKeys) { + // GIVEN + val emptyTrigger = Trigger() + + // WHEN + val trigger = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = modifierKeyCode, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + // THEN + assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(false)) + } + } + + /** + * Issue #753. + */ + @Test + fun `when add non-modifier key trigger, do not enable do not remap option`() { + // GIVEN + val emptyTrigger = Trigger() + + // WHEN + val trigger = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_A, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + // THEN + assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(true)) + } + +} \ No newline at end of file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCaseTest.kt deleted file mode 100644 index e84e7a0451..0000000000 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCaseTest.kt +++ /dev/null @@ -1,661 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -import android.view.KeyEvent -import io.github.sds100.keymapper.base.keymaps.ClickType -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl -import io.github.sds100.keymapper.base.keymaps.KeyMap -import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.utils.parallelTrigger -import io.github.sds100.keymapper.base.utils.sequenceTrigger -import io.github.sds100.keymapper.base.utils.triggerKey -import io.github.sds100.keymapper.common.models.EvdevDeviceInfo -import io.github.sds100.keymapper.common.utils.dataOrNull -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.hasSize -import org.hamcrest.Matchers.instanceOf -import org.hamcrest.Matchers.`is` -import org.junit.Before -import org.junit.Test -import org.mockito.kotlin.mock - -class ConfigTriggerUseCaseTest { - - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) - - private lateinit var useCase: ConfigTriggerUseCaseImpl - private lateinit var configKeyMapState: ConfigKeyMapStateImpl - - @Before - fun before() { - configKeyMapState = ConfigKeyMapStateImpl( - testScope, - keyMapRepository = mock(), - floatingButtonRepository = mock() - ) - - useCase = ConfigTriggerUseCaseImpl( - state = configKeyMapState, - preferenceRepository = mock(), - floatingButtonRepository = mock(), - devicesAdapter = mock(), - floatingLayoutRepository = mock(), - getDefaultKeyMapOptionsUseCase = mock() - ) - } - - - @Test - fun `Adding a non evdev key deletes all evdev keys in the trigger`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap( - KeyMap( - trigger = parallelTrigger( - FloatingButtonKey( - buttonUid = "floating_button", - button = null, - clickType = ClickType.SHORT_PRESS, - ), - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_UP, - scanCode = 123, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ), - AssistantTriggerKey( - type = AssistantTriggerType.ANY, - clickType = ClickType.SHORT_PRESS, - ), - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 100, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ), - ), - ), - ) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(3)) - assertThat(trigger.keys[0], instanceOf(FloatingButtonKey::class.java)) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - assertThat(trigger.keys[2], instanceOf(KeyEventTriggerKey::class.java)) - assertThat( - (trigger.keys[2] as KeyEventTriggerKey).requiresIme, - `is`(false), - ) - } - - @Test - fun `Adding an evdev key deletes all non evdev keys in the trigger`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap( - KeyMap( - trigger = parallelTrigger( - FloatingButtonKey( - buttonUid = "floating_button", - button = null, - clickType = ClickType.SHORT_PRESS, - ), - triggerKey( - KeyEvent.KEYCODE_VOLUME_UP, - KeyEventTriggerDevice.Internal, - ), - AssistantTriggerKey( - type = AssistantTriggerType.ANY, - clickType = ClickType.SHORT_PRESS, - ), - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - KeyEventTriggerDevice.Internal, - ), - ), - ), - ) - - val evdevDevice = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ) - useCase.addEvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - device = evdevDevice, - ) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(3)) - assertThat(trigger.keys[0], instanceOf(FloatingButtonKey::class.java)) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - assertThat(trigger.keys[2], instanceOf(EvdevTriggerKey::class.java)) - assertThat( - (trigger.keys[2] as EvdevTriggerKey).device, - `is`(evdevDevice), - ) - } - - @Test - fun `Converting a sequence trigger to parallel trigger removes duplicate evdev keys`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap( - KeyMap( - trigger = sequenceTrigger( - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ), - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ), - ), - ), - ) - - useCase.setParallelTriggerMode() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(1)) - assertThat(trigger.keys[0], instanceOf(EvdevTriggerKey::class.java)) - assertThat( - (trigger.keys[0] as EvdevTriggerKey).keyCode, - `is`(KeyEvent.KEYCODE_VOLUME_DOWN), - ) - } - - @Test - fun `Adding the same evdev trigger key from same device makes the trigger a sequence`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addEvdevTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ) - - useCase.addEvdevTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Sequence)) - } - - @Test - fun `Adding an evdev trigger key to a sequence trigger keeps it sequence`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap( - KeyMap( - trigger = sequenceTrigger( - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ), - EvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, - scanCode = 0, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ), - ), - ), - ) - - // Add a third key and it should still be a sequence trigger now - useCase.addEvdevTriggerKey( - keyCode = KeyEvent.KEYCODE_VOLUME_UP, - scanCode = 0, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Sequence)) - } - - @Test - fun `Adding the same evdev trigger key code from different devices keeps the trigger parallel`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addEvdevTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - device = EvdevDeviceInfo( - name = "Volume Keys", - bus = 0, - vendor = 1, - product = 2, - ), - ) - - useCase.addEvdevTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - device = EvdevDeviceInfo( - name = "Fake Controller", - bus = 1, - vendor = 2, - product = 1, - ), - ) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(ClickType.SHORT_PRESS))) - } - - @Test - fun `Do not allow setting double press for parallel trigger with side key`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - useCase.setTriggerDoublePress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Do not allow setting long press for parallel trigger with side key`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - useCase.setTriggerLongPress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Do not allow setting double press for side key`() = runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - useCase.setTriggerDoublePress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Undefined)) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Do not allow setting long press for side key`() = runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - useCase.setTriggerLongPress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Undefined)) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Set click type to short press if side key added to double press volume button`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - - useCase.setTriggerDoublePress() - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Set click type to short press if fingerprint gestures added to double press volume button`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - - useCase.setTriggerDoublePress() - - useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Set click type to short press if side key added to long press volume button`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - - useCase.setTriggerLongPress() - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Set click type to short press if fingerprint gestures added to long press volume button`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - - useCase.setTriggerLongPress() - - useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - /** - * This ensures that it isn't possible to have two or more assistant triggers when the mode is parallel. - */ - @Test - fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) - useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) - useCase.setParallelTriggerMode() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(2)) - assertThat( - trigger.keys[0], - instanceOf(KeyEventTriggerKey::class.java), - ) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - } - - @Test - fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) - useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) - useCase.setParallelTriggerMode() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(2)) - assertThat( - trigger.keys[0], - instanceOf(KeyEventTriggerKey::class.java), - ) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - } - - @Test - fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_UP, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - useCase.setTriggerLongPress() - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } - - @Test - fun `Set click type to short press when adding assistant key to double press trigger key`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - useCase.setTriggerDoublePress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } - - @Test - fun `Set click type to short press when adding assistant key to long press trigger key`() = - runTest(testDispatcher) { - configKeyMapState.setKeyMap(KeyMap()) - - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - useCase.setTriggerLongPress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } - - @Test - fun `Do not allow long press for parallel trigger with assistant key`() = - runTest(testDispatcher) { - val keyMap = KeyMap( - trigger = Trigger( - mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS), - keys = listOf( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), - AssistantTriggerKey( - type = AssistantTriggerType.ANY, - clickType = ClickType.SHORT_PRESS, - ), - ), - ), - ) - - configKeyMapState.setKeyMap(keyMap) - useCase.setTriggerLongPress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } - - /** - * Issue #753. If a modifier key is used as a trigger then it the - * option to not override the default action must be chosen so that the modifier - * key can still be used normally. - */ - @Test - fun `when add modifier key trigger, enable do not remap option`() = runTest(testDispatcher) { - val modifierKeys = setOf( - KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_SHIFT_RIGHT, - KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_ALT_RIGHT, - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_CTRL_RIGHT, - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_META_RIGHT, - KeyEvent.KEYCODE_SYM, - KeyEvent.KEYCODE_NUM, - KeyEvent.KEYCODE_FUNCTION, - ) - - for (modifierKeyCode in modifierKeys) { - // GIVEN - configKeyMapState.setKeyMap(KeyMap()) - - // WHEN - useCase.addKeyEventTriggerKey( - modifierKeyCode, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - - // THEN - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - - assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(false)) - } - } - - /** - * Issue #753. - */ - @Test - fun `when add non-modifier key trigger, do not enable do not remap option`() = - runTest(testDispatcher) { - // GIVEN - configKeyMapState.setKeyMap(KeyMap()) - - // WHEN - useCase.addKeyEventTriggerKey( - KeyEvent.KEYCODE_A, - 0, - KeyEventTriggerDevice.Internal, - false, - ) - - // THEN - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - - assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(true)) - } - -} \ No newline at end of file diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt index dd05f6d82d..9a5500646a 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt @@ -12,6 +12,7 @@ import io.github.sds100.keymapper.data.migration.fingerprintmaps.FingerprintToKe import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -34,7 +35,7 @@ class RoomKeyMapRepository @Inject constructor( private const val MAX_KEY_MAP_BATCH_SIZE = 200 } - override val keyMapList = keyMapDao.getAll() + override val keyMapList: StateFlow>> = keyMapDao.getAll() .map { State.Data(it) } .flowOn(dispatchers.io()) .stateIn(coroutineScope, SharingStarted.Eagerly, State.Loading) From 7ca9b9efcbf5fae712ad16658ea62707bf881c83 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 00:00:09 +0100 Subject: [PATCH 096/215] #1394 #761 automatically use scan code detection for a trigger if it conflicts with other triggers --- .../trigger/BaseConfigTriggerViewModel.kt | 3 +- .../base/trigger/ConfigTriggerDelegate.kt | 48 +++++ .../base/trigger/ConfigTriggerUseCase.kt | 42 +++- .../base/trigger/KeyCodeTriggerKey.kt | 5 +- base/src/main/res/values/strings.xml | 1 + .../base/trigger/ConfigTriggerDelegateTest.kt | 186 +++++++++++++++++- .../data/entities/TriggerKeyEntity.kt | 2 + 7 files changed, 271 insertions(+), 16 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index f470c1ca61..930412ddcb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -528,7 +528,7 @@ abstract class BaseConfigTriggerViewModel( } } - private fun onRecordEvdevEvent(key: RecordedKey.EvdevEvent) { + private suspend fun onRecordEvdevEvent(key: RecordedKey.EvdevEvent) { config.addEvdevTriggerKey( key.keyCode, key.scanCode, @@ -925,6 +925,7 @@ sealed class TriggerKeyOptionsState { abstract val showClickTypes: Boolean abstract val showLongPressClickType: Boolean + // TODO add isScanCodeSettingEnabled field and isScanCodeDetectionEnabled data class KeyEvent( val doNotRemapChecked: Boolean = false, override val clickType: ClickType, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index af4379a5d9..03f74853cb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -116,12 +116,17 @@ class ConfigTriggerDelegate { return trigger.copy(keys = newKeys, mode = newMode) } + /** + * @param otherTriggerKeys This needs to check the other triggers in the app so that it can + * enable scancode detection by default in some situations. + */ fun addKeyEventTriggerKey( trigger: Trigger, keyCode: Int, scanCode: Int, device: KeyEventTriggerDevice, requiresIme: Boolean, + otherTriggerKeys: List = emptyList() ): Trigger { val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -144,6 +149,12 @@ class ConfigTriggerDelegate { consumeKeyEvent = false } + // Scan code detection should be turned on by default if there are other + // keys from the same device that report the same key code but have a different scan code. + val conflictingKeys = otherTriggerKeys.plus(trigger.keys) + .filterIsInstance() + .filter { it.isConflictingKey(keyCode, scanCode, device) } + val triggerKey = KeyEventTriggerKey( keyCode = keyCode, device = device, @@ -151,6 +162,7 @@ class ConfigTriggerDelegate { scanCode = scanCode, consumeEvent = consumeKeyEvent, requiresIme = requiresIme, + detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty() ) var newKeys = trigger.keys.filter { it !is EvdevTriggerKey }.plus(triggerKey) @@ -172,11 +184,26 @@ class ConfigTriggerDelegate { return trigger.copy(keys = newKeys, mode = newMode) } + /** + * This will return true if the key has same key code but different + * scan code, and is from the same device. + */ + private fun KeyEventTriggerKey.isConflictingKey( + keyCode: Int, + scanCode: Int, + device: KeyEventTriggerDevice, + ): Boolean { + return this.keyCode == keyCode + && this.scanCode != scanCode + && this.device.isSameDevice(device) + } + fun addEvdevTriggerKey( trigger: Trigger, keyCode: Int, scanCode: Int, device: EvdevDeviceInfo, + otherTriggerKeys: List = emptyList() ): Trigger { val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -192,12 +219,19 @@ class ConfigTriggerDelegate { keyToCompare.keyCode == keyCode && keyToCompare.device == device } + // Scan code detection should be turned on by default if there are other + // keys from the same device that report the same key code but have a different scan code. + val conflictingKeys = otherTriggerKeys.plus(trigger.keys) + .filterIsInstance() + .filter { it.isConflictingKey(keyCode, scanCode, device) } + val triggerKey = EvdevTriggerKey( keyCode = keyCode, scanCode = scanCode, device = device, clickType = clickType, consumeEvent = true, + detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty() ) var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey }.plus(triggerKey) @@ -219,6 +253,20 @@ class ConfigTriggerDelegate { return trigger.copy(keys = newKeys, mode = newMode) } + /** + * This will return true if the key has same key code but different + * scan code, and is from the same device. + */ + private fun EvdevTriggerKey.isConflictingKey( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + ): Boolean { + return this.keyCode == keyCode + && this.scanCode != scanCode + && this.device == device + } + fun removeTriggerKey(trigger: Trigger, uid: String): Trigger { val newKeys = trigger.keys.toMutableList().apply { removeAll { it.uid == uid } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt index 94abc9d69e..730c64292a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -13,13 +13,21 @@ import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.entities.AssistantTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.EvdevTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.FingerprintTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.FloatingButtonKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyMapEntity import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository import io.github.sds100.keymapper.data.repositories.FloatingLayoutRepository +import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.devices.DevicesAdapter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import javax.inject.Inject @@ -31,7 +39,8 @@ class ConfigTriggerUseCaseImpl @Inject constructor( private val floatingButtonRepository: FloatingButtonRepository, private val devicesAdapter: DevicesAdapter, private val floatingLayoutRepository: FloatingLayoutRepository, - private val getDefaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase + private val getDefaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase, + private val keyMapRepository: KeyMapRepository ) : ConfigTriggerUseCase, GetDefaultKeyMapOptionsUseCase by getDefaultKeyMapOptionsUseCase { override val keyMap: StateFlow> = state.keyMap @@ -42,6 +51,25 @@ class ConfigTriggerUseCaseImpl @Inject constructor( private val delegate: ConfigTriggerDelegate = ConfigTriggerDelegate() + // This class is viewmodel scoped so this will be recomputed each time + // the user starts configuring a key map + private val otherTriggerKeys: List by lazy { + keyMapRepository.keyMapList + .filterIsInstance>>() + .map { state -> state.data.flatMap { it.trigger.keys } } + .map { keys -> + keys + .mapNotNull { key -> + when (key) { + is EvdevTriggerKeyEntity -> EvdevTriggerKey.fromEntity(key) + is KeyEventTriggerKeyEntity -> KeyEventTriggerKey.fromEntity(key) + is AssistantTriggerKeyEntity, is FingerprintTriggerKeyEntity, is FloatingButtonKeyEntity -> null + } + }.filterIsInstance() + + }.firstBlocking() + } + override fun setEnabled(enabled: Boolean) { state.update { it.copy(isEnabled = enabled) } } @@ -74,11 +102,11 @@ class ConfigTriggerUseCaseImpl @Inject constructor( delegate.addFingerprintGesture(trigger, type) } - override fun addKeyEventTriggerKey( + override suspend fun addKeyEventTriggerKey( keyCode: Int, scanCode: Int, device: KeyEventTriggerDevice, - requiresIme: Boolean, + requiresIme: Boolean ) = updateTrigger { trigger -> delegate.addKeyEventTriggerKey( trigger, @@ -86,10 +114,11 @@ class ConfigTriggerUseCaseImpl @Inject constructor( scanCode, device, requiresIme, + otherTriggerKeys = otherTriggerKeys ) } - override fun addEvdevTriggerKey( + override suspend fun addEvdevTriggerKey( keyCode: Int, scanCode: Int, device: EvdevDeviceInfo, @@ -99,6 +128,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( keyCode, scanCode, device, + otherTriggerKeys = otherTriggerKeys ) } @@ -269,7 +299,7 @@ interface ConfigTriggerUseCase : GetDefaultKeyMapOptionsUseCase { fun setEnabled(enabled: Boolean) // trigger - fun addKeyEventTriggerKey( + suspend fun addKeyEventTriggerKey( keyCode: Int, scanCode: Int, device: KeyEventTriggerDevice, @@ -279,7 +309,7 @@ interface ConfigTriggerUseCase : GetDefaultKeyMapOptionsUseCase { suspend fun addFloatingButtonTriggerKey(buttonUid: String) fun addAssistantTriggerKey(type: AssistantTriggerType) fun addFingerprintGesture(type: FingerprintGestureType) - fun addEvdevTriggerKey( + suspend fun addEvdevTriggerKey( keyCode: Int, scanCode: Int, device: EvdevDeviceInfo, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt index bfda8b4bf5..95f08e3dd7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt @@ -43,8 +43,11 @@ fun KeyCodeTriggerKey.isKeyCodeUnknown(): Boolean { */ fun KeyCodeTriggerKey.getCodeLabel(resourceProvider: ResourceProvider): String { if (detectWithScancode() && scanCode != null) { - return ScancodeStrings.getScancodeLabel(scanCode!!) + val codeLabel = ScancodeStrings.getScancodeLabel(scanCode!!) ?: resourceProvider.getString(R.string.trigger_key_unknown_scan_code, scanCode!!) + + return "$codeLabel (${resourceProvider.getString(R.string.trigger_key_scan_code_detection_flag)})" + } else { return KeyCodeStrings.keyCodeToString(keyCode) ?: resourceProvider.getString(R.string.trigger_key_unknown_key_code, keyCode) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 4f61bdd366..637efce6e0 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1517,6 +1517,7 @@ Advanced triggers Key code %d Scan code %d + Scan code Remove diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt index 4492721d2d..3d935221ab 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt @@ -5,13 +5,13 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.parallelTrigger import io.github.sds100.keymapper.base.utils.sequenceTrigger +import io.github.sds100.keymapper.base.utils.singleKeyTrigger import io.github.sds100.keymapper.base.utils.triggerKey import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.instanceOf import org.hamcrest.Matchers.`is` -import org.junit.Assert.fail import org.junit.Before import org.junit.Test @@ -29,7 +29,38 @@ class ConfigTriggerDelegateTest { */ @Test fun `Do not enable scan code detection if a key in another key map has the same key code, different scan code and is from a different device`() { - fail() + val device1 = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ) + + val device2 = KeyEventTriggerDevice.External( + descriptor = "keyboard1", + name = "Other Keyboard", + ) + + val otherTriggers = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device1, + clickType = ClickType.SHORT_PRESS + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger = Trigger(), + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device2, // Different device + requiresIme = false, + otherTriggers + ) + + assertThat( + (newTrigger.keys[0] as KeyEventTriggerKey).detectWithScanCodeUserSetting, + `is`(false), + ) } /** @@ -37,7 +68,37 @@ class ConfigTriggerDelegateTest { */ @Test fun `Do not enable scan code detection if a key in the trigger has the same key code, different scan code and is from a different device`() { - fail() + val device1 = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ) + + val device2 = KeyEventTriggerDevice.External( + descriptor = "keyboard1", + name = "Other Keyboard", + ) + + val trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device1, + clickType = ClickType.SHORT_PRESS + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device2, // Different device + requiresIme = false, + ) + + assertThat( + (newTrigger.keys[1] as KeyEventTriggerKey).detectWithScanCodeUserSetting, + `is`(false), + ) } /** @@ -45,7 +106,33 @@ class ConfigTriggerDelegateTest { */ @Test fun `Enable scan code detection for an evdev trigger if a key in another key map has the same key code but different scan code`() { - fail() + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val otherTriggers = listOf( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device, + ), + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = Trigger(), + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device, + otherTriggers + ) + + assertThat( + (newTrigger.keys[0] as EvdevTriggerKey).detectWithScanCodeUserSetting, + `is`(true), + ) } /** @@ -53,15 +140,99 @@ class ConfigTriggerDelegateTest { */ @Test fun `Enable scan code detection for a key event trigger if a key in another key map has the same key code but different scan code`() { - fail() + val device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ) + + val otherTriggers = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device, + clickType = ClickType.SHORT_PRESS + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger = Trigger(), + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device, + requiresIme = false, + otherTriggers + ) + + assertThat( + (newTrigger.keys[0] as KeyEventTriggerKey).detectWithScanCodeUserSetting, + `is`(true), + ) } /** * Issue #761 */ @Test - fun `Enable scan code detection if another key exists in the trigger with the same key code but different scan code`() { - fail() + fun `Enable scan code detection if another key event key exists in the trigger with the same key code but different scan code`() { + val device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ) + + val trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device, + clickType = ClickType.SHORT_PRESS + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device, + requiresIme = false, + ) + + assertThat( + (newTrigger.keys[1] as KeyEventTriggerKey).detectWithScanCodeUserSetting, + `is`(true), + ) + } + + /** + * Issue #761 + */ + @Test + fun `Enable scan code detection if another evdev key exists in the trigger with the same key code but different scan code`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device, + ), + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device, + ) + + assertThat( + (newTrigger.keys[1] as EvdevTriggerKey).detectWithScanCodeUserSetting, + `is`(true), + ) } @Test @@ -682,5 +853,4 @@ class ConfigTriggerDelegateTest { // THEN assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(true)) } - } \ No newline at end of file diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt index d8370e2c9e..cda4075f50 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt @@ -132,6 +132,7 @@ sealed class TriggerKeyEntity : Parcelable { uid: String, ): KeyEventTriggerKeyEntity { val keyCode by json.byInt(KeyEventTriggerKeyEntity.NAME_KEYCODE) + val scanCode by json.byInt(KeyEventTriggerKeyEntity.NAME_SCANCODE) val deviceId by json.byString(KeyEventTriggerKeyEntity.NAME_DEVICE_ID) val deviceName by json.byNullableString(KeyEventTriggerKeyEntity.NAME_DEVICE_NAME) val clickType by json.byInt(NAME_CLICK_TYPE) @@ -144,6 +145,7 @@ sealed class TriggerKeyEntity : Parcelable { clickType, flags ?: 0, uid, + scanCode ) } } From 4419f52f3af6388d657bfc8e4083742b9e23ac64 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 00:01:14 +0100 Subject: [PATCH 097/215] fix: use title case for volume key code strings --- .../io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt index d1e4f2cc52..b74936b4e9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt @@ -33,8 +33,8 @@ object KeyCodeStrings { * Maps keys which aren't single characters like the Control keys to a string representation */ private val NON_CHARACTER_KEY_LABELS: Map = mapOf( - KeyEvent.KEYCODE_VOLUME_DOWN to "Volume down", - KeyEvent.KEYCODE_VOLUME_UP to "Volume up", + KeyEvent.KEYCODE_VOLUME_DOWN to "Volume Down", + KeyEvent.KEYCODE_VOLUME_UP to "Volume Up", KeyEvent.KEYCODE_CTRL_LEFT to "Ctrl Left", KeyEvent.KEYCODE_CTRL_RIGHT to "Ctrl Right", From e99559ed92e5a1a8107a54dec5cfe91fd707295b Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 00:05:10 +0100 Subject: [PATCH 098/215] #1394 lock evdev devices when getting evdev devices --- sysbridge/src/main/cpp/libevdev_jni.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 62db424403..c0d18ba7eb 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -630,15 +630,18 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_getEvdevDevicesNa bool ignoreDevice = false; - // Ignore this device if it is a uinput device we created - for (const auto &pair: *evdevDevices) { - DeviceContext context = pair.second; - const char *uinputDevicePath = libevdev_uinput_get_devnode(context.uinputDev); - - if (strcmp(fullPath, uinputDevicePath) == 0) { - LOGW("Ignoring uinput device %s.", uinputDevicePath); - ignoreDevice = true; - break; + { + std::lock_guard lock(evdevDevicesMutex); + // Ignore this device if it is a uinput device we created + for (const auto &pair: *evdevDevices) { + DeviceContext context = pair.second; + const char *uinputDevicePath = libevdev_uinput_get_devnode(context.uinputDev); + + if (strcmp(fullPath, uinputDevicePath) == 0) { + LOGW("Ignoring uinput device %s.", uinputDevicePath); + ignoreDevice = true; + break; + } } } From 6efaf701c8b3f41c2c8cc6322d433ea96b4752f9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 00:37:43 +0100 Subject: [PATCH 099/215] #761 add button to turn on scan code detection --- CHANGELOG.md | 5 ++ .../trigger/BaseConfigTriggerViewModel.kt | 19 +++- .../base/trigger/BaseTriggerScreen.kt | 3 +- .../base/trigger/ConfigTriggerDelegate.kt | 20 +++++ .../base/trigger/ConfigTriggerUseCase.kt | 7 ++ .../base/trigger/KeyCodeTriggerKey.kt | 4 + .../trigger/TriggerKeyOptionsBottomSheet.kt | 87 ++++++++++++++++++- base/src/main/res/values/strings.xml | 3 + .../keymapper/base/trigger/TriggerKeyTest.kt | 36 ++++++++ 9 files changed, 180 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d69afd0a98..99d528f322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ #### TO BE RELEASED +## Added + +- #761 Detect keys with scancodes. Key Mapper will do this automatically if the key code is unknown + or you record different physical keys from the same device with the same key code. + ## Removed - The key event relay service is now also used on all Android versions below Android 14. The diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 930412ddcb..25390646a3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -374,6 +374,8 @@ abstract class BaseConfigTriggerViewModel( clickType = key.clickType, showClickTypes = showClickTypes, devices = deviceListItems, + isScanCodeDetectionSelected = key.detectWithScancode(), + isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable() ) } @@ -404,6 +406,8 @@ abstract class BaseConfigTriggerViewModel( doNotRemapChecked = !key.consumeEvent, clickType = key.clickType, showClickTypes = showClickTypes, + isScanCodeDetectionSelected = key.detectWithScancode(), + isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable() ) } } @@ -613,6 +617,12 @@ abstract class BaseConfigTriggerViewModel( } } + fun onSelectScanCodeDetection(isSelected: Boolean) { + triggerKeyOptionsUid.value?.let { triggerKeyUid -> + config.setScanCodeDetectionEnabled(triggerKeyUid, isSelected) + } + } + fun onRecordTriggerButtonClick() { viewModelScope.launch { val recordTriggerState = recordTrigger.state.firstOrNull() ?: return@launch @@ -925,12 +935,15 @@ sealed class TriggerKeyOptionsState { abstract val showClickTypes: Boolean abstract val showLongPressClickType: Boolean - // TODO add isScanCodeSettingEnabled field and isScanCodeDetectionEnabled data class KeyEvent( val doNotRemapChecked: Boolean = false, override val clickType: ClickType, override val showClickTypes: Boolean, val devices: List, + // Whether scan code is checked. + val isScanCodeDetectionSelected: Boolean, + // Whether the setting should be enabled and allow user interaction. + val isScanCodeSettingEnabled: Boolean, ) : TriggerKeyOptionsState() { override val showLongPressClickType: Boolean = true } @@ -939,6 +952,10 @@ sealed class TriggerKeyOptionsState { val doNotRemapChecked: Boolean = false, override val clickType: ClickType, override val showClickTypes: Boolean, + // Whether scan code is checked. + val isScanCodeDetectionSelected: Boolean, + // Whether the setting should be enabled and allow user interaction. + val isScanCodeSettingEnabled: Boolean, ) : TriggerKeyOptionsState() { override val showLongPressClickType: Boolean = true } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 97b6f78c6a..3ba41482ac 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -97,6 +97,7 @@ fun BaseTriggerScreen(modifier: Modifier = Modifier, viewModel: BaseConfigTrigge onEditFloatingButtonClick = viewModel::onEditFloatingButtonClick, onEditFloatingLayoutClick = viewModel::onEditFloatingLayoutClick, onSelectFingerprintGestureType = viewModel::onSelectFingerprintGestureType, + onScanCodeDetectionChanged = viewModel::onSelectScanCodeDetection ) } @@ -693,7 +694,7 @@ private fun HorizontalEmptyPreview() { ), ), - ), + ), recordTriggerState = RecordTriggerState.Idle, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index 03f74853cb..d7594bbdde 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -563,4 +563,24 @@ class ConfigTriggerDelegate { fun setShowToastEnabled(trigger: Trigger, enabled: Boolean): Trigger { return trigger.copy(showToast = enabled) } + + fun setScanCodeDetectionEnabled(trigger: Trigger, keyUid: String, enabled: Boolean): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid && key is KeyCodeTriggerKey && key.isScanCodeDetectionUserConfigurable()) { + when (key) { + is KeyEventTriggerKey -> { + key.copy(detectWithScanCodeUserSetting = enabled) + } + + is EvdevTriggerKey -> { + key.copy(detectWithScanCodeUserSetting = enabled) + } + } + } else { + key + } + } + + return trigger.copy(keys = newKeys) + } } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt index 730c64292a..993f12eaa7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -252,6 +252,12 @@ class ConfigTriggerUseCaseImpl @Inject constructor( } } + override fun setScanCodeDetectionEnabled(keyUid: String, enabled: Boolean) { + updateTrigger { trigger -> + delegate.setScanCodeDetectionEnabled(trigger, keyUid, enabled) + } + } + override fun getAvailableTriggerKeyDevices(): List { val externalKeyEventTriggerDevices = sequence { val inputDevices = @@ -342,6 +348,7 @@ interface ConfigTriggerUseCase : GetDefaultKeyMapOptionsUseCase { fun setTriggerWhenScreenOff(enabled: Boolean) fun setTriggerFromOtherAppsEnabled(enabled: Boolean) fun setShowToastEnabled(enabled: Boolean) + fun setScanCodeDetectionEnabled(keyUid: String, enabled: Boolean) fun getAvailableTriggerKeyDevices(): List diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt index 95f08e3dd7..419dc675e5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt @@ -38,6 +38,10 @@ fun KeyCodeTriggerKey.isKeyCodeUnknown(): Boolean { return KeyEventUtils.isKeyCodeUnknown(keyCode) } +fun KeyCodeTriggerKey.isScanCodeDetectionUserConfigurable(): Boolean { + return scanCode != null && !isKeyCodeUnknown() +} + /** * Get the label for the key code or scan code, depending on whether to detect it with a scan code. */ diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 83488caea2..bb6628c30a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -20,8 +20,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue.Expanded +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -58,6 +60,7 @@ fun TriggerKeyOptionsBottomSheet( onSelectFingerprintGestureType: (FingerprintGestureType) -> Unit = {}, onEditFloatingButtonClick: () -> Unit = {}, onEditFloatingLayoutClick: () -> Unit = {}, + onScanCodeDetectionChanged: (Boolean) -> Unit = {}, ) { ModalBottomSheet( modifier = modifier, @@ -96,9 +99,15 @@ fun TriggerKeyOptionsBottomSheet( Spacer(modifier = Modifier.height(8.dp)) - // TODO use segmented button to switch between key code and scancode. if (state is TriggerKeyOptionsState.KeyEvent) { + ScanCodeDetectionButtonRow( + modifier = Modifier.fillMaxWidth(), + isEnabled = state.isScanCodeSettingEnabled, + isScanCodeSelected = state.isScanCodeDetectionSelected, + onSelectedChange = onScanCodeDetectionChanged + ) + CheckBoxText( modifier = Modifier.padding(8.dp), text = stringResource(R.string.flag_dont_override_default_action), @@ -108,6 +117,13 @@ fun TriggerKeyOptionsBottomSheet( } if (state is TriggerKeyOptionsState.EvdevEvent) { + ScanCodeDetectionButtonRow( + modifier = Modifier.fillMaxWidth(), + isEnabled = state.isScanCodeSettingEnabled, + isScanCodeSelected = state.isScanCodeDetectionSelected, + onSelectedChange = onScanCodeDetectionChanged + ) + CheckBoxText( modifier = Modifier.padding(8.dp), text = stringResource(R.string.flag_dont_override_default_action), @@ -294,10 +310,51 @@ fun TriggerKeyOptionsBottomSheet( } } +@Composable +private fun ScanCodeDetectionButtonRow( + modifier: Modifier = Modifier, + isEnabled: Boolean, + isScanCodeSelected: Boolean, + onSelectedChange: (Boolean) -> Unit +) { + Column(modifier) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.trigger_scan_code_detection_explanation), + style = MaterialTheme.typography.bodyMedium + ) + Spacer(Modifier.height(8.dp)) + + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + Spacer(Modifier.width(16.dp)) + SegmentedButton( + selected = !isScanCodeSelected, + onClick = { onSelectedChange(false) }, + shape = MaterialTheme.shapes.medium, + enabled = isEnabled + ) { + Text(stringResource(R.string.trigger_use_key_code_button)) + } + + Spacer(Modifier.width(16.dp)) + + SegmentedButton( + selected = isScanCodeSelected, + onClick = { onSelectedChange(true) }, + shape = MaterialTheme.shapes.medium, + enabled = isEnabled + ) { + Text(stringResource(R.string.trigger_use_scan_code_button)) + } + Spacer(Modifier.width(16.dp)) + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable -private fun Preview() { +private fun PreviewKeyEvent() { KeyMapperTheme { val sheetState = SheetState( skipPartiallyExpanded = true, @@ -323,6 +380,32 @@ private fun Preview() { isChecked = false, ), ), + isScanCodeDetectionSelected = true, + isScanCodeSettingEnabled = true + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewEvdev() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = Expanded, + ) + + TriggerKeyOptionsBottomSheet( + sheetState = sheetState, + state = TriggerKeyOptionsState.EvdevEvent( + doNotRemapChecked = true, + clickType = ClickType.DOUBLE_PRESS, + showClickTypes = true, + isScanCodeDetectionSelected = false, + isScanCodeSettingEnabled = false ), ) } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 637efce6e0..abbcfc8b69 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1518,6 +1518,9 @@ Key code %d Scan code %d Scan code + Keys can be identified by either a \'key code\' or a \'scan code\'. A scan code is more unique than a key code but your trigger may not work with other devices. We recommend using key codes. + Use key code + Use scan code Remove diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt index a3c23989bf..43fe608d48 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt @@ -26,6 +26,42 @@ class TriggerKeyTest { mockedKeyEvent.close() } + @Test + fun `User can not change scan code detection if the scan code is null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = null, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ) + assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(false)) + } + + @Test + fun `User can not change scan code detection if the key code is unknown and scan code is non null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ) + assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(false)) + } + + @Test + fun `User can change scan code detection if the key code is known and scan code is non null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ) + assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(true)) + } + @Test fun `detect with scan code if key code is unknown and user setting enabled`() { val triggerKey = KeyEventTriggerKey( From 0c38091243392a8f593269b09d49fc84f6c29ec0 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 01:03:09 +0100 Subject: [PATCH 100/215] #761 update the algorithm for scan code detection --- .../base/detection/KeyMapAlgorithm.kt | 41 ++- .../base/keymaps/KeyMapAlgorithmTest.kt | 335 +++++++++++++++++- 2 files changed, 352 insertions(+), 24 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index 5a2976ae66..fe9f937e22 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -24,6 +24,7 @@ import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey import io.github.sds100.keymapper.base.trigger.TriggerMode +import io.github.sds100.keymapper.base.trigger.detectWithScancode import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag @@ -1738,18 +1739,30 @@ class KeyMapAlgorithm( private fun TriggerKey.matchesEvent(event: AlgoEvent): Boolean { if (this is KeyEventTriggerKey && event is KeyEventAlgo) { + val codeMatches = if (this.detectWithScancode()) { + this.scanCode == event.scanCode + } else { + this.keyCode == event.keyCode + } + return when (this.device) { - KeyEventTriggerDevice.Any -> this.keyCode == event.keyCode && this.clickType == event.clickType + KeyEventTriggerDevice.Any -> codeMatches && this.clickType == event.clickType is KeyEventTriggerDevice.External -> - event.isExternal && this.keyCode == event.keyCode && event.descriptor == this.device.descriptor && this.clickType == event.clickType + event.isExternal && codeMatches && event.descriptor == this.device.descriptor && this.clickType == event.clickType KeyEventTriggerDevice.Internal -> !event.isExternal && - this.keyCode == event.keyCode && + codeMatches && this.clickType == event.clickType } } else if (this is EvdevTriggerKey && event is EvdevEventAlgo) { - return this.keyCode == event.keyCode && this.clickType == event.clickType && this.device == event.device + val codeMatches = if (this.detectWithScancode()) { + this.scanCode == event.scanCode + } else { + this.keyCode == event.keyCode + } + + return codeMatches && this.clickType == event.clickType && this.device == event.device } else if (this is AssistantTriggerKey && event is AssistantEvent) { return if (this.type == AssistantTriggerType.ANY || event.type == AssistantTriggerType.ANY) { this.clickType == event.clickType @@ -1767,23 +1780,35 @@ class KeyMapAlgorithm( private fun TriggerKey.matchesWithOtherKey(otherKey: TriggerKey): Boolean { if (this is KeyEventTriggerKey && otherKey is KeyEventTriggerKey) { + val codeMatches = if (this.detectWithScancode()) { + otherKey.detectWithScancode() && this.scanCode == otherKey.scanCode + } else { + this.keyCode == otherKey.keyCode + } + return when (this.device) { KeyEventTriggerDevice.Any -> - this.keyCode == otherKey.keyCode && + codeMatches && this.clickType == otherKey.clickType is KeyEventTriggerDevice.External -> - this.keyCode == otherKey.keyCode && + codeMatches && this.device == otherKey.device && this.clickType == otherKey.clickType KeyEventTriggerDevice.Internal -> - this.keyCode == otherKey.keyCode && + codeMatches && otherKey.device == KeyEventTriggerDevice.Internal && this.clickType == otherKey.clickType } } else if (this is EvdevTriggerKey && otherKey is EvdevTriggerKey) { - return this.keyCode == otherKey.keyCode && this.clickType == otherKey.clickType && this.device == otherKey.device + val codeMatches = if (this.detectWithScancode()) { + otherKey.detectWithScancode() && this.scanCode == otherKey.scanCode + } else { + this.keyCode == otherKey.keyCode + } + + return codeMatches && this.clickType == otherKey.clickType && this.device == otherKey.device } else if (this is AssistantTriggerKey && otherKey is AssistantTriggerKey) { return this.type == otherKey.type && this.clickType == otherKey.clickType } else if (this is FloatingButtonKey && otherKey is FloatingButtonKey) { diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index de7837091f..8b188a6b55 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -39,6 +39,7 @@ import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.Scancode import junitparams.JUnitParamsRunner import junitparams.Parameters import junitparams.naming.TestCaseName @@ -53,11 +54,14 @@ import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic import org.mockito.kotlin.any import org.mockito.kotlin.atLeast import org.mockito.kotlin.doAnswer @@ -147,6 +151,7 @@ class KeyMapAlgorithmTest { private lateinit var detectKeyMapsUseCase: DetectKeyMapsUseCase private lateinit var performActionsUseCase: PerformActionsUseCase private lateinit var detectConstraintsUseCase: DetectConstraintsUseCase + private lateinit var mockedKeyEvent: MockedStatic @get:Rule var instantExecutorRule = InstantTaskExecutorRule() @@ -213,6 +218,9 @@ class KeyMapAlgorithmTest { on { getSnapshot() } doReturn TestConstraintSnapshot() } + mockedKeyEvent = mockStatic(KeyEvent::class.java) + mockedKeyEvent.`when` { KeyEvent.getMaxKeyCode() }.thenReturn(1000) + controller = KeyMapAlgorithm( testScope, detectKeyMapsUseCase, @@ -221,29 +229,322 @@ class KeyMapAlgorithmTest { ) } + @After + fun tearDown() { + mockedKeyEvent.close() + } + + @Test + fun `Detect mouse button which only has scan code`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.BTN_LEFT, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent( + KeyEvent.KEYCODE_UNKNOWN, + Scancode.BTN_LEFT, + FAKE_CONTROLLER_EVDEV_DEVICE + ) + inputUpEvdevEvent(KeyEvent.KEYCODE_UNKNOWN, Scancode.BTN_LEFT, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Detect evdev trigger with scan code if key has unknown key code`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Detect evdev trigger with scan code if user setting enabled`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Detect evdev trigger with scan code when scan code matches but key code differs`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input with different key code but matching scan code + inputDownEvdevEvent(KeyEvent.KEYCODE_C, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_C, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Do not detect evdev trigger when scan code differs`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input with matching key code but different scan code + inputDownEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_C, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_C, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + } + + @Test + fun `Sequence trigger with multiple evdev keys and scan code detection is triggered`() = + runTest(testDispatcher) { + val trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, // Different scan code + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_C, + scanCode = Scancode.KEY_D, // Different scan code + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input with scan codes that match the trigger + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + inputDownEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Parallel trigger with multiple evdev keys and scan code detection is triggered`() = + runTest(testDispatcher) { + val trigger = parallelTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, // Different scan code + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_C, + scanCode = Scancode.KEY_D, // Different scan code + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input both keys simultaneously with scan codes that match the trigger + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE) + + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + + @Test + fun `Scan code detection works with long press evdev trigger`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + clickType = ClickType.LONG_PRESS, + detectWithScanCodeUserSetting = true + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + // Wait for long press duration + delay(LONG_PRESS_DELAY + 100L) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Scan code detection works with double press evdev trigger`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + clickType = ClickType.DOUBLE_PRESS, + detectWithScanCodeUserSetting = true + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // First press + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + delay(50L) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + delay(50L) + + // Second press + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + delay(50L) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Scan code detection fails when device differs for evdev trigger`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input from different device + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_VOLUME_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_VOLUME_EVDEV_DEVICE) + + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + } + + @Test + fun `Detect key event trigger with scan code if key has unknown key code`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false // It will be automatically enabled even if the user hasn't explicitly turned it on + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_B, + action = KeyEvent.ACTION_DOWN, + device = FAKE_CONTROLLER_INPUT_DEVICE, + scanCode = Scancode.KEY_B + ) + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_B, + action = KeyEvent.ACTION_UP, + device = FAKE_CONTROLLER_INPUT_DEVICE, + scanCode = Scancode.KEY_B + ) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Detect key event trigger with scan code if user setting enabled`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_B, + action = KeyEvent.ACTION_DOWN, + device = FAKE_CONTROLLER_INPUT_DEVICE, + scanCode = Scancode.KEY_B + ) + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_B, + action = KeyEvent.ACTION_UP, + device = FAKE_CONTROLLER_INPUT_DEVICE, + scanCode = Scancode.KEY_B + ) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + @Test fun `Sequence trigger with multiple evdev keys is triggered`() = runTest(testDispatcher) { val trigger = sequenceTrigger( EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, - scanCode = 900, + scanCode = Scancode.KEY_A, device = FAKE_CONTROLLER_EVDEV_DEVICE, ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_B, - scanCode = 901, + scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) - inputDownEvdevEvent(KeyEvent.KEYCODE_A, 900, FAKE_CONTROLLER_EVDEV_DEVICE) - inputUpEvdevEvent(KeyEvent.KEYCODE_A, 900, FAKE_CONTROLLER_EVDEV_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE) - inputDownEvdevEvent(KeyEvent.KEYCODE_B, 901, FAKE_CONTROLLER_EVDEV_DEVICE) - inputUpEvdevEvent(KeyEvent.KEYCODE_B, 901, FAKE_CONTROLLER_EVDEV_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } @@ -254,23 +555,23 @@ class KeyMapAlgorithmTest { val trigger = parallelTrigger( EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, - scanCode = 900, + scanCode = Scancode.KEY_A, device = FAKE_CONTROLLER_EVDEV_DEVICE, ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_B, - scanCode = 901, + scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) - inputDownEvdevEvent(KeyEvent.KEYCODE_A, 900, FAKE_CONTROLLER_EVDEV_DEVICE) - inputDownEvdevEvent(KeyEvent.KEYCODE_B, 901, FAKE_CONTROLLER_EVDEV_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) - inputUpEvdevEvent(KeyEvent.KEYCODE_A, 900, FAKE_CONTROLLER_EVDEV_DEVICE) - inputUpEvdevEvent(KeyEvent.KEYCODE_B, 901, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } @@ -281,7 +582,7 @@ class KeyMapAlgorithmTest { val trigger = singleKeyTrigger( EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_POWER, - scanCode = 900, + scanCode = Scancode.KEY_POWER, device = FAKE_VOLUME_EVDEV_DEVICE, ), ) @@ -298,7 +599,7 @@ class KeyMapAlgorithmTest { val trigger = singleKeyTrigger( EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, - scanCode = 900, + scanCode = Scancode.KEY_POWER, device = FAKE_CONTROLLER_EVDEV_DEVICE, ), ) @@ -315,7 +616,7 @@ class KeyMapAlgorithmTest { val trigger = singleKeyTrigger( EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_POWER, - scanCode = 900, + scanCode = Scancode.KEY_POWER, device = FAKE_VOLUME_EVDEV_DEVICE, ), ) @@ -4402,7 +4703,8 @@ class KeyMapAlgorithmTest { metaState: Int? = null, scanCode: Int = 0, repeatCount: Int = 0, - ): Boolean = controller.onInputEvent( + + ): Boolean = controller.onInputEvent( KMKeyEvent( keyCode = keyCode, action = action, @@ -4529,4 +4831,5 @@ class KeyMapAlgorithmTest { sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD, ) } + } From eb251806d4b9976855370b1f3f85bfda1d867bc4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 01:26:20 +0100 Subject: [PATCH 101/215] fix: use disabled text for Advanced Triggers button when recording --- .../sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt index 09aabd1396..c2de7802c6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt @@ -8,6 +8,7 @@ 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.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface @@ -131,7 +132,7 @@ private fun AdvancedTriggersButton( enabled = isEnabled, onClick = onClick, ) { - val color = ButtonDefaults.textButtonColors().contentColor + val color = LocalContentColor.current BasicText( text = stringResource(R.string.button_advanced_triggers), maxLines = 1, From 158a2d2ca256e426c5b36f05c7cdce578f42f17d Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 13:00:41 +0100 Subject: [PATCH 102/215] #1394 WIP: add pro mode recording toggle switch --- .../base/trigger/BaseTriggerScreen.kt | 2 +- .../base/trigger/RecordTriggerButtonRow.kt | 85 +++++++++++-------- .../base/trigger/RecordTriggerController.kt | 54 ++++++++++-- base/src/main/res/values/strings.xml | 3 +- 4 files changed, 102 insertions(+), 42 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 3ba41482ac..e37c074f2c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -255,7 +255,7 @@ private fun TriggerScreenVertical( if (configState.triggerModeButtonsVisible) { if (!isCompact) { Text( - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier.padding(horizontal = 16.dp), text = stringResource(R.string.press_dot_dot_dot), style = MaterialTheme.typography.labelLarge, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt index c2de7802c6..a383a03beb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.trigger +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -12,6 +13,8 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -40,45 +43,57 @@ fun RecordTriggerButtonRow( 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, + Column { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.trigger_record_with_pro_mode), + overflow = TextOverflow.Ellipsis ) + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = false, + onCheckedChange = {}) } + 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)) + 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, - ) + 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, + ) + } } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 8a9962d97e..f33b341fe1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -1,10 +1,10 @@ package io.github.sds100.keymapper.base.trigger import android.view.KeyEvent +import io.github.sds100.keymapper.base.detection.DpadMotionEventTracker import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.input.InputEventHubCallback -import io.github.sds100.keymapper.base.detection.DpadMotionEventTracker import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.isError @@ -67,6 +67,30 @@ class RecordTriggerControllerImpl @Inject constructor( private val downEvdevEvents: MutableSet = mutableSetOf() private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() + // TODO update when system bridge is (dis)connected + override val isEvdevRecordingPermitted: MutableStateFlow = + MutableStateFlow(getIsEvdevRecordingPermitted()) + + // TODO set to false by default and turn into a flow + private var isEvdevRecordingEnabled: Boolean = true + + override fun setEvdevRecordingEnabled(enabled: Boolean) { + if (!isEvdevRecordingPermitted.value) { + return + } + + if (state.value is RecordTriggerState.CountingDown) { + return + } + + isEvdevRecordingEnabled = enabled + } + + private fun getIsEvdevRecordingPermitted(): Boolean { + // TODO check if the preference enabling pro mode key maps is turned on + return inputEventHub.isSystemBridgeConnected() + } + override fun onInputEvent( event: KMInputEvent, detectionSource: InputEventDetectionSource, @@ -77,6 +101,11 @@ class RecordTriggerControllerImpl @Inject constructor( when (event) { is KMEvdevEvent -> { + // TODO check +// if (!isEvdevRecordingEnabled || !isEvdevRecordingPermitted.value) { +// return false +// } + // Do not record evdev events that are not key events. if (event.type != KMEvdevEvent.TYPE_KEY_EVENT) { return false @@ -107,6 +136,10 @@ class RecordTriggerControllerImpl @Inject constructor( } is KMGamePadEvent -> { + if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { + return false + } + val dpadKeyEvents = dpadMotionEventTracker.convertMotionEvent(event) for (keyEvent in dpadKeyEvents) { @@ -124,6 +157,10 @@ class RecordTriggerControllerImpl @Inject constructor( } is KMKeyEvent -> { + if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { + return false + } + val matchingDownEvent: KMKeyEvent? = downKeyEvents.find { it.keyCode == event.keyCode && it.scanCode == event.scanCode && @@ -190,6 +227,10 @@ class RecordTriggerControllerImpl @Inject constructor( return false } + if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { + return false + } + val keyEvent = dpadMotionEventTracker.convertMotionEvent(event).firstOrNull() ?: return false @@ -203,10 +244,7 @@ class RecordTriggerControllerImpl @Inject constructor( InputEventDetectionSource.INPUT_METHOD, ) - recordedKeys.add(recordedKey) - coroutineScope.launch { - onRecordKey.emit(recordedKey) - } + onRecordKey(recordedKey) } return true @@ -246,6 +284,8 @@ class RecordTriggerControllerImpl @Inject constructor( dpadMotionEventTracker.reset() downKeyEvents.clear() + //TODO +// if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { inputEventHub.registerClient( INPUT_EVENT_HUB_ID, this@RecordTriggerControllerImpl, @@ -254,6 +294,7 @@ class RecordTriggerControllerImpl @Inject constructor( // Grab all evdev devices inputEventHub.grabAllEvdevDevices(INPUT_EVENT_HUB_ID) +// } repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration @@ -274,6 +315,9 @@ interface RecordTriggerController { val state: StateFlow val onRecordKey: Flow + val isEvdevRecordingPermitted: Flow + fun setEvdevRecordingEnabled(enabled: Boolean) + /** * @return Success if started and an Error if failed to start. */ diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index abbcfc8b69..c960cf5c0b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -398,7 +398,7 @@ NEW! Done Fix - Recording (%d…) + Recording now (%d…) Add constraint Choose Key code Add extra @@ -1521,6 +1521,7 @@ Keys can be identified by either a \'key code\' or a \'scan code\'. A scan code is more unique than a key code but your trigger may not work with other devices. We recommend using key codes. Use key code Use scan code + Use PRO mode Remove From d660d0dc9f9fa32d2d2eefe5f2fc88633b487b94 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 14:33:39 +0100 Subject: [PATCH 103/215] #761 show the key code and scan code number in the buttons to switch between them --- .../trigger/BaseConfigTriggerViewModel.kt | 8 +++++ .../trigger/TriggerKeyOptionsBottomSheet.kt | 30 +++++++++++++++++-- base/src/main/res/values/strings.xml | 6 ++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 25390646a3..d64141bab1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -374,6 +374,8 @@ abstract class BaseConfigTriggerViewModel( clickType = key.clickType, showClickTypes = showClickTypes, devices = deviceListItems, + keyCode = key.keyCode, + scanCode = key.scanCode, isScanCodeDetectionSelected = key.detectWithScancode(), isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable() ) @@ -406,6 +408,8 @@ abstract class BaseConfigTriggerViewModel( doNotRemapChecked = !key.consumeEvent, clickType = key.clickType, showClickTypes = showClickTypes, + keyCode = key.keyCode, + scanCode = key.scanCode, isScanCodeDetectionSelected = key.detectWithScancode(), isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable() ) @@ -940,6 +944,8 @@ sealed class TriggerKeyOptionsState { override val clickType: ClickType, override val showClickTypes: Boolean, val devices: List, + val keyCode: Int, + val scanCode: Int?, // Whether scan code is checked. val isScanCodeDetectionSelected: Boolean, // Whether the setting should be enabled and allow user interaction. @@ -952,6 +958,8 @@ sealed class TriggerKeyOptionsState { val doNotRemapChecked: Boolean = false, override val clickType: ClickType, override val showClickTypes: Boolean, + val keyCode: Int, + val scanCode: Int, // Whether scan code is checked. val isScanCodeDetectionSelected: Boolean, // Whether the setting should be enabled and allow user interaction. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index bb6628c30a..b91f88ac8d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.trigger +import android.view.KeyEvent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow @@ -44,6 +45,7 @@ import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe +import io.github.sds100.keymapper.system.inputevents.Scancode import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -105,6 +107,8 @@ fun TriggerKeyOptionsBottomSheet( modifier = Modifier.fillMaxWidth(), isEnabled = state.isScanCodeSettingEnabled, isScanCodeSelected = state.isScanCodeDetectionSelected, + keyCode = state.keyCode, + scanCode = state.scanCode, onSelectedChange = onScanCodeDetectionChanged ) @@ -121,6 +125,8 @@ fun TriggerKeyOptionsBottomSheet( modifier = Modifier.fillMaxWidth(), isEnabled = state.isScanCodeSettingEnabled, isScanCodeSelected = state.isScanCodeDetectionSelected, + keyCode = state.keyCode, + scanCode = state.scanCode, onSelectedChange = onScanCodeDetectionChanged ) @@ -313,6 +319,8 @@ fun TriggerKeyOptionsBottomSheet( @Composable private fun ScanCodeDetectionButtonRow( modifier: Modifier = Modifier, + keyCode: Int, + scanCode: Int?, isEnabled: Boolean, isScanCodeSelected: Boolean, onSelectedChange: (Boolean) -> Unit @@ -333,7 +341,13 @@ private fun ScanCodeDetectionButtonRow( shape = MaterialTheme.shapes.medium, enabled = isEnabled ) { - Text(stringResource(R.string.trigger_use_key_code_button)) + val text = if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { + stringResource(R.string.trigger_use_key_code_button_disabled) + } else { + stringResource(R.string.trigger_use_key_code_button_enabled, keyCode) + + } + Text(text) } Spacer(Modifier.width(16.dp)) @@ -344,7 +358,13 @@ private fun ScanCodeDetectionButtonRow( shape = MaterialTheme.shapes.medium, enabled = isEnabled ) { - Text(stringResource(R.string.trigger_use_scan_code_button)) + val text = if (scanCode == null) { + stringResource(R.string.trigger_use_scan_code_button_disabled) + } else { + stringResource(R.string.trigger_use_scan_code_button_enabled, scanCode) + + } + Text(text) } Spacer(Modifier.width(16.dp)) } @@ -380,6 +400,8 @@ private fun PreviewKeyEvent() { isChecked = false, ), ), + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, isScanCodeDetectionSelected = true, isScanCodeSettingEnabled = true ), @@ -404,7 +426,9 @@ private fun PreviewEvdev() { doNotRemapChecked = true, clickType = ClickType.DOUBLE_PRESS, showClickTypes = true, - isScanCodeDetectionSelected = false, + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + isScanCodeDetectionSelected = true, isScanCodeSettingEnabled = false ), ) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index c960cf5c0b..552cabcb44 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1519,8 +1519,10 @@ Scan code %d Scan code Keys can be identified by either a \'key code\' or a \'scan code\'. A scan code is more unique than a key code but your trigger may not work with other devices. We recommend using key codes. - Use key code - Use scan code + Unknown key code + Use key code %d + Use scan code %d + No scan code saved Use PRO mode From d1289424fee9b42393ea164854d03e5a8f67b9d6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 14:57:13 +0100 Subject: [PATCH 104/215] feat: use segmented buttons instead of radio buttons for click type --- .../base/trigger/BaseTriggerScreen.kt | 95 ++++++++++++------- .../trigger/TriggerKeyOptionsBottomSheet.kt | 95 +++++++++++-------- 2 files changed, 117 insertions(+), 73 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index e37c074f2c..0560e9a13c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -16,12 +16,18 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Fingerprint import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -32,9 +38,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +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.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowWidthSizeClass @@ -244,11 +252,13 @@ private fun TriggerScreenVertical( if (configState.clickTypeButtons.isNotEmpty()) { ClickTypeRadioGroup( - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), clickTypes = configState.clickTypeButtons, checkedClickType = configState.checkedClickType, onSelectClickType = onSelectClickType, - maxLines = if (isCompact) 1 else 2, + isCompact = isCompact ) } @@ -389,10 +399,13 @@ private fun TriggerScreenHorizontal( ) { if (configState.clickTypeButtons.isNotEmpty()) { ClickTypeRadioGroup( - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), clickTypes = configState.clickTypeButtons, checkedClickType = configState.checkedClickType, onSelectClickType = onSelectClickType, + isCompact = false ) } @@ -514,36 +527,54 @@ private fun ClickTypeRadioGroup( clickTypes: Set, checkedClickType: ClickType?, onSelectClickType: (ClickType) -> Unit, - maxLines: Int = 2, + isCompact: Boolean, ) { - Column(modifier = modifier) { - 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)) { - RadioButtonText( - modifier = Modifier.weight(1f), - 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)) { - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = checkedClickType == ClickType.DOUBLE_PRESS, - text = stringResource(R.string.radio_button_double_press), - onSelected = { onSelectClickType(ClickType.DOUBLE_PRESS) }, - maxLines = maxLines, - ) + val clickTypeButtonContent: List> = clickTypes.map { clickType -> + when (clickType) { + ClickType.SHORT_PRESS -> clickType to stringResource(R.string.radio_button_short_press) + ClickType.LONG_PRESS -> clickType to stringResource(R.string.radio_button_long_press) + ClickType.DOUBLE_PRESS -> clickType to stringResource(R.string.radio_button_double_press) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + SingleChoiceSegmentedButtonRow( + modifier = modifier, + ) { + for (content in clickTypeButtonContent) { + val (clickType, label) = content + + if (isCompact) { + SegmentedButton( + selected = clickType == checkedClickType, + onClick = { onSelectClickType(clickType) }, + icon = { }, + shape = SegmentedButtonDefaults.itemShape( + index = clickTypeButtonContent.indexOf(content), + count = clickTypeButtonContent.size, + baseShape = MaterialTheme.shapes.extraSmall + ), + ) { + BasicText( + text = label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = LocalTextStyle.current, + autoSize = TextAutoSize.StepBased(minFontSize = 10.sp) + ) + } + } else { + SegmentedButton( + selected = clickType == checkedClickType, + onClick = { onSelectClickType(clickType) }, + shape = SegmentedButtonDefaults.itemShape( + index = clickTypeButtonContent.indexOf(content), + count = clickTypeButtonContent.size, + ), + ) { + Text(text = label, maxLines = 1, overflow = TextOverflow.Ellipsis) + } } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index b91f88ac8d..629ff8f106 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base.trigger import android.view.KeyEvent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -22,6 +21,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue.Expanded import androidx.compose.material3.SingleChoiceSegmentedButtonRow @@ -139,44 +139,11 @@ fun TriggerKeyOptionsBottomSheet( } if (state.showClickTypes) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.trigger_key_click_types_header), - style = MaterialTheme.typography.titleSmall, - ) - - FlowRow( + ClickTypeSection( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp), - ) { - Spacer(Modifier.width(8.dp)) - - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = state.clickType == ClickType.SHORT_PRESS, - text = stringResource(R.string.radio_button_short_press), - onSelected = { onSelectClickType(ClickType.SHORT_PRESS) }, - ) - - if (state.showLongPressClickType) { - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = state.clickType == ClickType.LONG_PRESS, - text = stringResource(R.string.radio_button_long_press), - onSelected = { onSelectClickType(ClickType.LONG_PRESS) }, - ) - } - - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = state.clickType == ClickType.DOUBLE_PRESS, - text = stringResource(R.string.radio_button_double_press), - onSelected = { onSelectClickType(ClickType.DOUBLE_PRESS) }, - ) - - Spacer(Modifier.width(8.dp)) - } + .padding(horizontal = 16.dp), state, onSelectClickType + ) } if (state is TriggerKeyOptionsState.KeyEvent) { @@ -316,6 +283,48 @@ fun TriggerKeyOptionsBottomSheet( } } +@Composable +private fun ClickTypeSection( + modifier: Modifier, + state: TriggerKeyOptionsState, + onSelectClickType: (ClickType) -> Unit +) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.trigger_key_click_types_header), + style = MaterialTheme.typography.titleSmall, + ) + + val clickTypeButtonContent: List> = buildList { + add(ClickType.SHORT_PRESS to stringResource(R.string.radio_button_short_press)) + if (state.showLongPressClickType) { + add(ClickType.LONG_PRESS to stringResource(R.string.radio_button_long_press)) + } + add(ClickType.DOUBLE_PRESS to stringResource(R.string.radio_button_double_press)) + } + + Spacer(modifier = Modifier.height(8.dp)) + + SingleChoiceSegmentedButtonRow( + modifier = modifier, + ) { + for (content in clickTypeButtonContent) { + val (clickType, label) = content + SegmentedButton( + selected = state.clickType == clickType, + onClick = { onSelectClickType(clickType) }, + icon = {}, + shape = SegmentedButtonDefaults.itemShape( + index = clickTypeButtonContent.indexOf(content), + count = clickTypeButtonContent.size + ), + ) { + Text(label) + } + } + } +} + @Composable private fun ScanCodeDetectionButtonRow( modifier: Modifier = Modifier, @@ -338,7 +347,10 @@ private fun ScanCodeDetectionButtonRow( SegmentedButton( selected = !isScanCodeSelected, onClick = { onSelectedChange(false) }, - shape = MaterialTheme.shapes.medium, + shape = SegmentedButtonDefaults.itemShape( + index = 0, + count = 2, + ), enabled = isEnabled ) { val text = if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { @@ -350,12 +362,13 @@ private fun ScanCodeDetectionButtonRow( Text(text) } - Spacer(Modifier.width(16.dp)) - SegmentedButton( selected = isScanCodeSelected, onClick = { onSelectedChange(true) }, - shape = MaterialTheme.shapes.medium, + shape = SegmentedButtonDefaults.itemShape( + index = 1, + count = 2, + ), enabled = isEnabled ) { val text = if (scanCode == null) { From 9dd49a5cc405aea547a137864fdaf66576c49468 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 15:06:59 +0100 Subject: [PATCH 105/215] feat: use segmented buttons for trigger mode --- .../base/trigger/BaseTriggerScreen.kt | 111 ++++++++++++------ .../trigger/TriggerKeyOptionsBottomSheet.kt | 3 +- base/src/main/res/values/strings.xml | 5 +- 3 files changed, 76 insertions(+), 43 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 0560e9a13c..2d815eed95 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -54,7 +54,6 @@ import io.github.sds100.keymapper.base.keymaps.ShortcutRow import io.github.sds100.keymapper.base.utils.ui.LinkType import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.compose.DraggableItem -import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.rememberDragDropState import io.github.sds100.keymapper.common.utils.State @@ -263,21 +262,15 @@ private fun TriggerScreenVertical( } if (configState.triggerModeButtonsVisible) { - if (!isCompact) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.press_dot_dot_dot), - style = MaterialTheme.typography.labelLarge, - ) - } - TriggerModeRadioGroup( - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), mode = configState.checkedTriggerMode, isEnabled = configState.triggerModeButtonsEnabled, onSelectParallelMode = onSelectParallelMode, onSelectSequenceMode = onSelectSequenceMode, - maxLines = if (isCompact) 1 else 2, + isCompact = isCompact, ) } } @@ -409,19 +402,16 @@ 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), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), mode = configState.checkedTriggerMode, isEnabled = configState.triggerModeButtonsEnabled, onSelectParallelMode = onSelectParallelMode, onSelectSequenceMode = onSelectSequenceMode, + isCompact = false, ) } } @@ -561,7 +551,10 @@ private fun ClickTypeRadioGroup( maxLines = 1, overflow = TextOverflow.Ellipsis, style = LocalTextStyle.current, - autoSize = TextAutoSize.StepBased(minFontSize = 10.sp) + autoSize = TextAutoSize.StepBased( + maxFontSize = LocalTextStyle.current.fontSize, + minFontSize = 10.sp + ) ) } } else { @@ -587,26 +580,68 @@ private fun TriggerModeRadioGroup( isEnabled: Boolean, onSelectParallelMode: () -> Unit, onSelectSequenceMode: () -> Unit, - maxLines: Int = 2, + isCompact: Boolean ) { - Column(modifier = modifier) { - 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), - isSelected = mode == TriggerMode.Sequence, - isEnabled = isEnabled, - text = stringResource(R.string.radio_button_sequence), - onSelected = onSelectSequenceMode, - maxLines = maxLines, - ) + val triggerModeButtonContent = listOf( + TriggerMode.Parallel to stringResource(R.string.radio_button_parallel), + TriggerMode.Sequence to stringResource(R.string.radio_button_sequence) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SingleChoiceSegmentedButtonRow( + modifier = modifier, + ) { + for (content in triggerModeButtonContent) { + val (triggerMode, label) = content + val isSelected = mode == triggerMode + + if (isCompact) { + SegmentedButton( + selected = isSelected, + onClick = { + when (triggerMode) { + is TriggerMode.Parallel -> onSelectParallelMode() + is TriggerMode.Sequence -> onSelectSequenceMode() + } + }, + enabled = isEnabled, + icon = { }, + shape = SegmentedButtonDefaults.itemShape( + index = triggerModeButtonContent.indexOf(content), + count = triggerModeButtonContent.size, + baseShape = MaterialTheme.shapes.extraSmall + ), + ) { + BasicText( + text = label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = LocalTextStyle.current, + autoSize = TextAutoSize.StepBased( + maxFontSize = LocalTextStyle.current.fontSize, + minFontSize = 10.sp + ) + ) + } + } else { + SegmentedButton( + selected = isSelected, + onClick = { + when (triggerMode) { + is TriggerMode.Parallel -> onSelectParallelMode() + is TriggerMode.Sequence -> onSelectSequenceMode() + } + }, + enabled = isEnabled, + shape = SegmentedButtonDefaults.itemShape( + index = triggerModeButtonContent.indexOf(content), + count = triggerModeButtonContent.size, + ), + ) { + Text(text = label, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 629ff8f106..36ae7dc1e6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -313,7 +313,6 @@ private fun ClickTypeSection( SegmentedButton( selected = state.clickType == clickType, onClick = { onSelectClickType(clickType) }, - icon = {}, shape = SegmentedButtonDefaults.itemShape( index = clickTypeButtonContent.indexOf(content), count = clickTypeButtonContent.size @@ -351,7 +350,7 @@ private fun ScanCodeDetectionButtonRow( index = 0, count = 2, ), - enabled = isEnabled + enabled = false ) { val text = if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { stringResource(R.string.trigger_use_key_code_button_disabled) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 552cabcb44..aee6c509d1 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -10,7 +10,6 @@ ¯\\_(ツ)_/¯\n\nNothing here! The first step is to add some buttons that will trigger the key map.\n\nFirst tap ‘Record trigger’ and then press the buttons that you want to remap. They will appear here.\n\nAlternatively, you can trigger a key map using an ‘advanced trigger’.\n\nYou can mix and match any keys! Requires root - Press… No actions No trigger @@ -156,8 +155,8 @@ - At the same time - In sequence + Press together + Press in sequence AND OR Short press From 598d8e2344e34222847d6dc837ca8c1c67bc4a26 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 15:30:57 +0100 Subject: [PATCH 106/215] feat: extract segmented button rows styling into KeyMapperSegmentedButtonRow --- .../base/trigger/BaseTriggerScreen.kt | 141 ++++------------ .../trigger/TriggerKeyOptionsBottomSheet.kt | 150 ++++++++++-------- .../ui/compose/KeyMapperSegmentedButtonRow.kt | 106 +++++++++++++ 3 files changed, 226 insertions(+), 171 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 2d815eed95..fab500fe36 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -16,18 +16,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.text.TextAutoSize import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Fingerprint import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -38,11 +32,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -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.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowWidthSizeClass @@ -54,6 +46,7 @@ import io.github.sds100.keymapper.base.keymaps.ShortcutRow import io.github.sds100.keymapper.base.utils.ui.LinkType import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.compose.DraggableItem +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.rememberDragDropState import io.github.sds100.keymapper.common.utils.State @@ -259,6 +252,10 @@ private fun TriggerScreenVertical( onSelectClickType = onSelectClickType, isCompact = isCompact ) + + if (!isCompact) { + Spacer(Modifier.height(8.dp)) + } } if (configState.triggerModeButtonsVisible) { @@ -390,6 +387,7 @@ private fun TriggerScreenHorizontal( .weight(1f) .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(16.dp)) if (configState.clickTypeButtons.isNotEmpty()) { ClickTypeRadioGroup( modifier = Modifier @@ -402,6 +400,8 @@ private fun TriggerScreenHorizontal( ) } + Spacer(modifier = Modifier.height(8.dp)) + if (configState.triggerModeButtonsVisible) { TriggerModeRadioGroup( modifier = Modifier @@ -414,6 +414,8 @@ private fun TriggerScreenHorizontal( isCompact = false, ) } + + Spacer(modifier = Modifier.height(16.dp)) } RecordTriggerButtonRow( @@ -527,50 +529,13 @@ private fun ClickTypeRadioGroup( } } - Spacer(modifier = Modifier.height(8.dp)) - - SingleChoiceSegmentedButtonRow( + KeyMapperSegmentedButtonRow( modifier = modifier, - ) { - for (content in clickTypeButtonContent) { - val (clickType, label) = content - - if (isCompact) { - SegmentedButton( - selected = clickType == checkedClickType, - onClick = { onSelectClickType(clickType) }, - icon = { }, - shape = SegmentedButtonDefaults.itemShape( - index = clickTypeButtonContent.indexOf(content), - count = clickTypeButtonContent.size, - baseShape = MaterialTheme.shapes.extraSmall - ), - ) { - BasicText( - text = label, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = LocalTextStyle.current, - autoSize = TextAutoSize.StepBased( - maxFontSize = LocalTextStyle.current.fontSize, - minFontSize = 10.sp - ) - ) - } - } else { - SegmentedButton( - selected = clickType == checkedClickType, - onClick = { onSelectClickType(clickType) }, - shape = SegmentedButtonDefaults.itemShape( - index = clickTypeButtonContent.indexOf(content), - count = clickTypeButtonContent.size, - ), - ) { - Text(text = label, maxLines = 1, overflow = TextOverflow.Ellipsis) - } - } - } - } + buttonStates = clickTypeButtonContent, + selectedState = checkedClickType, + onStateSelected = onSelectClickType, + isCompact = isCompact + ) } @Composable @@ -583,67 +548,27 @@ private fun TriggerModeRadioGroup( isCompact: Boolean ) { val triggerModeButtonContent = listOf( - TriggerMode.Parallel to stringResource(R.string.radio_button_parallel), - TriggerMode.Sequence to stringResource(R.string.radio_button_sequence) + "parallel" to stringResource(R.string.radio_button_parallel), + "sequence" to stringResource(R.string.radio_button_sequence) ) - Spacer(modifier = Modifier.height(8.dp)) - - SingleChoiceSegmentedButtonRow( + KeyMapperSegmentedButtonRow( modifier = modifier, - ) { - for (content in triggerModeButtonContent) { - val (triggerMode, label) = content - val isSelected = mode == triggerMode - - if (isCompact) { - SegmentedButton( - selected = isSelected, - onClick = { - when (triggerMode) { - is TriggerMode.Parallel -> onSelectParallelMode() - is TriggerMode.Sequence -> onSelectSequenceMode() - } - }, - enabled = isEnabled, - icon = { }, - shape = SegmentedButtonDefaults.itemShape( - index = triggerModeButtonContent.indexOf(content), - count = triggerModeButtonContent.size, - baseShape = MaterialTheme.shapes.extraSmall - ), - ) { - BasicText( - text = label, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = LocalTextStyle.current, - autoSize = TextAutoSize.StepBased( - maxFontSize = LocalTextStyle.current.fontSize, - minFontSize = 10.sp - ) - ) - } - } else { - SegmentedButton( - selected = isSelected, - onClick = { - when (triggerMode) { - is TriggerMode.Parallel -> onSelectParallelMode() - is TriggerMode.Sequence -> onSelectSequenceMode() - } - }, - enabled = isEnabled, - shape = SegmentedButtonDefaults.itemShape( - index = triggerModeButtonContent.indexOf(content), - count = triggerModeButtonContent.size, - ), - ) { - Text(text = label, maxLines = 2, overflow = TextOverflow.Ellipsis) - } + buttonStates = triggerModeButtonContent, + selectedState = when (mode) { + is TriggerMode.Parallel -> "parallel" + TriggerMode.Sequence -> "sequence" + TriggerMode.Undefined -> null + }, + onStateSelected = { selectedMode -> + when (selectedMode) { + "parallel" -> onSelectParallelMode() + "sequence" -> onSelectSequenceMode() } - } - } + }, + isCompact = isCompact, + isEnabled = isEnabled + ) } private val sampleList = listOf( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 36ae7dc1e6..21db4fdaef 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -20,12 +20,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue.Expanded -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -37,12 +35,15 @@ 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.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe import io.github.sds100.keymapper.system.inputevents.Scancode @@ -101,6 +102,7 @@ fun TriggerKeyOptionsBottomSheet( Spacer(modifier = Modifier.height(8.dp)) + val isCompact = isVerticalCompactLayout() if (state is TriggerKeyOptionsState.KeyEvent) { ScanCodeDetectionButtonRow( @@ -109,7 +111,8 @@ fun TriggerKeyOptionsBottomSheet( isScanCodeSelected = state.isScanCodeDetectionSelected, keyCode = state.keyCode, scanCode = state.scanCode, - onSelectedChange = onScanCodeDetectionChanged + onSelectedChange = onScanCodeDetectionChanged, + isCompact = isCompact ) CheckBoxText( @@ -127,7 +130,8 @@ fun TriggerKeyOptionsBottomSheet( isScanCodeSelected = state.isScanCodeDetectionSelected, keyCode = state.keyCode, scanCode = state.scanCode, - onSelectedChange = onScanCodeDetectionChanged + onSelectedChange = onScanCodeDetectionChanged, + isCompact = isCompact ) CheckBoxText( @@ -142,7 +146,8 @@ fun TriggerKeyOptionsBottomSheet( ClickTypeSection( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), state, onSelectClickType + .padding(horizontal = 16.dp), state, onSelectClickType, + isCompact = isCompact ) } @@ -287,7 +292,8 @@ fun TriggerKeyOptionsBottomSheet( private fun ClickTypeSection( modifier: Modifier, state: TriggerKeyOptionsState, - onSelectClickType: (ClickType) -> Unit + onSelectClickType: (ClickType) -> Unit, + isCompact: Boolean ) { Text( modifier = Modifier.padding(horizontal = 16.dp), @@ -303,25 +309,13 @@ private fun ClickTypeSection( add(ClickType.DOUBLE_PRESS to stringResource(R.string.radio_button_double_press)) } - Spacer(modifier = Modifier.height(8.dp)) - - SingleChoiceSegmentedButtonRow( + KeyMapperSegmentedButtonRow( modifier = modifier, - ) { - for (content in clickTypeButtonContent) { - val (clickType, label) = content - SegmentedButton( - selected = state.clickType == clickType, - onClick = { onSelectClickType(clickType) }, - shape = SegmentedButtonDefaults.itemShape( - index = clickTypeButtonContent.indexOf(content), - count = clickTypeButtonContent.size - ), - ) { - Text(label) - } - } - } + buttonStates = clickTypeButtonContent, + selectedState = state.clickType, + onStateSelected = onSelectClickType, + isCompact = isCompact + ) } @Composable @@ -331,7 +325,8 @@ private fun ScanCodeDetectionButtonRow( scanCode: Int?, isEnabled: Boolean, isScanCodeSelected: Boolean, - onSelectedChange: (Boolean) -> Unit + onSelectedChange: (Boolean) -> Unit, + isCompact: Boolean, ) { Column(modifier) { Text( @@ -341,48 +336,39 @@ private fun ScanCodeDetectionButtonRow( ) Spacer(Modifier.height(8.dp)) - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - Spacer(Modifier.width(16.dp)) - SegmentedButton( - selected = !isScanCodeSelected, - onClick = { onSelectedChange(false) }, - shape = SegmentedButtonDefaults.itemShape( - index = 0, - count = 2, - ), - enabled = false - ) { - val text = if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { - stringResource(R.string.trigger_use_key_code_button_disabled) - } else { - stringResource(R.string.trigger_use_key_code_button_enabled, keyCode) - - } - Text(text) + val buttonStates = listOf( + false to if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { + stringResource(R.string.trigger_use_key_code_button_disabled) + } else { + stringResource(R.string.trigger_use_key_code_button_enabled, keyCode) + }, + true to if (scanCode == null) { + stringResource(R.string.trigger_use_scan_code_button_disabled) + } else { + stringResource(R.string.trigger_use_scan_code_button_enabled, scanCode) } + ) - SegmentedButton( - selected = isScanCodeSelected, - onClick = { onSelectedChange(true) }, - shape = SegmentedButtonDefaults.itemShape( - index = 1, - count = 2, - ), - enabled = isEnabled - ) { - val text = if (scanCode == null) { - stringResource(R.string.trigger_use_scan_code_button_disabled) - } else { - stringResource(R.string.trigger_use_scan_code_button_enabled, scanCode) - - } - Text(text) - } - Spacer(Modifier.width(16.dp)) - } + KeyMapperSegmentedButtonRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + buttonStates = buttonStates, + selectedState = isScanCodeSelected, + onStateSelected = onSelectedChange, + isCompact = isCompact, + isEnabled = isEnabled + ) } } +@Composable +private fun isVerticalCompactLayout(): Boolean { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + return windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT && windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT +} + @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable @@ -421,6 +407,44 @@ private fun PreviewKeyEvent() { } } +@OptIn(ExperimentalMaterial3Api::class) +@Preview(heightDp = 400, widthDp = 300) +@Composable +private fun PreviewKeyEventTiny() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = Expanded, + ) + + TriggerKeyOptionsBottomSheet( + sheetState = sheetState, + state = TriggerKeyOptionsState.KeyEvent( + doNotRemapChecked = true, + clickType = ClickType.DOUBLE_PRESS, + showClickTypes = true, + devices = listOf( + CheckBoxListItem( + id = "id1", + label = "Device 1", + isChecked = true, + ), + CheckBoxListItem( + id = "id2", + label = "Device 2", + isChecked = false, + ), + ), + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + isScanCodeDetectionSelected = true, + isScanCodeSettingEnabled = true + ), + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt new file mode 100644 index 0000000000..f25194933f --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt @@ -0,0 +1,106 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * A reusable segmented button row that follows KeyMapper's design patterns. + * + * @param modifier The modifier to apply to the segmented button row + * @param buttonStates List of pairs containing the data and display text for each button + * @param selectedState The currently selected state + * @param onStateSelected Callback when a button is selected + * @param isCompact Whether to use compact styling (smaller shapes, auto-sizing text) + * @param isEnabled Whether the buttons are enabled + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun KeyMapperSegmentedButtonRow( + modifier: Modifier = Modifier, + buttonStates: List>, + selectedState: T?, + onStateSelected: (T) -> Unit, + isCompact: Boolean = false, + isEnabled: Boolean = true, +) { + val colors = if (isEnabled) { + SegmentedButtonDefaults.colors() + } else { + // The disabled border color of the inactive button is by default not greyed out enough + SegmentedButtonDefaults.colors( + disabledInactiveBorderColor = + SegmentedButtonDefaults.colors().inactiveBorderColor.copy(alpha = 0.5f), + ) + } + + SingleChoiceSegmentedButtonRow( + modifier = modifier, + ) { + for (content in buttonStates) { + val (state, label) = content + val isSelected = state == selectedState + val isDisabled = !isEnabled + val isUnselectedDisabled = isDisabled && !isSelected + + if (isCompact) { + SegmentedButton( + modifier = Modifier.height(36.dp), + selected = isSelected, + onClick = { onStateSelected(state) }, + enabled = isEnabled, + icon = { }, + shape = SegmentedButtonDefaults.itemShape( + index = buttonStates.indexOf(content), + count = buttonStates.size, + baseShape = MaterialTheme.shapes.extraSmall + ), + colors = colors + ) { + BasicText( + text = label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = LocalTextStyle.current, + autoSize = TextAutoSize.StepBased( + maxFontSize = LocalTextStyle.current.fontSize, + minFontSize = 10.sp + ), + modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier + ) + } + } else { + SegmentedButton( + selected = isSelected, + onClick = { onStateSelected(state) }, + enabled = isEnabled, + shape = SegmentedButtonDefaults.itemShape( + index = buttonStates.indexOf(content), + count = buttonStates.size, + ), + colors = colors + ) { + Text( + text = label, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier + ) + } + } + } + } +} From 6b1e73ad8ec4c14cb4c500ecffee80f5f611281e Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 16:35:31 +0100 Subject: [PATCH 107/215] use text with lower weight for compact SegmentedButtonRow --- .../base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt index f25194933f..8c6a64ddd3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt @@ -71,15 +71,14 @@ fun KeyMapperSegmentedButtonRow( colors = colors ) { BasicText( + modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier, text = label, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = LocalTextStyle.current, autoSize = TextAutoSize.StepBased( maxFontSize = LocalTextStyle.current.fontSize, minFontSize = 10.sp ), - modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier ) } } else { From 333cd0ac64dae5451c80a3948a841e2db0657bb4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 16:35:47 +0100 Subject: [PATCH 108/215] fix: fix padding in TriggerKeyOptionsBottomSheet.kt --- .../trigger/TriggerKeyOptionsBottomSheet.kt | 92 ++++++++++++------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 21db4fdaef..4519929112 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.window.core.layout.WindowHeightSizeClass @@ -65,6 +66,8 @@ fun TriggerKeyOptionsBottomSheet( onEditFloatingLayoutClick: () -> Unit = {}, onScanCodeDetectionChanged: (Boolean) -> Unit = {}, ) { + val isCompact = isVerticalCompactLayout() + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, @@ -72,37 +75,32 @@ fun TriggerKeyOptionsBottomSheet( // Hide drag handle because other bottom sheets don't have it dragHandle = {}, ) { - val uriHandler = LocalUriHandler.current - val ctx = LocalContext.current - val helpUrl = stringResource(R.string.url_trigger_key_options_guide) + val scope = rememberCoroutineScope() Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Spacer(modifier = Modifier.height(12.dp)) Box(modifier = Modifier.fillMaxWidth()) { Text( - modifier = Modifier.align(Alignment.Center), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth() + .padding(horizontal = 48.dp), textAlign = TextAlign.Center, text = stringResource(R.string.trigger_key_options_title), style = MaterialTheme.typography.headlineMedium, + overflow = TextOverflow.Ellipsis ) - IconButton( + HelpIconButton( modifier = Modifier .align(Alignment.TopEnd) - .padding(horizontal = 8.dp), - onClick = { uriHandler.openUriSafe(ctx, helpUrl) }, - ) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.HelpOutline, - contentDescription = null, - ) - } + .padding(horizontal = 8.dp) + ) } - Spacer(modifier = Modifier.height(8.dp)) - val isCompact = isVerticalCompactLayout() + Spacer(modifier = Modifier.height(8.dp)) if (state is TriggerKeyOptionsState.KeyEvent) { ScanCodeDetectionButtonRow( @@ -149,6 +147,8 @@ fun TriggerKeyOptionsBottomSheet( .padding(horizontal = 16.dp), state, onSelectClickType, isCompact = isCompact ) + + Spacer(Modifier.height(8.dp)) } if (state is TriggerKeyOptionsState.KeyEvent) { @@ -158,6 +158,8 @@ fun TriggerKeyOptionsBottomSheet( style = MaterialTheme.typography.titleSmall, ) + Spacer(Modifier.height(8.dp)) + for (device in state.devices) { RadioButtonText( modifier = Modifier.padding(horizontal = 8.dp), @@ -288,6 +290,25 @@ fun TriggerKeyOptionsBottomSheet( } } +@Composable +private fun HelpIconButton( + modifier: Modifier +) { + val uriHandler = LocalUriHandler.current + val helpUrl = stringResource(R.string.url_trigger_key_options_guide) + val ctx = LocalContext.current + + IconButton( + modifier = modifier, + onClick = { uriHandler.openUriSafe(ctx, helpUrl) }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.HelpOutline, + contentDescription = null, + ) + } +} + @Composable private fun ClickTypeSection( modifier: Modifier, @@ -295,27 +316,30 @@ private fun ClickTypeSection( onSelectClickType: (ClickType) -> Unit, isCompact: Boolean ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.trigger_key_click_types_header), - style = MaterialTheme.typography.titleSmall, - ) - - val clickTypeButtonContent: List> = buildList { - add(ClickType.SHORT_PRESS to stringResource(R.string.radio_button_short_press)) - if (state.showLongPressClickType) { - add(ClickType.LONG_PRESS to stringResource(R.string.radio_button_long_press)) + Column(modifier) { + Text( + text = stringResource(R.string.trigger_key_click_types_header), + style = MaterialTheme.typography.titleSmall, + ) + + Spacer(Modifier.height(8.dp)) + + val clickTypeButtonContent: List> = buildList { + add(ClickType.SHORT_PRESS to stringResource(R.string.radio_button_short_press)) + if (state.showLongPressClickType) { + add(ClickType.LONG_PRESS to stringResource(R.string.radio_button_long_press)) + } + add(ClickType.DOUBLE_PRESS to stringResource(R.string.radio_button_double_press)) } - add(ClickType.DOUBLE_PRESS to stringResource(R.string.radio_button_double_press)) - } - KeyMapperSegmentedButtonRow( - modifier = modifier, - buttonStates = clickTypeButtonContent, - selectedState = state.clickType, - onStateSelected = onSelectClickType, - isCompact = isCompact - ) + KeyMapperSegmentedButtonRow( + modifier = Modifier.fillMaxWidth(), + buttonStates = clickTypeButtonContent, + selectedState = state.clickType, + onStateSelected = onSelectClickType, + isCompact = isCompact + ) + } } @Composable From 727e4668ac5083d3f7e4dfbe156a5dd7ff8d361e Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 18:47:31 +0100 Subject: [PATCH 109/215] refactor trigger validation when configuring --- .../base/trigger/ConfigTriggerDelegate.kt | 268 ++++-------- .../keymapper/base/trigger/EvdevTriggerKey.kt | 7 + .../base/trigger/KeyCodeTriggerKey.kt | 2 + .../base/trigger/KeyEventTriggerKey.kt | 10 +- .../keymapper/base/trigger/TriggerKey.kt | 26 ++ .../base/trigger/TriggerValidator.kt | 100 +++++ .../base/trigger/ConfigTriggerDelegateTest.kt | 396 ++++++++++++++++++ .../sysbridge/SystemBridgeHiltModule.kt | 2 +- 8 files changed, 616 insertions(+), 195 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index d7594bbdde..3fe2237faf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -22,35 +22,13 @@ class ConfigTriggerDelegate { TriggerMode.Undefined -> ClickType.SHORT_PRESS } - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> keyToCompare.buttonUid == buttonUid } - val triggerKey = FloatingButtonKey( buttonUid = buttonUid, button = button, clickType = clickType, ) - var newKeys = trigger.keys.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - return trigger.copy(keys = newKeys, mode = newMode) + return addTriggerKey(trigger, triggerKey) } @@ -61,28 +39,9 @@ class ConfigTriggerDelegate { TriggerMode.Undefined -> ClickType.SHORT_PRESS } - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsAssistantKey = trigger.keys.any { it is AssistantTriggerKey } - val triggerKey = AssistantTriggerKey(type = type, clickType = clickType) - val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsAssistantKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys. - - It must be a short press because long pressing the assistant key isn't supported. - */ - !containsAssistantKey -> TriggerMode.Parallel(ClickType.SHORT_PRESS) - else -> trigger.mode - } - - return trigger.copy(keys = newKeys, mode = newMode) + return addTriggerKey(trigger, triggerKey) } fun addFingerprintGesture(trigger: Trigger, type: FingerprintGestureType): Trigger { @@ -92,28 +51,9 @@ class ConfigTriggerDelegate { TriggerMode.Undefined -> ClickType.SHORT_PRESS } - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsFingerprintGesture = trigger.keys.any { it is FingerprintTriggerKey } - val triggerKey = FingerprintTriggerKey(type = type, clickType = clickType) - val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsFingerprintGesture -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys. - - It must be a short press because long pressing the assistant key isn't supported. - */ - !containsFingerprintGesture -> TriggerMode.Parallel(ClickType.SHORT_PRESS) - else -> trigger.mode - } - - return trigger.copy(keys = newKeys, mode = newMode) + return addTriggerKey(trigger, triggerKey) } /** @@ -134,14 +74,6 @@ class ConfigTriggerDelegate { TriggerMode.Undefined -> ClickType.SHORT_PRESS } - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) - } - var consumeKeyEvent = true // Issue #753 @@ -151,9 +83,13 @@ class ConfigTriggerDelegate { // Scan code detection should be turned on by default if there are other // keys from the same device that report the same key code but have a different scan code. - val conflictingKeys = otherTriggerKeys.plus(trigger.keys) + val logicallyEqualKeys = otherTriggerKeys.plus(trigger.keys) .filterIsInstance() - .filter { it.isConflictingKey(keyCode, scanCode, device) } + .filter { + it.keyCode == keyCode + && it.scanCode != scanCode + && it.device == device + } val triggerKey = KeyEventTriggerKey( keyCode = keyCode, @@ -162,40 +98,12 @@ class ConfigTriggerDelegate { scanCode = scanCode, consumeEvent = consumeKeyEvent, requiresIme = requiresIme, - detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty() + detectWithScanCodeUserSetting = logicallyEqualKeys.isNotEmpty() ) - var newKeys = trigger.keys.filter { it !is EvdevTriggerKey }.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - return trigger.copy(keys = newKeys, mode = newMode) - } + val newKeys = trigger.keys.filter { it !is EvdevTriggerKey } - /** - * This will return true if the key has same key code but different - * scan code, and is from the same device. - */ - private fun KeyEventTriggerKey.isConflictingKey( - keyCode: Int, - scanCode: Int, - device: KeyEventTriggerDevice, - ): Boolean { - return this.keyCode == keyCode - && this.scanCode != scanCode - && this.device.isSameDevice(device) + return addTriggerKey(trigger.copy(keys = newKeys), triggerKey) } fun addEvdevTriggerKey( @@ -211,19 +119,15 @@ class ConfigTriggerDelegate { TriggerMode.Undefined -> ClickType.SHORT_PRESS } - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .filterIsInstance() - .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device == device - } - // Scan code detection should be turned on by default if there are other // keys from the same device that report the same key code but have a different scan code. val conflictingKeys = otherTriggerKeys.plus(trigger.keys) .filterIsInstance() - .filter { it.isConflictingKey(keyCode, scanCode, device) } + .filter { + it.keyCode == keyCode + && it.scanCode != scanCode + && it.device == device + } val triggerKey = EvdevTriggerKey( keyCode = keyCode, @@ -234,7 +138,20 @@ class ConfigTriggerDelegate { detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty() ) - var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey }.plus(triggerKey) + val newKeys = trigger.keys.filter { it !is KeyEventTriggerKey } + + return addTriggerKey(trigger.copy(keys = newKeys), triggerKey) + } + + private fun addTriggerKey( + trigger: Trigger, + key: TriggerKey + ): Trigger { + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys.any { otherKey -> key.isLogicallyEqual(otherKey) } + + var newKeys: List = trigger.keys.plus(key) val newMode = when { trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence @@ -243,28 +160,14 @@ class ConfigTriggerDelegate { /* Automatically make it a parallel trigger when the user makes a trigger with more than one key because this is what most users are expecting when they make a trigger with multiple keys */ newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) + newKeys = newKeys.map { it.setClickType(key.clickType) } + TriggerMode.Parallel(key.clickType) } else -> trigger.mode } - return trigger.copy(keys = newKeys, mode = newMode) - } - - /** - * This will return true if the key has same key code but different - * scan code, and is from the same device. - */ - private fun EvdevTriggerKey.isConflictingKey( - keyCode: Int, - scanCode: Int, - device: EvdevDeviceInfo, - ): Boolean { - return this.keyCode == keyCode - && this.scanCode != scanCode - && this.device == device + return trigger.copy(keys = newKeys, mode = newMode).validate() } fun removeTriggerKey(trigger: Trigger, uid: String): Trigger { @@ -277,10 +180,11 @@ class ConfigTriggerDelegate { else -> trigger.mode } - return trigger.copy(keys = newKeys, mode = newMode) + return trigger.copy(keys = newKeys, mode = newMode).validate() } fun moveTriggerKey(trigger: Trigger, fromIndex: Int, toIndex: Int): Trigger { + // Don't need to validate. This should be low latency so moving is responsive. return trigger.copy( keys = trigger.keys.toMutableList().apply { add(toIndex, removeAt(fromIndex)) @@ -299,39 +203,27 @@ class ConfigTriggerDelegate { } val oldKeys = trigger.keys - var newKeys = oldKeys - - // set all the keys to a short press if coming from a non-parallel trigger - // because they must all be the same click type and can't all be double pressed - newKeys = newKeys - .map { key -> key.setClickType(clickType = ClickType.SHORT_PRESS) } - // remove duplicates of keys that have the same keycode and device id - .distinctBy { key -> - when (key) { - // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time - is AssistantTriggerKey, is FingerprintTriggerKey -> 0 - is KeyEventTriggerKey -> Pair( - key.keyCode, - key.device, - ) - - is FloatingButtonKey -> key.buttonUid - is EvdevTriggerKey -> Pair( - key.keyCode, - key.device, - ) + val newKeys: MutableList = mutableListOf() + + // In a parallel trigger keys must be triggered by different key events + outerLoop@ for (key in oldKeys) { + for (other in newKeys) { + if (key.isLogicallyEqual(other)) { + continue@outerLoop } } - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(newKeys[0].clickType) + // set all the keys to a short press if coming from a non-parallel trigger + // because they must all be the same click type and can't all be double pressed + newKeys.add(key.setClickType(ClickType.SHORT_PRESS)) } - return trigger.copy(keys = newKeys, mode = newMode) + return trigger + .copy(keys = newKeys, mode = TriggerMode.Parallel(ClickType.SHORT_PRESS)) + .validate() } + fun setSequenceTriggerMode(trigger: Trigger): Trigger { if (trigger.mode == TriggerMode.Sequence) return trigger // undefined mode only allowed if one or no keys @@ -339,7 +231,7 @@ class ConfigTriggerDelegate { return trigger.copy(mode = TriggerMode.Undefined) } - return trigger.copy(mode = TriggerMode.Sequence) + return trigger.copy(mode = TriggerMode.Sequence).validate() } fun setUndefinedTriggerMode(trigger: Trigger): Trigger { @@ -350,7 +242,7 @@ class ConfigTriggerDelegate { return trigger } - return trigger.copy(mode = TriggerMode.Undefined) + return trigger.copy(mode = TriggerMode.Undefined).validate() } fun setTriggerShortPress(trigger: Trigger): Trigger { @@ -364,7 +256,7 @@ class ConfigTriggerDelegate { } else { TriggerMode.Parallel(ClickType.SHORT_PRESS) } - return trigger.copy(keys = newKeys, mode = newMode) + return trigger.copy(keys = newKeys, mode = newMode).validate() } fun setTriggerLongPress(trigger: Trigger): Trigger { @@ -386,7 +278,7 @@ class ConfigTriggerDelegate { TriggerMode.Parallel(ClickType.LONG_PRESS) } - return trigger.copy(keys = newKeys, mode = newMode) + return trigger.copy(keys = newKeys, mode = newMode).validate() } fun setTriggerDoublePress(trigger: Trigger): Trigger { @@ -401,7 +293,7 @@ class ConfigTriggerDelegate { val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) } val newMode = TriggerMode.Undefined - return trigger.copy(keys = newKeys, mode = newMode) + return trigger.copy(keys = newKeys, mode = newMode).validate() } fun setTriggerKeyClickType(trigger: Trigger, keyUid: String, clickType: ClickType): Trigger { @@ -413,7 +305,7 @@ class ConfigTriggerDelegate { } } - return trigger.copy(keys = newKeys) + return trigger.copy(keys = newKeys).validate() } fun setTriggerKeyDevice( @@ -433,7 +325,7 @@ class ConfigTriggerDelegate { } } - return trigger.copy(keys = newKeys) + return trigger.copy(keys = newKeys).validate() } fun setTriggerKeyConsumeKeyEvent( @@ -461,7 +353,7 @@ class ConfigTriggerDelegate { } } - return trigger.copy(keys = newKeys) + return trigger.copy(keys = newKeys).validate() } fun setAssistantTriggerKeyType( @@ -481,7 +373,7 @@ class ConfigTriggerDelegate { } } - return trigger.copy(keys = newKeys) + return trigger.copy(keys = newKeys).validate() } fun setFingerprintGestureType( @@ -501,11 +393,11 @@ class ConfigTriggerDelegate { } } - return trigger.copy(keys = newKeys) + return trigger.copy(keys = newKeys).validate() } fun setVibrateEnabled(trigger: Trigger, enabled: Boolean): Trigger { - return trigger.copy(vibrate = enabled) + return trigger.copy(vibrate = enabled).validate() } fun setVibrationDuration( @@ -514,25 +406,25 @@ class ConfigTriggerDelegate { defaultVibrateDuration: Int ): Trigger { return if (duration == defaultVibrateDuration) { - trigger.copy(vibrateDuration = null) + trigger.copy(vibrateDuration = null).validate() } else { - trigger.copy(vibrateDuration = duration) + trigger.copy(vibrateDuration = duration).validate() } } fun setLongPressDelay(trigger: Trigger, delay: Int, defaultLongPressDelay: Int): Trigger { return if (delay == defaultLongPressDelay) { - trigger.copy(longPressDelay = null) + trigger.copy(longPressDelay = null).validate() } else { - trigger.copy(longPressDelay = delay) + trigger.copy(longPressDelay = delay).validate() } } fun setDoublePressDelay(trigger: Trigger, delay: Int, defaultDoublePressDelay: Int): Trigger { return if (delay == defaultDoublePressDelay) { - trigger.copy(doublePressDelay = null) + trigger.copy(doublePressDelay = null).validate() } else { - trigger.copy(doublePressDelay = delay) + trigger.copy(doublePressDelay = delay).validate() } } @@ -542,45 +434,45 @@ class ConfigTriggerDelegate { defaultSequenceTriggerTimeout: Int ): Trigger { return if (delay == defaultSequenceTriggerTimeout) { - trigger.copy(sequenceTriggerTimeout = null) + trigger.copy(sequenceTriggerTimeout = null).validate() } else { - trigger.copy(sequenceTriggerTimeout = delay) + trigger.copy(sequenceTriggerTimeout = delay).validate() } } fun setLongPressDoubleVibrationEnabled(trigger: Trigger, enabled: Boolean): Trigger { - return trigger.copy(longPressDoubleVibration = enabled) + return trigger.copy(longPressDoubleVibration = enabled).validate() } fun setTriggerWhenScreenOff(trigger: Trigger, enabled: Boolean): Trigger { - return trigger.copy(screenOffTrigger = enabled) + return trigger.copy(screenOffTrigger = enabled).validate() } fun setTriggerFromOtherAppsEnabled(trigger: Trigger, enabled: Boolean): Trigger { - return trigger.copy(triggerFromOtherApps = enabled) + return trigger.copy(triggerFromOtherApps = enabled).validate() } fun setShowToastEnabled(trigger: Trigger, enabled: Boolean): Trigger { - return trigger.copy(showToast = enabled) + return trigger.copy(showToast = enabled).validate() } fun setScanCodeDetectionEnabled(trigger: Trigger, keyUid: String, enabled: Boolean): Trigger { - val newKeys = trigger.keys.map { key -> - if (key.uid == keyUid && key is KeyCodeTriggerKey && key.isScanCodeDetectionUserConfigurable()) { - when (key) { + val newKeys = trigger.keys.map { otherKey -> + if (otherKey.uid == keyUid && otherKey is KeyCodeTriggerKey && otherKey.isScanCodeDetectionUserConfigurable()) { + when (otherKey) { is KeyEventTriggerKey -> { - key.copy(detectWithScanCodeUserSetting = enabled) + otherKey.copy(detectWithScanCodeUserSetting = enabled) } is EvdevTriggerKey -> { - key.copy(detectWithScanCodeUserSetting = enabled) + otherKey.copy(detectWithScanCodeUserSetting = enabled) } } } else { - key + otherKey } } - return trigger.copy(keys = newKeys) + return trigger.copy(keys = newKeys).validate() } } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt index c0c3013003..776f353ef2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt @@ -25,6 +25,13 @@ data class EvdevTriggerKey( override val allowedDoublePress: Boolean = true override val allowedLongPress: Boolean = true + override fun isSameDevice(otherKey: KeyCodeTriggerKey): Boolean { + if (otherKey !is EvdevTriggerKey) { + return false + } + return device == otherKey.device + } + companion object { fun fromEntity(entity: EvdevTriggerKeyEntity): TriggerKey { val clickType = when (entity.clickType) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt index 419dc675e5..3ebc041f5d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt @@ -28,6 +28,8 @@ sealed interface KeyCodeTriggerKey { * doesn't change. */ val consumeEvent: Boolean + + fun isSameDevice(otherKey: KeyCodeTriggerKey): Boolean } fun KeyCodeTriggerKey.detectWithScancode(): Boolean { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt index 0e7a4f3abf..c405652920 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -27,13 +27,11 @@ data class KeyEventTriggerKey( override val allowedLongPress: Boolean = true override val allowedDoublePress: Boolean = true - override fun toString(): String { - val deviceString = when (device) { - KeyEventTriggerDevice.Any -> "any" - is KeyEventTriggerDevice.External -> "external" - KeyEventTriggerDevice.Internal -> "internal" + override fun isSameDevice(otherKey: KeyCodeTriggerKey): Boolean { + if (otherKey !is KeyEventTriggerKey) { + return false } - return "KeyCodeTriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeEvent) " + return device.isSameDevice(otherKey.device) } // key code -> click type -> device -> consume key event diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt index e3681f1426..5845cb546c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt @@ -20,4 +20,30 @@ sealed class TriggerKey : Comparable { } override fun compareTo(other: TriggerKey) = this.javaClass.name.compareTo(other.javaClass.name) + + fun isLogicallyEqual(other: TriggerKey): Boolean { + when { + this is KeyCodeTriggerKey && other is KeyCodeTriggerKey -> { + if (this.detectWithScancode()) { + return this.scanCode == other.scanCode && this.isSameDevice(other) + } else { + return this.keyCode == other.keyCode && this.isSameDevice(other) + } + } + + this is AssistantTriggerKey && other is AssistantTriggerKey -> { + return this.type == other.type + } + + this is FingerprintTriggerKey && other is FingerprintTriggerKey -> { + return this.type == other.type + } + + this is FloatingButtonKey && other is FloatingButtonKey -> { + return this.buttonUid == other.buttonUid + } + } + + return false + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt new file mode 100644 index 0000000000..0231d8f9b2 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt @@ -0,0 +1,100 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.keymaps.ClickType + +/** + * Checks that the trigger is still valid. If it is a parallel trigger it removes + * conflicting keys that can't both be pressed down at the same time. And if it is a single + * key trigger then it will check it has one key. If not then it will convert it into a sequence + * trigger and not a parallel trigger so no keys are removed. + */ +fun Trigger.validate(): Trigger { + when (this.mode) { + is TriggerMode.Parallel -> { + return validateParallelTrigger(this) + } + + TriggerMode.Undefined -> { + return validateSingleKeyTrigger(this) + } + + TriggerMode.Sequence -> { + // No validation needed for sequence triggers. Any keys can be pressed in any order + if (keys.size <= 1) { + return copy(mode = TriggerMode.Undefined) + } + + return this + } + } +} + +private fun validateSingleKeyTrigger(trigger: Trigger): Trigger { + if (trigger.keys.size > 1) { + // If there are multiple keys then we convert it to a sequence trigger + return Trigger(mode = TriggerMode.Sequence, keys = trigger.keys.take(1)) + } else { + return trigger + } +} + +private fun validateParallelTrigger(trigger: Trigger): Trigger { + if (trigger.keys.size <= 1) { + return trigger.copy(mode = TriggerMode.Undefined) + } + + var newMode = trigger.mode + + outerLoop@ for (key in trigger.keys) { + // If there are conflicting keys then set the mode to sequence trigger + for (otherKey in trigger.keys) { + if (key != otherKey && key.isLogicallyEqual(otherKey)) { + newMode = TriggerMode.Sequence + break@outerLoop + } + } + + // Set the trigger mode to a short press if any keys are not compatible with the selected + // trigger mode + if (newMode is TriggerMode.Parallel) { + if ( + (newMode.clickType == ClickType.LONG_PRESS && !key.allowedLongPress) || + (newMode.clickType == ClickType.DOUBLE_PRESS && !key.allowedDoublePress) + ) { + newMode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS) + } + } + } + + var newKeys = trigger.keys + + // If the trigger is still a parallel trigger then check that all the keys can be + // pressed at the same time. + if (newMode is TriggerMode.Parallel) { + newKeys = trigger.keys.distinctBy { key -> + when (key) { + // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time + is AssistantTriggerKey, is FingerprintTriggerKey -> 0 + is FloatingButtonKey -> key.buttonUid + + is KeyEventTriggerKey -> { + if (key.detectWithScancode()) { + Pair(key.scanCode, key.device) + } else { + Pair(key.keyCode, key.device) + } + } + + is EvdevTriggerKey -> { + if (key.detectWithScancode()) { + Pair(key.scanCode, key.device) + } else { + Pair(key.keyCode, key.device) + } + } + } + }.toMutableList() + } + + return trigger.copy(mode = newMode, keys = newKeys) +} \ No newline at end of file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt index 3d935221ab..d9f0627656 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt @@ -8,22 +8,176 @@ import io.github.sds100.keymapper.base.utils.sequenceTrigger import io.github.sds100.keymapper.base.utils.singleKeyTrigger import io.github.sds100.keymapper.base.utils.triggerKey import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.system.inputevents.Scancode import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.instanceOf import org.hamcrest.Matchers.`is` +import org.junit.After import org.junit.Before import org.junit.Test +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic class ConfigTriggerDelegateTest { + private lateinit var mockedKeyEvent: MockedStatic private lateinit var delegate: ConfigTriggerDelegate @Before fun before() { + + mockedKeyEvent = mockStatic(KeyEvent::class.java) + mockedKeyEvent.`when` { KeyEvent.getMaxKeyCode() }.thenReturn(1000) + delegate = ConfigTriggerDelegate() } + @After + fun tearDown() { + mockedKeyEvent.close() + } + + @Test + fun `Remove keys with the same scan code if scan code detection is enabled when switching to a parallel trigger`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ) + + val trigger = sequenceTrigger( + key, + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + + assertThat(newTrigger.mode, `is`(TriggerMode.Undefined)) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(key)) + } + + @Test + fun `Convert to sequence trigger when enabling scan code detection when scan codes are the same`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + val trigger = parallelTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ), + key + ) + + val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + assertThat(newTrigger.keys, hasSize(2)) + assertThat(newTrigger.keys[1], `is`(key.copy(detectWithScanCodeUserSetting = true))) + } + + @Test + fun `Do not remove other keys with the same scan code when enabling scan code detection for sequence triggers`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ) + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + key + ) + + val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) + assertThat(newTrigger.keys, hasSize(2)) + assertThat( + newTrigger.keys, + contains(trigger.keys[0], key.copy(detectWithScanCodeUserSetting = true)) + ) + } + + @Test + fun `Convert to sequence trigger when disabling scan code detection and other keys with same key code`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEUP, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + + val trigger = parallelTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + key + ) + + val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, false) + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + assertThat(newTrigger.keys, hasSize(2)) + assertThat(newTrigger.keys[1], `is`(key.copy(detectWithScanCodeUserSetting = false))) + } + + + @Test + fun `Do not remove other keys from different devices when enabling scan code detection`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External(descriptor = "keyboard0", name = "Keyboard"), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + val trigger = parallelTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + key + ) + + val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) + assertThat(newTrigger.keys, hasSize(2)) + assertThat( + newTrigger.keys, + contains(trigger.keys[0], key.copy(detectWithScanCodeUserSetting = true)) + ) + } + + /** * Issue #761 */ @@ -395,6 +549,65 @@ class ConfigTriggerDelegateTest { assertThat(triggerWithSecondKey.mode, `is`(TriggerMode.Sequence)) } + @Test + fun `Adding a key which has the same scan code as another key with scan code detection enabled makes the trigger a sequence`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val trigger = parallelTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + clickType = ClickType.SHORT_PRESS, + device = device, + detectWithScanCodeUserSetting = true + ), + AssistantTriggerKey(type = AssistantTriggerType.ANY, clickType = ClickType.SHORT_PRESS) + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = device, + ) + + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding a key which has the same scan code as the only other key with scan code detection enabled makes the trigger a sequence`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + clickType = ClickType.SHORT_PRESS, + device = device, + detectWithScanCodeUserSetting = true + ) + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = device, + ) + + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + } + @Test fun `Adding an evdev trigger key to a sequence trigger keeps it sequence`() { val device = EvdevDeviceInfo( @@ -853,4 +1066,187 @@ class ConfigTriggerDelegateTest { // THEN assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(true)) } + + + @Test + fun `Remove keys with same key code from the same internal device when converting to a parallel trigger`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + + val trigger = sequenceTrigger( + key, + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(key)) + } + + @Test + fun `Do not remove keys with same key code from different devices when converting to a parallel trigger`() { + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard" + ), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(2)) + assertThat(newTrigger.keys, `is`(trigger.keys)) + } + + @Test + fun `Do not remove keys with different key code from the same device when converting to a parallel trigger`() { + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEUP, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(2)) + assertThat(newTrigger.keys, `is`(trigger.keys)) + } + + @Test + fun `Remove keys from an internal device if it conflicts with any-device key when converting to a parallel trigger`() { + val internalKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + + val anyDeviceKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Any, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + + val trigger = sequenceTrigger(internalKey, anyDeviceKey) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(internalKey)) + } + + @Test + fun `Remove keys with the same key code from the same external device when converting to a parallel trigger`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard" + ), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ) + + val trigger = sequenceTrigger( + key, + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard" + ), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(key)) + } + + @Test + fun `Remove conflicting keys that are all any-device or internal when converting to a parallel trigger`() { + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Any, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Any, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard" + ), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Any, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(trigger.keys[0])) + } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt index 63ca029459..dda80eeb70 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt @@ -16,7 +16,7 @@ abstract class SystemBridgeHiltModule { @Singleton @Binds - abstract fun bindPrivServiceSetupController(impl: SystemBridgeSetupControllerImpl): SystemBridgeSetupController + abstract fun bindSystemBridgeSetupController(impl: SystemBridgeSetupControllerImpl): SystemBridgeSetupController @Singleton @Binds From 7ba0c71c12eb475c6a75157a5c2c9f07620f1ea6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 21:31:08 +0100 Subject: [PATCH 110/215] #1394 fix some memory access crashes and race conditions in libevdev_jni.cpp --- .../keymapper/base/input/EvdevHandleCache.kt | 7 +- .../keymapper/sysbridge/ISystemBridge.aidl | 1 - sysbridge/src/main/cpp/libevdev_jni.cpp | 277 +++++++----------- .../sysbridge/service/SystemBridge.kt | 10 +- 4 files changed, 119 insertions(+), 176 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt index f22223f400..9b6923a5ba 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt @@ -6,10 +6,12 @@ import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.system.devices.DevicesAdapter import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext import timber.log.Timber class EvdevHandleCache( @@ -22,7 +24,10 @@ class EvdevHandleCache( systemBridge ?: return@combine emptyMap() try { - systemBridge.evdevInputDevices.associateBy { it.path } + // Do it on a separate thread in case there is deadlock + withContext(Dispatchers.IO) { + systemBridge.evdevInputDevices.associateBy { it.path } + } } catch (e: RemoteException) { Timber.e("Failed to get evdev input devices from system bridge $e") emptyMap() diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 6c2df2c6fd..7578496fbd 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -12,7 +12,6 @@ interface ISystemBridge { void destroy() = 16777114; boolean grabEvdevDevice(String devicePath) = 1; - boolean grabAllEvdevDevices() = 2; boolean ungrabEvdevDevice(String devicePath) = 3; boolean ungrabAllEvdevDevices() = 4; diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index c0d18ba7eb..fa036e303b 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -34,101 +34,25 @@ struct DeviceContext { struct libevdev_uinput *uinputDev; struct android::KeyLayoutMap keyLayoutMap; char devicePath[256]; + int fd; }; static int epollFd = -1; -static int commandEventFd = -1; +static std::mutex epollMutex; +static int commandEventFd = -1; static std::queue commandQueue; static std::mutex commandMutex; // This maps the file descriptor of an evdev device to its context. -static std::map *evdevDevices = new std::map(); +static std::map *evdevDevices = new std::map(); static std::mutex evdevDevicesMutex; +static std::map *fdToDevicePath = new std::map(); #define DEBUG_PROBE false -static int findEvdevDevice( - std::string name, - int bus, - int vendor, - int product, - libevdev **outDev, - char *outPath -) { - DIR *dir = opendir("/dev/input"); - - if (dir == nullptr) { - LOGE("Failed to open /dev/input directory"); - return -1; - } - - struct dirent *entry; - - while ((entry = readdir(dir)) != nullptr) { - // Skip . and .. entries - if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { - continue; - } - - char fullPath[256]; - snprintf(fullPath, sizeof(fullPath), "/dev/input/%s", entry->d_name); - - // MUST be NONBLOCK so that the loop reading the evdev events eventually returns - // due to an EAGAIN error. - int fd = open(fullPath, O_RDONLY | O_NONBLOCK); - - if (fd == -1) { - continue; - } - - int status = libevdev_new_from_fd(fd, outDev); - - if (status != 0) { - LOGE("Failed to open libevdev device from path %s: %s", fullPath, strerror(errno)); - close(fd); - continue; - } - - const char *devName = libevdev_get_name(*outDev); - int devVendor = libevdev_get_id_vendor(*outDev); - int devProduct = libevdev_get_id_product(*outDev); - int devBus = libevdev_get_id_bustype(*outDev); - - if (DEBUG_PROBE) { - LOGD("Evdev device: %s, bus: %d, vendor: %d, product: %d, path: %s", - devName, devBus, devVendor, devProduct, fullPath); - } - - if (devName != name || - devVendor != vendor || - devProduct != product || - // The hidden device bus field was only added to InputDevice.java in Android 14. - // So only check it if it is a real value - (bus != -1 && devBus != bus)) { - - libevdev_free(*outDev); - close(fd); - continue; - } - - closedir(dir); - - strcpy(outPath, fullPath); - return 0; - } - - closedir(dir); - - LOGE("Input device not found with name: %s, bus: %d, vendor: %d, product: %d", name.c_str(), - bus, - vendor, product); - - return -1; -} - jint JNI_OnLoad(JavaVM *vm, void *reserved) { - evdevDevices = new std::map(); + evdevDevices = new std::map(); return JNI_VERSION_1_6; } @@ -145,18 +69,21 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNa bool result = false; { + std::lock_guard epollLock(epollMutex); + if (epollFd == -1) { + LOGE("Epoll is not initialized. Cannot grab evdev device."); + return false; + } + // Lock to prevent concurrent grab/ungrab operations on the same device std::lock_guard lock(evdevDevicesMutex); // Check if device is already grabbed - for (const auto &pair: *evdevDevices) { - DeviceContext context = pair.second; - if (strcmp(context.devicePath, devicePath) == 0) { - LOGW("Device %s is already grabbed. Maybe it is a virtual uinput device.", - devicePath); - env->ReleaseStringUTFChars(jDevicePath, devicePath); - return false; - } + if (evdevDevices->contains(devicePath)) { + LOGW("Device %s is already grabbed. Maybe it is a virtual uinput device.", + devicePath); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } // Perform synchronous grab operation @@ -189,8 +116,6 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNa return false; } - int evdevFd = libevdev_get_fd(dev); - // Create a dummy InputDeviceIdentifier for key layout loading android::InputDeviceIdentifier identifier; identifier.name = std::string(libevdev_get_name(dev)); @@ -240,30 +165,30 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNa dev, uinputDev, *klResult.value(), - {} // Initialize devicePath array + {}, // Initialize devicePath array + fd }; strcpy(context.devicePath, devicePath); - // Add to epoll for event monitoring (only if event loop is running) - if (epollFd != -1) { - struct epoll_event event{}; - event.events = EPOLLIN; - event.data.fd = evdevFd; - - if (epoll_ctl(epollFd, EPOLL_CTL_ADD, evdevFd, &event) == -1) { - LOGE("Failed to add new device to epoll: %s", strerror(errno)); - libevdev_uinput_destroy(uinputDev); - libevdev_grab(dev, LIBEVDEV_UNGRAB); - libevdev_free(dev); - close(fd); - env->ReleaseStringUTFChars(jDevicePath, devicePath); - return false; - } + // Already checked at the start of the method whether epoll was running + struct epoll_event event{}; + event.events = EPOLLIN; + event.data.fd = fd; + + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &event) == -1) { + LOGE("Failed to add new device to epoll: %s", strerror(errno)); + libevdev_uinput_destroy(uinputDev); + libevdev_grab(dev, LIBEVDEV_UNGRAB); + libevdev_free(dev); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; } - evdevDevices->insert_or_assign(evdevFd, context); + evdevDevices->insert_or_assign(devicePath, context); result = true; + fdToDevicePath->insert_or_assign(fd, devicePath); LOGI("Grabbed device %s, %s", libevdev_get_name(dev), context.devicePath); } @@ -272,8 +197,11 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNa return result; } - -void onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { +/** + * @return Whether the events were all handled by the callback. If the callback dies then this + * returns false. + */ +bool onEpollEvdevEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { struct input_event inputEvent{}; int rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); @@ -285,14 +213,18 @@ void onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); bool returnValue; - callback->onEvdevEvent(deviceContext->devicePath, - inputEvent.time.tv_sec, - inputEvent.time.tv_usec, - inputEvent.type, - inputEvent.code, - inputEvent.value, - outKeycode, - &returnValue); + ndk::ScopedAStatus callbackResult = callback->onEvdevEvent(deviceContext->devicePath, + inputEvent.time.tv_sec, + inputEvent.time.tv_usec, + inputEvent.type, + inputEvent.code, + inputEvent.value, + outKeycode, + &returnValue); + + if (!callbackResult.isOk()) { + return false; + } if (!returnValue) { libevdev_uinput_write_event(deviceContext->uinputDev, @@ -309,21 +241,21 @@ void onEpollEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { &inputEvent); } } while (rc != -EAGAIN); + + return true; } // Set this to some upper limit. It is unlikely that Key Mapper will be polling // more than a few evdev devices at once. static int MAX_EPOLL_EVENTS = 100; -void handleCommand(const Command &cmd) { - // Only STOP commands are handled here now, grab/ungrab are synchronous -} - extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, jobject thiz, jobject jCallbackBinder) { + std::unique_lock epollLock(epollMutex); + if (epollFd != -1 || commandEventFd != -1) { LOGE("The evdev event loop has already started."); return; @@ -350,9 +282,12 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo LOGE("Failed to add command eventfd to epoll: %s", strerror(errno)); close(epollFd); close(commandEventFd); + epollLock.unlock(); return; } + epollLock.unlock(); + AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); const ::ndk::SpAIBinder spBinder(callbackAIBinder); std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); @@ -362,13 +297,19 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo LOGI("Start evdev event loop"); - callback->onEvdevEventLoopStarted(); + ndk::ScopedAStatus callbackResult = callback->onEvdevEventLoopStarted(); + + if (!callbackResult.isOk()) { + LOGE("Callback is dead. Not starting evdev loop."); + return; + } while (running) { int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); for (int i = 0; i < n; ++i) { - if (events[i].data.fd == commandEventFd) { + int fd = events[i].data.fd; + if (fd == commandEventFd) { uint64_t val; ssize_t s = read(commandEventFd, &val, sizeof(val)); @@ -391,11 +332,22 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo running = false; break; } - handleCommand(cmd); } } else { - DeviceContext *dc = &evdevDevices->at(events[i].data.fd); - onEpollEvent(dc, callback.get()); + std::lock_guard lock(evdevDevicesMutex); + + auto it = fdToDevicePath->find(fd); + if (it != fdToDevicePath->end()) { + DeviceContext *dc = &evdevDevices->at(it->second); + // If handling the evdev event fails then stop the event loop + // and ungrab all the devices. + bool result = onEpollEvdevEvent(dc, callback.get()); + + if (!result) { + running = false; + break; + } + } } } } @@ -403,7 +355,8 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo // Cleanup std::lock_guard lock(evdevDevicesMutex); - for (auto const &[fd, dc]: *evdevDevices) { + for (auto const &[path, dc]: *evdevDevices) { + libevdev_uinput_destroy(dc.uinputDev); libevdev_grab(dc.evdev, LIBEVDEV_UNGRAB); libevdev_free(dc.evdev); } @@ -431,33 +384,31 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDevice // Lock to prevent concurrent grab/ungrab operations std::lock_guard lock(evdevDevicesMutex); - for (auto it = evdevDevices->begin(); it != evdevDevices->end(); ++it) { - if (strcmp(it->second.devicePath, devicePath) == 0) { - // Remove from epoll first (if event loop is running) - if (epollFd != -1) { - int fd = it->first; - if (epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr) == -1) { - LOGW("Failed to remove device from epoll: %s", strerror(errno)); - // Continue with ungrab even if epoll removal fails - } + auto it = evdevDevices->find(devicePath); + if (it != evdevDevices->end()) { + // Remove from epoll first (if event loop is running) + if (epollFd != -1) { + if (epoll_ctl(epollFd, EPOLL_CTL_DEL, it->second.fd, nullptr) == -1) { + LOGW("Failed to remove device from epoll: %s", strerror(errno)); + // Continue with ungrab even if epoll removal fails } + } - // Do this before freeing the evdev file descriptor - libevdev_uinput_destroy(it->second.uinputDev); + // Do this before freeing the evdev file descriptor + libevdev_uinput_destroy(it->second.uinputDev); - // Ungrab the device - libevdev_grab(it->second.evdev, LIBEVDEV_UNGRAB); + // Ungrab the device + libevdev_grab(it->second.evdev, LIBEVDEV_UNGRAB); - // Free resources - libevdev_free(it->second.evdev); + // Free resources + libevdev_free(it->second.evdev); - // Remove from device map - evdevDevices->erase(it); - result = true; + // Remove from device map + evdevDevices->erase(it); + fdToDevicePath->erase(it->second.fd); + result = true; - LOGI("Ungrabbed device %s", devicePath); - break; - } + LOGI("Ungrabbed device %s", devicePath); } if (!result) { @@ -502,16 +453,13 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNa bool result = false; { - std::lock_guard lock(evdevDevicesMutex); - for (const auto &pair: *evdevDevices) { - if (strcmp(pair.second.devicePath, devicePath) == 0) { - int rc = libevdev_uinput_write_event(pair.second.uinputDev, type, code, value); - if (rc == 0) { - rc = libevdev_uinput_write_event(pair.second.uinputDev, EV_SYN, SYN_REPORT, 0); - } - result = (rc == 0); - break; + auto it = evdevDevices->find(devicePath); + if (it != evdevDevices->end()) { + int rc = libevdev_uinput_write_event(it->second.uinputDev, type, code, value); + if (rc == 0) { + rc = libevdev_uinput_write_event(it->second.uinputDev, EV_SYN, SYN_REPORT, 0); } + result = (rc == 0); } } @@ -527,17 +475,17 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDev { // Lock to prevent concurrent grab/ungrab operations std::lock_guard lock(evdevDevicesMutex); + std::lock_guard epollLock(epollMutex); // Create a copy of the iterator to avoid issues with erasing during iteration auto devicesCopy = *evdevDevices; for (const auto &pair: devicesCopy) { - int fd = pair.first; const DeviceContext &context = pair.second; // Remove from epoll first (if event loop is running) if (epollFd != -1) { - if (epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr) == -1) { + if (epoll_ctl(epollFd, EPOLL_CTL_DEL, context.fd, nullptr) == -1) { LOGW("Failed to remove device %s from epoll: %s", context.devicePath, strerror(errno)); // Continue with ungrab even if epoll removal fails @@ -558,16 +506,11 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDev // Clear all devices from the map evdevDevices->clear(); + fdToDevicePath->clear(); } return true; } -extern "C" -JNIEXPORT jboolean JNICALL -Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabAllEvdevDevicesNative( - JNIEnv *env, jobject thiz) { - // TODO: implement grabAllEvdevDevicesNative() -} // Helper function to create a Java EvdevDeviceHandle object jobject diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 7658529e6e..804d2a774c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -40,7 +40,6 @@ internal class SystemBridge : ISystemBridge.Stub() { // TODO return error code and map this to a SystemBridgeError in key mapper external fun grabEvdevDeviceNative(devicePath: String): Boolean - external fun grabAllEvdevDevicesNative(): Boolean external fun ungrabEvdevDeviceNative(devicePath: String): Boolean external fun ungrabAllEvdevDevicesNative(): Boolean @@ -175,7 +174,9 @@ internal class SystemBridge : ISystemBridge.Stub() { private var evdevCallback: IEvdevCallback? = null private val evdevCallbackDeathRecipient: IBinder.DeathRecipient = IBinder.DeathRecipient { Log.i(TAG, "EvdevCallback binder died") - stopEvdevEventLoop() + coroutineScope.launch(Dispatchers.Default) { + stopEvdevEventLoop() + } } init { @@ -183,7 +184,6 @@ internal class SystemBridge : ISystemBridge.Stub() { @SuppressLint("UnsafeDynamicallyLoadedCode") System.load("$libraryPath/libevdev.so") - Log.i(TAG, "SystemBridge started") waitSystemService("package") @@ -263,10 +263,6 @@ internal class SystemBridge : ISystemBridge.Stub() { return grabEvdevDeviceNative(devicePath) } - override fun grabAllEvdevDevices(): Boolean { - return grabAllEvdevDevicesNative() - } - override fun ungrabEvdevDevice(devicePath: String?): Boolean { devicePath ?: return false ungrabEvdevDeviceNative(devicePath) From 03532c882d080c750bfa46ea0a70eab41c000a9d Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 21:55:47 +0100 Subject: [PATCH 111/215] #1394 show switch with icon for enabling/disabling PRO mode recording and have pulse dot for recording countdown --- .../base/trigger/BaseTriggerScreen.kt | 7 +- .../base/trigger/RecordTriggerButtonRow.kt | 142 +++++++++++++----- base/src/main/res/values/strings.xml | 2 +- 3 files changed, 109 insertions(+), 42 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index fab500fe36..a10b26ca69 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -191,6 +191,8 @@ private fun TriggerScreenVertical( ) { Surface(modifier = modifier) { Column { + val isCompact = isVerticalCompactLayout() + when (configState) { is ConfigTriggerState.Empty -> { Column( @@ -227,7 +229,6 @@ private fun TriggerScreenVertical( } is ConfigTriggerState.Loaded -> { - val isCompact = isVerticalCompactLayout() Spacer(Modifier.height(8.dp)) TriggerList( @@ -273,6 +274,10 @@ private fun TriggerScreenVertical( } } + if (!isCompact) { + Spacer(Modifier.height(8.dp)) + } + RecordTriggerButtonRow( modifier = Modifier .fillMaxWidth() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt index a383a03beb..e84bf2cdb5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt @@ -1,23 +1,35 @@ package io.github.sds100.keymapper.base.trigger +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape 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.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -29,6 +41,9 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperTapTarget +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon +import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIconDisabled import io.github.sds100.keymapper.base.utils.ui.compose.keyMapperShowcaseStyle @Composable @@ -42,18 +57,17 @@ fun RecordTriggerButtonRow( onSkipTapTarget: () -> Unit = {}, showAdvancedTriggerTapTarget: Boolean = false, onAdvancedTriggerTapTargetCompleted: () -> Unit = {}, + isProModeSelected: Boolean = false // TODO ) { Column { - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.trigger_record_with_pro_mode), - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = false, - onCheckedChange = {}) - } +// Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { +// Text( +// text = stringResource(R.string.trigger_record_with_pro_mode), +// overflow = TextOverflow.Ellipsis +// ) +// Spacer(modifier = Modifier.width(16.dp)) +// +// } Row(modifier, verticalAlignment = Alignment.CenterVertically) { IntroShowcase( showIntroShowCase = showRecordTriggerTapTarget, @@ -76,24 +90,39 @@ fun RecordTriggerButtonRow( 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, - ) - } + Switch( + checked = isProModeSelected, + onCheckedChange = {}, + enabled = recordTriggerState !is RecordTriggerState.CountingDown, + thumbContent = { + if (isProModeSelected) { + Icon(imageVector = KeyMapperIcons.ProModeIcon, contentDescription = null) + } else { + Icon( + imageVector = KeyMapperIcons.ProModeIconDisabled, + contentDescription = null + ) + + } + }) +// 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, +// ) +// } } } } @@ -117,22 +146,52 @@ private fun RecordTriggerButton( stringResource(R.string.button_record_trigger) } + // Create pulsing animation for the recording dot + val infiniteTransition = rememberInfiniteTransition(label = "recording_dot_pulse") + val alpha by infiniteTransition.animateFloat( + initialValue = 0.3f, + targetValue = 1.0f, + animationSpec = infiniteRepeatable( + animation = tween(1000), + repeatMode = RepeatMode.Reverse + ), + label = "recording_dot_alpha" + ) + 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, - ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + // White recording dot + if (state is RecordTriggerState.CountingDown) { + Box( + modifier = Modifier + .size(8.dp) + .alpha(alpha) + .background( + color = Color.White, + shape = CircleShape + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + + 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, + ) + } } } @@ -170,6 +229,7 @@ private fun PreviewCountingDown() { RecordTriggerButtonRow( modifier = Modifier.fillMaxWidth(), recordTriggerState = RecordTriggerState.CountingDown(3), + isProModeSelected = true ) } } @@ -183,6 +243,7 @@ private fun PreviewStopped() { RecordTriggerButtonRow( modifier = Modifier.fillMaxWidth(), recordTriggerState = RecordTriggerState.Idle, + isProModeSelected = false ) } } @@ -196,6 +257,7 @@ private fun PreviewStoppedCompact() { RecordTriggerButtonRow( modifier = Modifier.fillMaxWidth(), recordTriggerState = RecordTriggerState.Idle, + isProModeSelected = true ) } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index aee6c509d1..260869bfa6 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -397,7 +397,7 @@ NEW! Done Fix - Recording now (%d…) + Press your keys Add constraint Choose Key code Add extra From f4c4f9442bcf36eda7a2f1eaf379058d3c4291a9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 10 Aug 2025 22:15:28 +0100 Subject: [PATCH 112/215] #1394 fix device disconnection causing infinite loop in onEpollEvdevEvent --- .../keymapper/base/input/InputEventHub.kt | 29 ++++++----- sysbridge/src/main/cpp/libevdev_jni.cpp | 49 ++++++++++++++----- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 810f1d2bca..1d0eb71810 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -271,23 +271,26 @@ class InputEventHubImpl @Inject constructor( return } - try { - val ungrabResult = systemBridge.ungrabAllEvdevDevices() - Timber.i("Ungrabbed all evdev devices: $ungrabResult") + // Grabbing can block if there are other grabbing or event loop start/stop operations happening. + coroutineScope.launch(Dispatchers.IO) { + try { + val ungrabResult = systemBridge.ungrabAllEvdevDevices() + Timber.i("Ungrabbed all evdev devices: $ungrabResult") - if (!ungrabResult) { - Timber.e("Failed to ungrab all evdev devices before grabbing.") - return - } + if (!ungrabResult) { + Timber.e("Failed to ungrab all evdev devices before grabbing.") + return@launch + } - for (device in evdevDevices) { - val handle = evdevHandles.getByInfo(device) ?: continue - val grabResult = systemBridge.grabEvdevDevice(handle.path) + for (device in evdevDevices) { + val handle = evdevHandles.getByInfo(device) ?: continue + val grabResult = systemBridge.grabEvdevDevice(handle.path) - Timber.i("Grabbed evdev device ${device.name}: $grabResult") + Timber.i("Grabbed evdev device ${device.name}: $grabResult") + } + } catch (_: RemoteException) { + Timber.e("Failed to invalidate grabbed device. Is the system bridge dead?") } - } catch (_: RemoteException) { - Timber.e("Failed to invalidate grabbed device. Is the system bridge dead?") } } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index fa036e303b..5e652d4c93 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -206,6 +206,13 @@ bool onEpollEvdevEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { int rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); + if (rc < 0) { + if (rc != -EAGAIN) { + LOGE("libevdev_next_event failed with error %d: %s", rc, strerror(-rc)); + } + return rc == -EAGAIN; + } + do { if (rc == LIBEVDEV_READ_STATUS_SUCCESS) { // rc == 0 int32_t outKeycode = -1; @@ -240,6 +247,11 @@ bool onEpollEvdevEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC, &inputEvent); } + + if (rc < 0 && rc != -EAGAIN) { + LOGE("libevdev_next_event failed with error %d: %s", rc, strerror(-rc)); + return false; + } } while (rc != -EAGAIN); return true; @@ -334,18 +346,29 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo } } } else { - std::lock_guard lock(evdevDevicesMutex); - - auto it = fdToDevicePath->find(fd); - if (it != fdToDevicePath->end()) { - DeviceContext *dc = &evdevDevices->at(it->second); - // If handling the evdev event fails then stop the event loop - // and ungrab all the devices. - bool result = onEpollEvdevEvent(dc, callback.get()); - - if (!result) { - running = false; - break; + if ((events[i].events & (EPOLLHUP | EPOLLERR))) { + LOGI("Device disconnected, removing from epoll."); + epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr); + + std::lock_guard lock(evdevDevicesMutex); + auto it = fdToDevicePath->find(fd); + if (it != fdToDevicePath->end()) { + evdevDevices->erase(it->second); + fdToDevicePath->erase(it); + } + } else { + std::lock_guard lock(evdevDevicesMutex); + auto it = fdToDevicePath->find(fd); + if (it != fdToDevicePath->end()) { + DeviceContext *dc = &evdevDevices->at(it->second); + // If handling the evdev event fails then stop the event loop + // and ungrab all the devices. + bool result = onEpollEvdevEvent(dc, callback.get()); + + if (!result) { + running = false; + break; + } } } } @@ -562,6 +585,7 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_getEvdevDevicesNa std::vector deviceHandles; struct dirent *entry; + std::lock_guard lock(evdevDevicesMutex); while ((entry = readdir(dir)) != nullptr) { // Skip . and .. entries if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { @@ -574,7 +598,6 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_getEvdevDevicesNa bool ignoreDevice = false; { - std::lock_guard lock(evdevDevicesMutex); // Ignore this device if it is a uinput device we created for (const auto &pair: *evdevDevices) { DeviceContext context = pair.second; From d7fd092568b0a53de03468aaf9a14fc06e2a3cf4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 11 Aug 2025 00:56:25 +0100 Subject: [PATCH 113/215] #1394 always put the click type buttons in the same order --- .../keymapper/base/trigger/BaseTriggerScreen.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index a10b26ca69..70a66f2732 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -526,11 +526,18 @@ private fun ClickTypeRadioGroup( onSelectClickType: (ClickType) -> Unit, isCompact: Boolean, ) { - val clickTypeButtonContent: List> = clickTypes.map { clickType -> - when (clickType) { - ClickType.SHORT_PRESS -> clickType to stringResource(R.string.radio_button_short_press) - ClickType.LONG_PRESS -> clickType to stringResource(R.string.radio_button_long_press) - ClickType.DOUBLE_PRESS -> clickType to stringResource(R.string.radio_button_double_press) + // Always put the buttons in the same order + val clickTypeButtonContent: List> = buildList { + if (clickTypes.contains(ClickType.SHORT_PRESS)) { + add(ClickType.SHORT_PRESS to stringResource(R.string.radio_button_short_press)) + } + + if (clickTypes.contains(ClickType.LONG_PRESS)) { + add(ClickType.LONG_PRESS to stringResource(R.string.radio_button_long_press)) + } + + if (clickTypes.contains(ClickType.DOUBLE_PRESS)) { + add(ClickType.DOUBLE_PRESS to stringResource(R.string.radio_button_double_press)) } } From f16e4e001a9eacb0c02ba0dc03949a67a9dd0784 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 11 Aug 2025 02:48:41 +0100 Subject: [PATCH 114/215] #1394 stopping system bridge from pro mode screen works and showing root/shizuku detection is a WIP --- .../keymapper/base/BaseViewModelHiltModule.kt | 6 +- .../base/actions/ActionErrorSnapshot.kt | 1 + .../base/detection/DetectKeyMapsUseCase.kt | 2 +- .../keymapper/base/promode/ProModeScreen.kt | 208 +++++++++++++----- .../base/promode/ProModeSetupUseCase.kt | 25 --- .../base/promode/ProModeViewModel.kt | 93 ++++++-- .../base/promode/ShizukuSetupState.kt | 8 + .../base/promode/SystemBridgeSetupUseCase.kt | 114 ++++++++++ .../base/settings/ConfigSettingsUseCase.kt | 2 +- .../ManageNotificationsUseCase.kt | 2 +- .../utils/ui/compose/icons/ProModeDisabled.kt | 162 ++++++++++++++ base/src/main/res/values/strings.xml | 9 +- .../sysbridge/manager/SystemBridgeManager.kt | 25 ++- .../service/SystemBridgeSetupController.kt | 7 + .../service/SystemBridgeSetupStep.kt | 11 + .../permissions/AndroidPermissionAdapter.kt | 4 +- .../system/permissions/Permission.kt | 2 - .../sds100/keymapper/system/root/SuAdapter.kt | 26 +-- 18 files changed, 578 insertions(+), 129 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupUseCase.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt index 31f7d1133f..b31a3833c4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt @@ -31,8 +31,8 @@ import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCaseImpl import io.github.sds100.keymapper.base.logging.DisplayLogUseCase import io.github.sds100.keymapper.base.logging.DisplayLogUseCaseImpl -import io.github.sds100.keymapper.base.promode.ProModeSetupUseCase -import io.github.sds100.keymapper.base.promode.ProModeSetupUseCaseImpl +import io.github.sds100.keymapper.base.promode.SystemBridgeSetupUseCase +import io.github.sds100.keymapper.base.promode.SystemBridgeSetupUseCaseImpl import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCase import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCaseImpl import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase @@ -131,7 +131,7 @@ abstract class BaseViewModelHiltModule { @Binds @ViewModelScoped - abstract fun bindProModeSetupUseCase(impl: ProModeSetupUseCaseImpl): ProModeSetupUseCase + abstract fun bindProModeSetupUseCase(impl: SystemBridgeSetupUseCaseImpl): SystemBridgeSetupUseCase @Binds @ViewModelScoped diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index fb2d13bde6..fcfffc494e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -56,6 +56,7 @@ class LazyActionErrorSnapshot( } } + // TODO return system bridge errors override fun getErrors(actions: List): Map { // Fixes #797 and #1719 // Store which input method would be selected if the actions run successfully. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt index a4225691e0..41fa70eed9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt @@ -140,7 +140,7 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( override val detectScreenOffTriggers: Flow = combine( allKeyMapList, - suAdapter.isRooted, + suAdapter.isRootGranted, ) { keyMapList, isRootPermissionGranted -> keyMapList.any { it.keyMap.trigger.screenOffTrigger } && isRootPermissionGranted }.flowOn(Dispatchers.Default) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 381110bff4..ca7d19ded8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -23,6 +23,7 @@ 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.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon @@ -32,6 +33,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.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -49,6 +51,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.common.utils.State @Composable fun ProModeScreen( @@ -56,12 +59,15 @@ fun ProModeScreen( viewModel: ProModeViewModel, onNavigateBack: () -> Unit, ) { - val proModeWarningState by viewModel.proModeWarningState.collectAsStateWithLifecycle() + val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() + val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle() ProModeScreen(modifier = modifier, onBackClick = onNavigateBack) { Content( - proModeWarningState = proModeWarningState, + warningState = proModeWarningState, + setupState = proModeSetupState, onWarningButtonClick = viewModel::onWarningButtonClick, + onStopServiceClick = viewModel::onStopServiceClick, ) } } @@ -111,62 +117,38 @@ private fun ProModeScreen( @Composable private fun Content( modifier: Modifier = Modifier, - proModeWarningState: ProModeWarningState, + warningState: ProModeWarningState, + setupState: State, onWarningButtonClick: () -> Unit = {}, + onShizukuButtonClick: () -> Unit = {}, + onStopServiceClick: () -> Unit = {}, ) { Column(modifier = modifier.verticalScroll(rememberScrollState())) { WarningCard( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), - state = proModeWarningState, + state = warningState, 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)) + Spacer(modifier = Modifier.height(16.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, + if (warningState is ProModeWarningState.Understood) { + when (setupState) { + is State.Loading -> { + CircularProgressIndicator() + } + + is State.Data -> { + SetupSection( + modifier = Modifier.fillMaxWidth(), + state = setupState.data, + onShizukuButtonClick = onShizukuButtonClick, + onStopServiceClick = onStopServiceClick ) - }, - buttonText = stringResource(R.string.pro_mode_shizuku_detected_button), - ) + } + } } else { Text( modifier = Modifier.padding(horizontal = 32.dp), @@ -175,7 +157,87 @@ private fun Content( ) } + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun SetupSection( + modifier: Modifier, + state: ProModeSetupState, + onShizukuButtonClick: () -> Unit, + onStopServiceClick: () -> Unit +) { + Column(modifier) { + 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)) + + when (state) { + ProModeSetupState.Started -> ProModeStartedCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + onStopClick = onStopServiceClick + ) + + is ProModeSetupState.Stopped -> { + if (state.isRootDetected) { + 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 = if (state.isRootGranted) { + stringResource(R.string.pro_mode_root_detected_button_start_service) + } else { + stringResource(R.string.pro_mode_root_detected_button_request_permission) + }, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + + val shizukuButtonText: String? = when (state.shizukuSetupState) { + ShizukuSetupState.INSTALLED -> stringResource(R.string.pro_mode_shizuku_detected_button_start) + ShizukuSetupState.STARTED -> stringResource(R.string.pro_mode_shizuku_detected_button_request_permission) + ShizukuSetupState.PERMISSION_GRANTED -> stringResource(R.string.pro_mode_shizuku_detected_button_start_service) + ShizukuSetupState.NOT_FOUND -> null + } + + if (shizukuButtonText != null) { + 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 = shizukuButtonText, + onButtonClick = onShizukuButtonClick + ) + } + } + } } } @@ -249,6 +311,44 @@ private fun WarningCard( } } +@Composable +private fun ProModeStartedCard( + modifier: Modifier = Modifier, + onStopClick: () -> Unit = {}, +) { + OutlinedCard(modifier) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(16.dp)) + + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = null, + tint = LocalCustomColorsPalette.current.green + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.pro_mode_service_started) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + TextButton( + onClick = onStopClick, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) + ) { + Text(stringResource(R.string.pro_mode_stop_service_button)) + } + + Spacer(modifier = Modifier.width(16.dp)) + } + } +} + @Composable private fun SetupCard( modifier: Modifier = Modifier, @@ -311,7 +411,15 @@ private fun Preview() { KeyMapperTheme { ProModeScreen { Content( - proModeWarningState = ProModeWarningState.Understood, + warningState = ProModeWarningState.Understood, + setupState = State.Data( + ProModeSetupState.Stopped( + isRootDetected = true, + isRootGranted = false, + shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED, + setupProgress = 0.5f + ) + ) ) } } @@ -323,7 +431,8 @@ private fun PreviewDark() { KeyMapperTheme(darkTheme = true) { ProModeScreen { Content( - proModeWarningState = ProModeWarningState.Understood, + warningState = ProModeWarningState.Understood, + setupState = State.Data(ProModeSetupState.Started) ) } } @@ -335,9 +444,10 @@ private fun PreviewCountingDown() { KeyMapperTheme { ProModeScreen { Content( - proModeWarningState = ProModeWarningState.CountingDown( + warningState = ProModeWarningState.CountingDown( seconds = 5, ), + setupState = State.Loading ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupUseCase.kt deleted file mode 100644 index d5e0368f0e..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.sds100.keymapper.base.promode - -import dagger.hilt.android.scopes.ViewModelScoped -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 -import javax.inject.Inject - -@ViewModelScoped -class ProModeSetupUseCaseImpl @Inject constructor( - 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/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index 236f4623f5..6ba85e6e4c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -6,10 +6,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.utils.State import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf @@ -18,7 +21,7 @@ import javax.inject.Inject @HiltViewModel class ProModeViewModel @Inject constructor( - private val useCase: ProModeSetupUseCase, + private val useCase: SystemBridgeSetupUseCase, resourceProvider: ResourceProvider, dialogProvider: DialogProvider, navigationProvider: NavigationProvider, @@ -32,31 +35,69 @@ class ProModeViewModel @Inject constructor( } @OptIn(ExperimentalCoroutinesApi::class) - val proModeWarningState: StateFlow = - useCase.isWarningUnderstood.flatMapLatest { isUnderstood -> - if (isUnderstood) { - flowOf(ProModeWarningState.Understood) - } else { - flow { - repeat(WARNING_COUNT_DOWN_SECONDS) { - emit(ProModeWarningState.CountingDown(WARNING_COUNT_DOWN_SECONDS - it)) - delay(1000L) - } + val warningState: StateFlow = + useCase.isWarningUnderstood + .flatMapLatest { isUnderstood -> createWarningStateFlow(isUnderstood) } + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + ProModeWarningState.CountingDown( + WARNING_COUNT_DOWN_SECONDS, + ), + ) - emit(ProModeWarningState.Idle) + val setupState: StateFlow> = + combine( + useCase.isSystemBridgeConnected, + useCase.setupProgress, + useCase.isRootDetected, + useCase.isRootGranted, + useCase.shizukuSetupState, + ::buildSetupState + ).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + + private fun createWarningStateFlow(isUnderstood: Boolean): Flow = + if (isUnderstood) { + flowOf(ProModeWarningState.Understood) + } else { + flow { + repeat(WARNING_COUNT_DOWN_SECONDS) { + emit(ProModeWarningState.CountingDown(WARNING_COUNT_DOWN_SECONDS - it)) + delay(1000L) } + + emit(ProModeWarningState.Idle) } - }.stateIn( - viewModelScope, - SharingStarted.Eagerly, - ProModeWarningState.CountingDown( - WARNING_COUNT_DOWN_SECONDS, - ), - ) + } fun onWarningButtonClick() { useCase.onUnderstoodWarning() } + + fun onStopServiceClick() { + useCase.stopSystemBridge() + } + + private fun buildSetupState( + isSystemBridgeConnected: Boolean, + setupProgress: Float, + isRootDetected: Boolean, + isRootGranted: Boolean, + shizukuSetupState: ShizukuSetupState + ): State { + if (isSystemBridgeConnected) { + return State.Data(ProModeSetupState.Started) + } else { + return State.Data( + ProModeSetupState.Stopped( + isRootDetected = isRootDetected, + isRootGranted = isRootGranted, + shizukuSetupState = shizukuSetupState, + setupProgress = setupProgress + ) + ) + } + } } sealed class ProModeWarningState { @@ -65,7 +106,13 @@ sealed class ProModeWarningState { data object Understood : ProModeWarningState() } -data class ProModeSetupState( - val isRootDetected: Boolean, - val isShizukuDetected: Boolean, -) +sealed class ProModeSetupState { + data class Stopped( + val isRootDetected: Boolean, + val isRootGranted: Boolean, + val shizukuSetupState: ShizukuSetupState, + val setupProgress: Float + ) : ProModeSetupState() + + data object Started : ProModeSetupState() +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt new file mode 100644 index 0000000000..69077c5102 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt @@ -0,0 +1,8 @@ +package io.github.sds100.keymapper.base.promode + +enum class ShizukuSetupState { + NOT_FOUND, + INSTALLED, + STARTED, + PERMISSION_GRANTED +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt new file mode 100644 index 0000000000..bb8a7b82ca --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -0,0 +1,114 @@ +package io.github.sds100.keymapper.base.promode + +import android.os.Build +import androidx.annotation.RequiresApi +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep +import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter +import io.github.sds100.keymapper.system.permissions.Permission +import io.github.sds100.keymapper.system.permissions.PermissionAdapter +import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import javax.inject.Inject + + +@RequiresApi(Build.VERSION_CODES.Q) +@ViewModelScoped +class SystemBridgeSetupUseCaseImpl @Inject constructor( + private val preferences: PreferenceRepository, + private val suAdapter: SuAdapter, + private val systemBridgeSetupController: SystemBridgeSetupController, + private val systemBridgeManager: SystemBridgeManager, + private val shizukuAdapter: ShizukuAdapter, + private val permissionAdapter: PermissionAdapter, + private val accessibilityServiceAdapter: AccessibilityServiceAdapter +) : SystemBridgeSetupUseCase { + override val isWarningUnderstood: Flow = + preferences.get(Keys.isProModeWarningUnderstood).map { it ?: false } + + override fun onUnderstoodWarning() { + preferences.set(Keys.isProModeWarningUnderstood, true) + } + + override val isSystemBridgeConnected: Flow = systemBridgeManager.isConnected + + override val nextSetupStep: Flow = + systemBridgeSetupController.nextSetupStep + + override val setupProgress: Flow = nextSetupStep.map { step -> + step.stepIndex.toFloat() / SystemBridgeSetupStep.entries.size + } + + override val isRootDetected: Flow = suAdapter.isRootDetected + override val isRootGranted: Flow = suAdapter.isRootGranted + + override val shizukuSetupState: Flow = combine( + shizukuAdapter.isInstalled, + shizukuAdapter.isInstalled, + permissionAdapter.isGrantedFlow(Permission.SHIZUKU) + ) { isInstalled, isStarted, isPermissionGranted -> + when { + isPermissionGranted -> ShizukuSetupState.PERMISSION_GRANTED + isStarted -> ShizukuSetupState.STARTED + isInstalled -> ShizukuSetupState.INSTALLED + else -> ShizukuSetupState.NOT_FOUND + } + } + + + override fun stopSystemBridge() { + systemBridgeManager.stopSystemBridge() + } + + override fun enableAccessibilityService() { + accessibilityServiceAdapter.start() + } + + override fun openDeveloperOptions() { + TODO("Not yet implemented") + } + + override fun connectWifiNetwork() { + TODO("Not yet implemented") + } + + override fun enableWirelessDebugging() { + TODO("Not yet implemented") + } + + override fun pairAdb() { + TODO("Not yet implemented") + } + + override fun startSystemBridge() { + TODO("Not yet implemented") + } +} + +interface SystemBridgeSetupUseCase { + val isWarningUnderstood: Flow + fun onUnderstoodWarning() + + val isSystemBridgeConnected: Flow + val nextSetupStep: Flow + val setupProgress: Flow + + val isRootDetected: Flow + val isRootGranted: Flow + val shizukuSetupState: Flow + + fun stopSystemBridge() + fun enableAccessibilityService() + fun openDeveloperOptions() + fun connectWifiNetwork() + fun enableWirelessDebugging() + fun pairAdb() + fun startSystemBridge() +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index fb7842c667..5a810e6361 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -46,7 +46,7 @@ class ConfigSettingsUseCaseImpl @Inject constructor( ) } - override val isRootGranted: Flow = suAdapter.isRooted + override val isRootGranted: Flow = suAdapter.isRootGranted override val isWriteSecureSettingsGranted: Flow = channelFlow { send(permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt index 8178027873..752b54d27e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt @@ -23,7 +23,7 @@ class ManageNotificationsUseCaseImpl @Inject constructor( override val showImePickerNotification: Flow = combine( - suAdapter.isRooted, + suAdapter.isRootGranted, preferences.get(Keys.showImePickerNotification), ) { hasRootPermission, show -> when { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt new file mode 100644 index 0000000000..cd00c50e4e --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt @@ -0,0 +1,162 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.ProModeIconDisabled: ImageVector + get() { + if (_ProModeDisabled != null) { + return _ProModeDisabled!! + } + _ProModeDisabled = ImageVector.Builder( + name = "ProModeDisabled", + defaultWidth = 32.dp, + defaultHeight = 32.dp, + viewportWidth = 32f, + viewportHeight = 32f + ).apply { + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, -0f) + lineTo(32f, -0f) + lineTo(32f, 32f) + lineTo(-0f, 32f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + } + ) { + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(4f, 11f) + verticalLineToRelative(10f) + horizontalLineToRelative(2f) + verticalLineToRelative(-4f) + horizontalLineToRelative(2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, -2f) + verticalLineTo(13f) + arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 8f, 11f) + horizontalLineTo(4f) + moveToRelative(2f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineTo(6f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(13f, 11f) + verticalLineToRelative(10f) + horizontalLineToRelative(2f) + verticalLineToRelative(-4f) + horizontalLineToRelative(0.8f) + lineToRelative(1.2f, 4f) + horizontalLineToRelative(2f) + lineTo(17.76f, 16.85f) + curveTo(18.5f, 16.55f, 19f, 15.84f, 19f, 15f) + verticalLineToRelative(-2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, -2f) + horizontalLineToRelative(-4f) + moveToRelative(2f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineToRelative(-2f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(24f, 11f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, 2f) + verticalLineToRelative(6f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, 2f) + horizontalLineToRelative(2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, -2f) + verticalLineToRelative(-6f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, -2f) + horizontalLineToRelative(-2f) + moveToRelative(0f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(6f) + horizontalLineToRelative(-2f) + close() + } + path( + fill = SolidColor(Color(0xFF808080)), + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + pathFillType = PathFillType.EvenOdd + ) { + moveTo(26.664f, 5.353f) + lineTo(5.354f, 26.753f) + } + }.build() + + return _ProModeDisabled!! + } + +@Suppress("ObjectPropertyName") +private var _ProModeDisabled: ImageVector? = null diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 260869bfa6..683054affd 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1593,15 +1593,20 @@ 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 + Request permission + Start PRO mode Shizuku detected You can skip the set up process by giving Key Mapper Shizuku permission. - Use Shizuku + Start Shizuku + Request permission + Start PRO mode 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. + PRO mode service is running + Stop diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 06df4223e3..7e4314ada8 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -4,8 +4,13 @@ import android.annotation.SuppressLint import android.os.Build import android.os.IBinder import android.os.IBinder.DeathRecipient +import android.os.RemoteException import androidx.annotation.RequiresApi import io.github.sds100.keymapper.sysbridge.ISystemBridge +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import javax.inject.Inject import javax.inject.Singleton @@ -16,14 +21,16 @@ import javax.inject.Singleton class SystemBridgeManagerImpl @Inject constructor() : SystemBridgeManager { private val systemBridgeLock: Any = Any() - private var systemBridge: ISystemBridge? = null + private var systemBridge: MutableStateFlow = MutableStateFlow(null) + + override val isConnected: Flow = systemBridge.map { it != null } private val connectionsLock: Any = Any() private val connections: MutableSet = mutableSetOf() private val deathRecipient: DeathRecipient = DeathRecipient { synchronized(systemBridgeLock) { - systemBridge = null + systemBridge.update { null } } synchronized(connectionsLock) { @@ -35,7 +42,7 @@ class SystemBridgeManagerImpl @Inject constructor() : SystemBridgeManager { fun pingBinder(): Boolean { synchronized(systemBridgeLock) { - return systemBridge?.asBinder()?.pingBinder() == true + return systemBridge.value?.asBinder()?.pingBinder() == true } } @@ -47,7 +54,7 @@ class SystemBridgeManagerImpl @Inject constructor() : SystemBridgeManager { synchronized(systemBridgeLock) { systemBridge.asBinder().linkToDeath(deathRecipient, 0) - this.systemBridge = systemBridge + this.systemBridge.update { systemBridge } } synchronized(connectionsLock) { @@ -82,13 +89,21 @@ class SystemBridgeManagerImpl @Inject constructor() : SystemBridgeManager { } override fun stopSystemBridge() { - TODO("Not yet implemented") + synchronized(systemBridgeLock) { + try { + systemBridge.value?.destroy() + } catch (_: RemoteException) { + deathRecipient.binderDied() + } + } } } @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeManager { + val isConnected: Flow + fun registerConnection(connection: SystemBridgeConnection) fun unregisterConnection(connection: SystemBridgeConnection) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 74c19246bc..4392658316 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -17,7 +17,9 @@ import io.github.sds100.keymapper.sysbridge.adb.PreferenceAdbKeyStore import io.github.sds100.keymapper.sysbridge.starter.Starter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import timber.log.Timber @@ -39,6 +41,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) private val adbConnectMdns: AdbMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) + override val nextSetupStep: Flow = + flowOf(SystemBridgeSetupStep.ACCESSIBILITY_SERVICE) + init { // TODO remove if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -203,6 +208,8 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeSetupController { + val nextSetupStep: Flow + @RequiresApi(Build.VERSION_CODES.R) fun pairWirelessAdb(port: Int, code: Int) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt new file mode 100644 index 0000000000..e8e3488bbd --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt @@ -0,0 +1,11 @@ +package io.github.sds100.keymapper.sysbridge.service + +enum class SystemBridgeSetupStep(val stepIndex: Int) { + ACCESSIBILITY_SERVICE(stepIndex = 0), + DEVELOPER_OPTIONS(stepIndex = 1), + WIFI_NETWORK(stepIndex = 2), + WIRELESS_DEBUGGING(stepIndex = 3), + ADB_PAIRING(stepIndex = 4), + ADB_CONNECT(stepIndex = 5), + START_SERVICE(stepIndex = 6) +} \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt index fa8d8eb785..d84c3e27e3 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt @@ -103,7 +103,7 @@ class AndroidPermissionAdapter @Inject constructor( .stateIn(coroutineScope, SharingStarted.Eagerly, false) init { - suAdapter.isRooted + suAdapter.isRootGranted .drop(1) .onEach { onPermissionsChanged() } .launchIn(coroutineScope) @@ -281,7 +281,7 @@ class AndroidPermissionAdapter @Inject constructor( Manifest.permission.CALL_PHONE, ) == PERMISSION_GRANTED - Permission.ROOT -> suAdapter.isRooted.value + Permission.ROOT -> suAdapter.isRootGranted.value Permission.IGNORE_BATTERY_OPTIMISATION -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt index 6db7203434..f552ff3c8d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt @@ -12,8 +12,6 @@ enum class Permission { CALL_PHONE, ROOT, IGNORE_BATTERY_OPTIMISATION, - - // TODO remove. Replace with System Bridge. SHIZUKU, ACCESS_FINE_LOCATION, ANSWER_PHONE_CALL, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt index eef9730338..09ef470771 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt @@ -7,7 +7,6 @@ import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -16,26 +15,22 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class SuAdapterImpl @Inject constructor( - coroutineScope: CoroutineScope, -) : SuAdapter { - private var process: Process? = null - - override val isRooted: MutableStateFlow = MutableStateFlow(false) +class SuAdapterImpl @Inject constructor() : SuAdapter { + override val isRootGranted: MutableStateFlow = MutableStateFlow(false) + override val isRootDetected: MutableStateFlow = MutableStateFlow(false) init { + Shell.getShell() invalidateIsRooted() } override fun requestPermission(): Boolean { - // show the su prompt - Shell.getShell() - - return isRooted.updateAndGet { Shell.isAppGrantedRoot() ?: false } + Shell.cmd("su").exec() + return isRootGranted.updateAndGet { Shell.isAppGrantedRoot() ?: false } } override fun execute(command: String, block: Boolean): KMResult<*> { - if (!isRooted.firstBlocking()) { + if (!isRootGranted.firstBlocking()) { return SystemError.PermissionDenied(Permission.ROOT) } @@ -53,13 +48,14 @@ class SuAdapterImpl @Inject constructor( } fun invalidateIsRooted() { - Shell.getShell() - isRooted.update { Shell.isAppGrantedRoot() ?: false } + isRootDetected.update { Shell.cmd("su").exec().isSuccess } + isRootGranted.update { Shell.isAppGrantedRoot() ?: false } } } interface SuAdapter { - val isRooted: StateFlow + val isRootGranted: StateFlow + val isRootDetected: StateFlow /** * @return whether root permission was granted successfully From d48b7901e58de824961e421da1b70c026b77addf Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 11 Aug 2025 02:49:31 +0100 Subject: [PATCH 115/215] #1394 update pro mode warning text --- base/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 683054affd..7c6f3d2cbd 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1586,7 +1586,7 @@ 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. + 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 and volume buttons in the correct combination for a long time. %d… I understand Understood From 9006051e7a58fd16f708a1e4cd3d68eda3eb3b47 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 11 Aug 2025 14:27:22 +0100 Subject: [PATCH 116/215] feat: just say "short, long, double" for click types on compact screens --- .../base/trigger/BaseTriggerScreen.kt | 35 +++++++++++++------ base/src/main/res/values/strings.xml | 3 ++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 70a66f2732..7ca52b2538 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -244,7 +244,7 @@ private fun TriggerScreenVertical( ) if (configState.clickTypeButtons.isNotEmpty()) { - ClickTypeRadioGroup( + ClickTypeSegmentedButtons( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), @@ -260,7 +260,7 @@ private fun TriggerScreenVertical( } if (configState.triggerModeButtonsVisible) { - TriggerModeRadioGroup( + TriggerModeSegmentedButtons( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), @@ -394,7 +394,7 @@ private fun TriggerScreenHorizontal( ) { Spacer(modifier = Modifier.height(16.dp)) if (configState.clickTypeButtons.isNotEmpty()) { - ClickTypeRadioGroup( + ClickTypeSegmentedButtons( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), @@ -408,7 +408,7 @@ private fun TriggerScreenHorizontal( Spacer(modifier = Modifier.height(8.dp)) if (configState.triggerModeButtonsVisible) { - TriggerModeRadioGroup( + TriggerModeSegmentedButtons( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), @@ -519,7 +519,7 @@ private fun TriggerList( } @Composable -private fun ClickTypeRadioGroup( +private fun ClickTypeSegmentedButtons( modifier: Modifier = Modifier, clickTypes: Set, checkedClickType: ClickType?, @@ -529,15 +529,30 @@ private fun ClickTypeRadioGroup( // Always put the buttons in the same order val clickTypeButtonContent: List> = buildList { if (clickTypes.contains(ClickType.SHORT_PRESS)) { - add(ClickType.SHORT_PRESS to stringResource(R.string.radio_button_short_press)) + val text = if (isCompact) { + stringResource(R.string.radio_button_short) + } else { + stringResource(R.string.radio_button_short_press) + } + add(ClickType.SHORT_PRESS to text) } if (clickTypes.contains(ClickType.LONG_PRESS)) { - add(ClickType.LONG_PRESS to stringResource(R.string.radio_button_long_press)) + val text = if (isCompact) { + stringResource(R.string.radio_button_long) + } else { + stringResource(R.string.radio_button_long_press) + } + add(ClickType.LONG_PRESS to text) } if (clickTypes.contains(ClickType.DOUBLE_PRESS)) { - add(ClickType.DOUBLE_PRESS to stringResource(R.string.radio_button_double_press)) + val text = if (isCompact) { + stringResource(R.string.radio_button_double) + } else { + stringResource(R.string.radio_button_double_press) + } + add(ClickType.DOUBLE_PRESS to text) } } @@ -551,7 +566,7 @@ private fun ClickTypeRadioGroup( } @Composable -private fun TriggerModeRadioGroup( +private fun TriggerModeSegmentedButtons( modifier: Modifier = Modifier, mode: TriggerMode, isEnabled: Boolean, @@ -642,7 +657,7 @@ private fun VerticalPreview() { } } -@Preview(heightDp = 400, widthDp = 300) +@Preview(heightDp = 300, widthDp = 300) @Composable private fun VerticalPreviewTiny() { KeyMapperTheme { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 7c6f3d2cbd..5215991dcb 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -162,6 +162,9 @@ Short press Long press Double press + Short + Long + Double True False Activity From 094681796469bd38e906b8c33b61608a7356e579 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 11 Aug 2025 23:22:46 +0100 Subject: [PATCH 117/215] #1394 add pro mode card to set up with key mapper --- .../keymapper/base/promode/ProModeScreen.kt | 75 ++++- .../base/promode/SystemBridgeSetupUseCase.kt | 6 + .../utils/ui/compose/icons/FakeShizuku.kt | 45 +++ .../utils/ui/compose/icons/KeyMapperIcon.kt | 313 ++++++++++++++++++ base/src/main/res/values/strings.xml | 1 + .../service/SystemBridgeSetupController.kt | 16 +- 6 files changed, 439 insertions(+), 17 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index ca7d19ded8..8ba2edfeab 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -1,6 +1,8 @@ package io.github.sds100.keymapper.base.promode +import android.os.Build import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,12 +14,12 @@ 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.size 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 @@ -28,6 +30,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold @@ -40,7 +43,6 @@ 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 @@ -51,6 +53,9 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.icons.FakeShizuku +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcon +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons import io.github.sds100.keymapper.common.utils.State @Composable @@ -192,7 +197,13 @@ private fun SetupSection( .fillMaxWidth() .padding(horizontal = 8.dp), color = LocalCustomColorsPalette.current.magiskTeal, - icon = Icons.Rounded.Numbers, + icon = { + Icon( + imageVector = Icons.Rounded.Numbers, + contentDescription = null, + tint = LocalCustomColorsPalette.current.magiskTeal + ) + }, title = stringResource(R.string.pro_mode_root_detected_title), content = { Text( @@ -206,10 +217,9 @@ private fun SetupSection( stringResource(R.string.pro_mode_root_detected_button_request_permission) }, ) - } - - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) + } val shizukuButtonText: String? = when (state.shizukuSetupState) { ShizukuSetupState.INSTALLED -> stringResource(R.string.pro_mode_shizuku_detected_button_start) @@ -224,7 +234,12 @@ private fun SetupSection( .fillMaxWidth() .padding(horizontal = 8.dp), color = LocalCustomColorsPalette.current.shizukuBlue, - icon = Icons.Rounded.Android, + icon = { + Image( + imageVector = KeyMapperIcons.FakeShizuku, + contentDescription = null, + ) + }, title = stringResource(R.string.pro_mode_shizuku_detected_title), content = { Text( @@ -236,6 +251,40 @@ private fun SetupSection( onButtonClick = onShizukuButtonClick ) } + + Spacer(modifier = Modifier.height(8.dp)) + + val setupKeyMapperText: String = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> stringResource(R.string.pro_mode_set_up_with_key_mapper_button_incompatible) + else -> stringResource(R.string.pro_mode_set_up_with_key_mapper_button) + } + + SetupCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + color = MaterialTheme.colorScheme.primaryContainer, + icon = { + Image( + modifier = Modifier.padding(2.dp), + imageVector = KeyMapperIcons.KeyMapperIcon, + contentDescription = null, + ) + }, + title = stringResource(R.string.pro_mode_set_up_with_key_mapper_title), + content = { + if (state.setupProgress < 1) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + progress = { state.setupProgress }) + } + }, + buttonText = setupKeyMapperText, + onButtonClick = onShizukuButtonClick, + enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + ) } } } @@ -353,20 +402,19 @@ private fun ProModeStartedCard( private fun SetupCard( modifier: Modifier = Modifier, color: Color, - icon: ImageVector, + icon: @Composable () -> Unit, title: String, content: @Composable () -> Unit, buttonText: String, onButtonClick: () -> Unit = {}, + enabled: Boolean = true ) { OutlinedCard(modifier = modifier) { Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.padding(horizontal = 16.dp)) { - Icon( - imageVector = icon, - contentDescription = null, - tint = color, - ) + Box(Modifier.size(24.dp)) { + icon() + } Spacer(modifier = Modifier.width(8.dp)) @@ -393,6 +441,7 @@ private fun SetupCard( .align(Alignment.End) .padding(horizontal = 16.dp), onClick = onButtonClick, + enabled = enabled, colors = ButtonDefaults.filledTonalButtonColors( containerColor = color, contentColor = LocalCustomColorsPalette.current.contentColorFor(color), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index bb8a7b82ca..e792cadfa3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -63,6 +63,10 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } + override fun requestShizukuPermission() { + permissionAdapter.request(Permission.SHIZUKU) + } + override fun stopSystemBridge() { systemBridgeManager.stopSystemBridge() } @@ -102,7 +106,9 @@ interface SystemBridgeSetupUseCase { val isRootDetected: Flow val isRootGranted: Flow + val shizukuSetupState: Flow + fun requestShizukuPermission() fun stopSystemBridge() fun enableAccessibilityService() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt new file mode 100644 index 0000000000..2c728437fc --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt @@ -0,0 +1,45 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.FakeShizuku: ImageVector + get() { + if (_FakeShizuku != null) { + return _FakeShizuku!! + } + _FakeShizuku = ImageVector.Builder( + name = "FakeShizuku", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path(fill = SolidColor(Color(0xFF4053B9))) { + moveTo(12f, 24f) + curveTo(18.627f, 24f, 24f, 18.627f, 24f, 12f) + curveTo(24f, 5.373f, 18.627f, 0f, 12f, 0f) + curveTo(5.373f, 0f, 0f, 5.373f, 0f, 12f) + curveTo(0f, 18.627f, 5.373f, 24f, 12f, 24f) + close() + } + path(fill = SolidColor(Color(0xFF717DC0))) { + moveTo(15.384f, 17.771f) + lineTo(8.74f, 17.752f) + lineTo(5.434f, 11.989f) + lineTo(8.772f, 6.244f) + lineTo(15.416f, 6.263f) + lineTo(18.722f, 12.026f) + lineTo(15.384f, 17.771f) + close() + } + }.build() + + return _FakeShizuku!! + } + +@Suppress("ObjectPropertyName") +private var _FakeShizuku: ImageVector? = null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt new file mode 100644 index 0000000000..8fe3042070 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt @@ -0,0 +1,313 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.KeyMapperIcon: ImageVector + get() { + if (_KeyMapperIcon != null) { + return _KeyMapperIcon!! + } + _KeyMapperIcon = ImageVector.Builder( + name = "KeyMapperIcon", + defaultWidth = 61.637.dp, + defaultHeight = 61.637.dp, + viewportWidth = 61.637f, + viewportHeight = 61.637f + ).apply { + group( + clipPathData = PathData { + moveTo(-42f, -40.363f) + lineTo(102f, -40.363f) + lineTo(102f, 103.637f) + lineTo(-42f, 103.637f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-42f, -40.363f) + lineTo(102f, -40.363f) + lineTo(102f, 103.637f) + lineTo(-42f, 103.637f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-42.094f, -40.457f) + lineTo(102.095f, -40.457f) + lineTo(102.095f, 103.731f) + lineTo(-42.094f, 103.731f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-42.047f, -40.409f) + lineTo(102.047f, -40.409f) + lineTo(102.047f, 103.684f) + lineTo(-42.047f, 103.684f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-42.047f, -40.409f) + lineTo(102.047f, -40.409f) + lineTo(102.047f, 103.684f) + lineTo(-42.047f, 103.684f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-4.667f, -3.029f) + lineTo(64.667f, -3.029f) + lineTo(64.667f, 66.304f) + lineTo(-4.667f, 66.304f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(6f, 7.637f) + lineTo(54f, 7.637f) + lineTo(54f, 55.637f) + lineTo(6f, 55.637f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-4.8f, 7.504f) + lineTo(64.8f, 7.504f) + lineTo(64.8f, 55.771f) + lineTo(-4.8f, 55.771f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(5.867f, -3.163f) + lineTo(54.133f, -3.163f) + lineTo(54.133f, 66.437f) + lineTo(5.867f, 66.437f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(0.533f, 2.171f) + lineTo(59.467f, 2.171f) + lineTo(59.467f, 61.104f) + lineTo(0.533f, 61.104f) + close() + } + ) { + } + group( + clipPathData = PathData { + moveTo(-18f, -16.363f) + lineTo(78f, -16.363f) + lineTo(78f, 79.637f) + lineTo(-18f, 79.637f) + close() + } + ) { + } + path( + fill = SolidColor(Color(0xFFD32F2F)), + strokeLineWidth = 1.27586f + ) { + moveTo(4f, 24.637f) + lineTo(33f, 24.637f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 37f, 28.637f) + lineTo(37f, 57.637f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 33f, 61.637f) + lineTo(4f, 61.637f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0f, 57.637f) + lineTo(0f, 28.637f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 4f, 24.637f) + close() + } + path( + fill = SolidColor(Color.White), + strokeLineWidth = 1.3f + ) { + moveTo(21.9f, 37.514f) + lineTo(21.9f, 31.937f) + curveTo(21.9f, 31.222f, 21.315f, 30.637f, 20.6f, 30.637f) + horizontalLineToRelative(-5.2f) + curveToRelative(-0.715f, 0f, -1.3f, 0.585f, -1.3f, 1.3f) + verticalLineToRelative(5.577f) + curveToRelative(0f, 0.169f, 0.065f, 0.338f, 0.195f, 0.455f) + lineToRelative(3.25f, 3.25f) + curveToRelative(0.26f, 0.26f, 0.663f, 0.26f, 0.923f, 0f) + lineToRelative(3.25f, -3.25f) + curveToRelative(0.117f, -0.117f, 0.182f, -0.273f, 0.182f, -0.455f) + close() + moveTo(11.877f, 39.737f) + lineTo(6.3f, 39.737f) + curveTo(5.585f, 39.737f, 5f, 40.322f, 5f, 41.037f) + verticalLineToRelative(5.2f) + curveToRelative(0f, 0.715f, 0.585f, 1.3f, 1.3f, 1.3f) + lineTo(11.877f, 47.537f) + curveToRelative(0.169f, 0f, 0.338f, -0.065f, 0.455f, -0.195f) + lineToRelative(3.25f, -3.25f) + curveToRelative(0.26f, -0.26f, 0.26f, -0.663f, 0f, -0.923f) + lineToRelative(-3.25f, -3.25f) + curveToRelative(-0.117f, -0.117f, -0.273f, -0.182f, -0.455f, -0.182f) + close() + moveTo(14.1f, 49.76f) + verticalLineToRelative(5.577f) + curveTo(14.1f, 56.052f, 14.685f, 56.637f, 15.4f, 56.637f) + horizontalLineToRelative(5.2f) + curveToRelative(0.715f, 0f, 1.3f, -0.585f, 1.3f, -1.3f) + lineTo(21.9f, 49.76f) + curveToRelative(0f, -0.169f, -0.065f, -0.338f, -0.195f, -0.455f) + lineToRelative(-3.25f, -3.25f) + curveToRelative(-0.26f, -0.26f, -0.663f, -0.26f, -0.923f, 0f) + lineToRelative(-3.25f, 3.25f) + curveToRelative(-0.117f, 0.117f, -0.182f, 0.273f, -0.182f, 0.455f) + close() + moveTo(23.655f, 39.932f) + lineTo(20.405f, 43.182f) + curveToRelative(-0.26f, 0.26f, -0.26f, 0.663f, 0f, 0.923f) + lineToRelative(3.25f, 3.25f) + curveToRelative(0.117f, 0.117f, 0.286f, 0.195f, 0.455f, 0.195f) + horizontalLineToRelative(5.59f) + curveTo(30.415f, 47.55f, 31f, 46.965f, 31f, 46.25f) + lineTo(31f, 41.05f) + curveTo(31f, 40.335f, 30.415f, 39.75f, 29.7f, 39.75f) + lineTo(24.123f, 39.75f) + curveToRelative(-0.182f, -0.013f, -0.338f, 0.052f, -0.468f, 0.182f) + close() + } + path( + fill = SolidColor(Color(0xFF212121)), + fillAlpha = 0f, + strokeLineWidth = 1.27586f + ) { + moveTo(22.717f, 4.354f) + curveTo(21.679f, 5.075f, 21f, 6.272f, 21f, 7.637f) + verticalLineToRelative(29f) + curveToRelative(0f, 2.216f, 1.784f, 4f, 4f, 4f) + horizontalLineToRelative(29f) + curveToRelative(1.365f, 0f, 2.562f, -0.679f, 3.283f, -1.717f) + curveTo(56.636f, 39.37f, 55.851f, 39.637f, 55f, 39.637f) + lineTo(26f, 39.637f) + curveToRelative(-2.216f, 0f, -4f, -1.784f, -4f, -4f) + lineTo(22f, 6.637f) + curveToRelative(0f, -0.851f, 0.267f, -1.636f, 0.717f, -2.283f) + close() + } + path( + fill = SolidColor(Color(0xFF1565C0)), + strokeLineWidth = 1.27586f + ) { + moveTo(55f, 2.862f) + lineTo(26f, 2.862f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = false, 22f, 6.862f) + lineTo(22f, 35.862f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = false, 26f, 39.862f) + lineTo(55f, 39.862f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = false, 59f, 35.862f) + lineTo(59f, 6.862f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = false, 55f, 2.862f) + close() + } + path( + fill = SolidColor(Color.White), + strokeLineWidth = 1.3f + ) { + moveTo(51.4f, 11.437f) + lineTo(30.6f, 11.437f) + curveToRelative(-1.43f, 0f, -2.587f, 1.17f, -2.587f, 2.6f) + lineTo(28f, 27.037f) + curveToRelative(0f, 1.43f, 1.17f, 2.6f, 2.6f, 2.6f) + lineTo(51.4f, 29.637f) + curveTo(52.83f, 29.637f, 54f, 28.467f, 54f, 27.037f) + lineTo(54f, 14.037f) + curveToRelative(0f, -1.43f, -1.17f, -2.6f, -2.6f, -2.6f) + close() + moveTo(39.7f, 15.337f) + horizontalLineToRelative(2.6f) + verticalLineToRelative(2.6f) + lineTo(39.7f, 17.937f) + close() + moveTo(39.7f, 19.237f) + horizontalLineToRelative(2.6f) + lineTo(42.3f, 21.837f) + lineTo(39.7f, 21.837f) + close() + moveTo(35.8f, 15.337f) + horizontalLineToRelative(2.6f) + verticalLineToRelative(2.6f) + lineTo(35.8f, 17.937f) + close() + moveTo(35.8f, 19.237f) + horizontalLineToRelative(2.6f) + lineTo(38.4f, 21.837f) + lineTo(35.8f, 21.837f) + close() + moveTo(34.5f, 21.837f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(34.5f, 17.937f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(44.9f, 27.037f) + horizontalLineToRelative(-7.8f) + curveTo(36.385f, 27.037f, 35.8f, 26.452f, 35.8f, 25.737f) + curveTo(35.8f, 25.022f, 36.385f, 24.437f, 37.1f, 24.437f) + horizontalLineToRelative(7.8f) + curveToRelative(0.715f, 0f, 1.3f, 0.585f, 1.3f, 1.3f) + curveToRelative(0f, 0.715f, -0.585f, 1.3f, -1.3f, 1.3f) + close() + moveTo(46.2f, 21.837f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(46.2f, 17.937f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(50.1f, 21.837f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(50.1f, 17.937f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + } + }.build() + + return _KeyMapperIcon!! + } + +@Suppress("ObjectPropertyName") +private var _KeyMapperIcon: ImageVector? = null diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 5215991dcb..22a32665a8 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1605,6 +1605,7 @@ Start PRO mode Set up with Key Mapper Continue + Continue (Android 11+) Options Enable PRO mode for all key maps Key Mapper will use the ADB Shell for remapping diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 4392658316..e56548be90 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -38,16 +38,19 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private val sb = StringBuilder() - @RequiresApi(Build.VERSION_CODES.R) - private val adbConnectMdns: AdbMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) + private val adbConnectMdns: AdbMdns? override val nextSetupStep: Flow = flowOf(SystemBridgeSetupStep.ACCESSIBILITY_SERVICE) init { - // TODO remove if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + adbConnectMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) + + // TODO remove startWithAdb() + } else { + adbConnectMdns = null } } @@ -55,8 +58,13 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) override fun startWithAdb() { + if (adbConnectMdns == null) { + return + } + coroutineScope.launch(Dispatchers.IO) { - adbConnectMdns.start() + + adbConnectMdns.start() val host = "127.0.0.1" val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } From 7f4a6bc88888070c4243cbc434110c3f7959f51e Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 11 Aug 2025 23:32:03 +0100 Subject: [PATCH 118/215] #1394 setting up Shizuku for pro mode works --- .../keymapper/base/promode/ProModeScreen.kt | 4 +- .../base/promode/ProModeViewModel.kt | 25 +++++++ .../base/promode/SystemBridgeSetupUseCase.kt | 6 +- .../BaseAccessibilityServiceController.kt | 65 +++++++++---------- .../service/SystemBridgeSetupController.kt | 5 +- 5 files changed, 66 insertions(+), 39 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 8ba2edfeab..923e6dd0ea 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -73,6 +73,7 @@ fun ProModeScreen( setupState = proModeSetupState, onWarningButtonClick = viewModel::onWarningButtonClick, onStopServiceClick = viewModel::onStopServiceClick, + onShizukuButtonClick = viewModel::onShizukuButtonClick ) } } @@ -381,7 +382,8 @@ private fun ProModeStartedCard( Text( modifier = Modifier.weight(1f), - text = stringResource(R.string.pro_mode_service_started) + text = stringResource(R.string.pro_mode_service_started), + style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.width(16.dp)) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index 6ba85e6e4c..b66477d333 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -13,10 +13,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -78,6 +80,29 @@ class ProModeViewModel @Inject constructor( useCase.stopSystemBridge() } + fun onShizukuButtonClick() { + viewModelScope.launch { + val shizukuState = useCase.shizukuSetupState.first() + when (shizukuState) { + ShizukuSetupState.NOT_FOUND -> { + // Do nothing + } + + ShizukuSetupState.INSTALLED -> { + useCase.openShizukuApp() + } + + ShizukuSetupState.STARTED -> { + useCase.requestShizukuPermission() + } + + ShizukuSetupState.PERMISSION_GRANTED -> { + useCase.startSystemBridge() + } + } + } + } + private fun buildSetupState( isSystemBridgeConnected: Boolean, setupProgress: Float, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index e792cadfa3..a77ab114e3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -51,7 +51,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( override val shizukuSetupState: Flow = combine( shizukuAdapter.isInstalled, - shizukuAdapter.isInstalled, + shizukuAdapter.isStarted, permissionAdapter.isGrantedFlow(Permission.SHIZUKU) ) { isInstalled, isStarted, isPermissionGranted -> when { @@ -62,6 +62,9 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } } + override fun openShizukuApp() { + shizukuAdapter.openShizukuApp() + } override fun requestShizukuPermission() { permissionAdapter.request(Permission.SHIZUKU) @@ -108,6 +111,7 @@ interface SystemBridgeSetupUseCase { val isRootGranted: Flow val shizukuSetupState: Flow + fun openShizukuApp() fun requestShizukuPermission() fun stopSystemBridge() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 2587e35a22..1102a3c2b6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -13,14 +13,14 @@ import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.actions.PerformActionsUseCaseImpl import io.github.sds100.keymapper.base.actions.TestActionEvent import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCaseImpl +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.base.detection.KeyMapDetectionController +import io.github.sds100.keymapper.base.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent -import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl -import io.github.sds100.keymapper.base.detection.KeyMapDetectionController -import io.github.sds100.keymapper.base.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.common.utils.firstBlocking @@ -35,7 +35,6 @@ import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -408,35 +407,35 @@ abstract class BaseAccessibilityServiceController( } // TODO only run this code, and listen for these events if searching for a pairing code. Check that package name is settings app. - if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { - val pairingCodeRegex = Regex("^\\d{6}$") - val portRegex = Regex(".*:([0-9]{1,5})") - val pairingCodeNode = - service.rootInActiveWindow.findNodeRecursively { - it.text != null && pairingCodeRegex.matches(it.text) - } - - val portNode = service.rootInActiveWindow.findNodeRecursively { - it.text != null && portRegex.matches(it.text) - } - - if (pairingCodeNode != null && portNode != null) { - val pairingCode = pairingCodeNode.text?.toString()?.toIntOrNull() - val port = portNode.text?.split(":")?.last()?.toIntOrNull() - Timber.e("PAIRING CODE = $pairingCode") - Timber.e("PORT = $port") - - if (pairingCode != null && port != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - service.lifecycleScope.launch { - systemBridgeSetupController.pairWirelessAdb(port, pairingCode) - delay(1000) - systemBridgeSetupController.startWithAdb() - } - } - } - } - } +// if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { +// val pairingCodeRegex = Regex("^\\d{6}$") +// val portRegex = Regex(".*:([0-9]{1,5})") +// val pairingCodeNode = +// service.rootInActiveWindow.findNodeRecursively { +// it.text != null && pairingCodeRegex.matches(it.text) +// } +// +// val portNode = service.rootInActiveWindow.findNodeRecursively { +// it.text != null && portRegex.matches(it.text) +// } +// +// if (pairingCodeNode != null && portNode != null) { +// val pairingCode = pairingCodeNode.text?.toString()?.toIntOrNull() +// val port = portNode.text?.split(":")?.last()?.toIntOrNull() +// Timber.e("PAIRING CODE = $pairingCode") +// Timber.e("PORT = $port") +// +// if (pairingCode != null && port != null) { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { +// service.lifecycleScope.launch { +// systemBridgeSetupController.pairWirelessAdb(port, pairingCode) +// delay(1000) +// systemBridgeSetupController.startWithAdb() +// } +// } +// } +// } +// } } fun onFingerprintGesture(type: FingerprintGestureType) { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index e56548be90..b43d37322c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -46,9 +46,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( init { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { adbConnectMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) - - // TODO remove - startWithAdb() } else { adbConnectMdns = null } @@ -64,7 +61,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( coroutineScope.launch(Dispatchers.IO) { - adbConnectMdns.start() + adbConnectMdns.start() val host = "127.0.0.1" val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } From 07f750104fb9fa69d040feed9e737a3630a11949 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 00:33:59 +0100 Subject: [PATCH 119/215] #1394 starting the system bridge with Shizuku works --- .../base/promode/SystemBridgeSetupUseCase.kt | 2 +- sysbridge/build.gradle.kts | 3 + .../sysbridge/IShizukuStarterService.aidl | 8 ++ .../sysbridge/manager/SystemBridgeManager.kt | 22 ++--- .../service/SystemBridgeSetupController.kt | 96 +++++++++++++++++-- .../shizuku/ShizukuStarterService.kt | 42 ++++++++ .../{Starter.kt => SystemBridgeStarter.kt} | 21 +++- 7 files changed, 164 insertions(+), 30 deletions(-) create mode 100644 sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt rename sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/{Starter.kt => SystemBridgeStarter.kt} (87%) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index a77ab114e3..cb2e26517f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -95,7 +95,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } override fun startSystemBridge() { - TODO("Not yet implemented") + systemBridgeSetupController.startService() } } diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index efb40a9de7..79b7c2374a 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -54,6 +54,7 @@ android { buildFeatures { aidl = true prefab = true + buildConfig = true } packaging { @@ -91,6 +92,8 @@ dependencies { implementation(libs.dagger.hilt.android) ksp(libs.dagger.hilt.android.compiler) + implementation(libs.rikka.shizuku.api) + implementation(libs.rikka.shizuku.provider) implementation(libs.rikka.hidden.compat) compileOnly(libs.rikka.hidden.stub) diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl new file mode 100644 index 0000000000..ee6c6b6bd3 --- /dev/null +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl @@ -0,0 +1,8 @@ +package io.github.sds100.keymapper.sysbridge; + +interface IShizukuStarterService { + void destroy() = 16777114; // Destroy method defined by Shizuku server + + // Make it oneway so that an exception isn't thrown when the method kills itself at the end + oneway void startSystemBridge(String scriptPath, String apkPath, String libPath, String packageName) = 1; +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt index 7e4314ada8..1ca9c9ba7c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt @@ -1,11 +1,13 @@ package io.github.sds100.keymapper.sysbridge.manager import android.annotation.SuppressLint +import android.content.Context import android.os.Build import android.os.IBinder import android.os.IBinder.DeathRecipient import android.os.RemoteException import androidx.annotation.RequiresApi +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.sysbridge.ISystemBridge import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -18,7 +20,9 @@ import javax.inject.Singleton * This class handles starting, stopping and (dis)connecting to the system bridge. */ @Singleton -class SystemBridgeManagerImpl @Inject constructor() : SystemBridgeManager { +class SystemBridgeManagerImpl @Inject constructor( + @ApplicationContext private val ctx: Context +) : SystemBridgeManager { private val systemBridgeLock: Any = Any() private var systemBridge: MutableStateFlow = MutableStateFlow(null) @@ -76,18 +80,6 @@ class SystemBridgeManagerImpl @Inject constructor() : SystemBridgeManager { } } - override fun startWithShizuku() { - TODO("Not yet implemented") - } - - override fun startWithAdb() { - TODO("Not yet implemented") - } - - override fun startWithRoot() { - TODO("Not yet implemented") - } - override fun stopSystemBridge() { synchronized(systemBridgeLock) { try { @@ -97,6 +89,7 @@ class SystemBridgeManagerImpl @Inject constructor() : SystemBridgeManager { } } } + } @SuppressLint("ObsoleteSdkInt") @@ -107,8 +100,5 @@ interface SystemBridgeManager { fun registerConnection(connection: SystemBridgeConnection) fun unregisterConnection(connection: SystemBridgeConnection) - fun startWithShizuku() - fun startWithAdb() - fun startWithRoot() fun stopSystemBridge() } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index b43d37322c..c278a695a3 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,12 +1,19 @@ package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint +import android.content.ComponentName import android.content.Context +import android.content.ServiceConnection import android.os.Build +import android.os.IBinder +import android.os.RemoteException import android.preference.PreferenceManager import android.util.Log import androidx.annotation.RequiresApi import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.sysbridge.BuildConfig +import io.github.sds100.keymapper.sysbridge.IShizukuStarterService import io.github.sds100.keymapper.sysbridge.adb.AdbClient import io.github.sds100.keymapper.sysbridge.adb.AdbKey import io.github.sds100.keymapper.sysbridge.adb.AdbKeyException @@ -14,7 +21,8 @@ import io.github.sds100.keymapper.sysbridge.adb.AdbMdns import io.github.sds100.keymapper.sysbridge.adb.AdbPairingClient import io.github.sds100.keymapper.sysbridge.adb.AdbServiceType import io.github.sds100.keymapper.sysbridge.adb.PreferenceAdbKeyStore -import io.github.sds100.keymapper.sysbridge.starter.Starter +import io.github.sds100.keymapper.sysbridge.shizuku.ShizukuStarterService +import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -22,6 +30,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import rikka.shizuku.Shizuku import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -33,7 +42,8 @@ import javax.inject.Singleton @Singleton class SystemBridgeSetupControllerImpl @Inject constructor( @ApplicationContext private val ctx: Context, - private val coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope, + private val buildConfigProvider: BuildConfigProvider ) : SystemBridgeSetupController { private val sb = StringBuilder() @@ -43,6 +53,35 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override val nextSetupStep: Flow = flowOf(SystemBridgeSetupStep.ACCESSIBILITY_SERVICE) + private var scriptPath: String? = null + private val apkPath = ctx.applicationInfo.sourceDir + private val libPath = ctx.applicationInfo.nativeLibraryDir + private val packageName = ctx.applicationInfo.packageName + + private val shizukuStarterConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected( + name: ComponentName?, + binder: IBinder? + ) { + Timber.i("Shizuku starter service connected") + + val service = IShizukuStarterService.Stub.asInterface(binder) + + Timber.i("Starting System Bridge with Shizuku starter service") + try { + service.startSystemBridge(scriptPath, apkPath, libPath, packageName) + + } catch (e: RemoteException) { + Timber.e("Exception starting with Shizuku starter service: $e") + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + // Do nothing. The service is supposed to immediately kill itself + // after starting the command. + } + } + init { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { adbConnectMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) @@ -51,10 +90,45 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } + private fun startWithShizuku() { + if (!Shizuku.pingBinder()) { + Timber.e("Unable to start System Bridge with Shizuku. Shizuku Binder is not connected.") + return + } + + preStart() + + // Shizuku will start a service which will then start the System Bridge. Shizuku won't be + // used to start the System Bridge directly because native libraries need to be used + // and we want to limit the dependency on Shizuku as much as possible. Also, the System + // Bridge should still be running even if Shizuku dies. + val serviceComponentName = ComponentName(ctx, ShizukuStarterService::class.java) + val args = Shizuku.UserServiceArgs(serviceComponentName) + .daemon(false) + .processNameSuffix("service") + .debuggable(BuildConfig.DEBUG) + .version(buildConfigProvider.versionCode) + + try { + Shizuku.bindUserService( + args, + shizukuStarterConnection + ) + } catch (e: Exception) { + Timber.e("Exception when starting System Bridge with Shizuku. $e") + } + } + // TODO clean up // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) - override fun startWithAdb() { + override fun startService() { + // TODO check if shizuku permission is granted, and its running and start it that way + if (Shizuku.pingBinder()) { + startWithShizuku() + return + } + if (adbConnectMdns == null) { return } @@ -91,7 +165,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( AdbClient(host, port, key).runCatching { connect() - shellCommand(Starter.sdcardCommand) { + shellCommand(SystemBridgeStarter.sdcardCommand) { sb.append(String(it)) postResult() } @@ -114,11 +188,11 @@ class SystemBridgeSetupControllerImpl @Inject constructor( .appendLine() postResult() - Starter.writeDataFiles(ctx, true) + SystemBridgeStarter.writeDataFiles(ctx, true) AdbClient(host, port, key).runCatching { connect() - shellCommand(Starter.dataCommand) { + shellCommand(SystemBridgeStarter.dataCommand) { sb.append(String(it)) postResult() } @@ -139,7 +213,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private fun writeStarterFiles() { coroutineScope.launch(Dispatchers.IO) { try { - Starter.writeSdcardFiles(ctx) + SystemBridgeStarter.writeSdcardFiles(ctx) } catch (e: Throwable) { // TODO show error message if fails to start } @@ -156,6 +230,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) override fun pairWirelessAdb(port: Int, code: Int) { + // TODO move this to AdbManager class coroutineScope.launch(Dispatchers.IO) { val host = "127.0.0.1" @@ -208,6 +283,10 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // } // } } + + private fun preStart() { + scriptPath = SystemBridgeStarter.writeSdcardFiles(ctx) + } } @SuppressLint("ObsoleteSdkInt") @@ -218,6 +297,5 @@ interface SystemBridgeSetupController { @RequiresApi(Build.VERSION_CODES.R) fun pairWirelessAdb(port: Int, code: Int) - @RequiresApi(Build.VERSION_CODES.R) - fun startWithAdb() + fun startService() } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt new file mode 100644 index 0000000000..2af2802842 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt @@ -0,0 +1,42 @@ +package io.github.sds100.keymapper.sysbridge.shizuku + +import android.annotation.SuppressLint +import android.util.Log +import io.github.sds100.keymapper.sysbridge.IShizukuStarterService +import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter +import kotlin.system.exitProcess + +@SuppressLint("LogNotTimber") +class ShizukuStarterService : IShizukuStarterService.Stub() { + companion object { + private val TAG = "ShizukuStarterService" + } + + override fun destroy() { + Log.i(TAG, "ShizukuStarterService destroyed") + + // Must be last line in this method because it halts the JVM. + exitProcess(0) + } + + override fun startSystemBridge( + scriptPath: String?, + apkPath: String?, + libPath: String?, + packageName: String? + ) { + if (scriptPath == null || apkPath == null || libPath == null || packageName == null) { + return + } + + try { + val command = + SystemBridgeStarter.buildStartCommand(scriptPath, apkPath, libPath, packageName) + Runtime.getRuntime().exec(command).waitFor() + } catch (e: Exception) { + Log.e(TAG, "Failed to start system bridge", e) + } finally { + destroy() + } + } +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt similarity index 87% rename from sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index fb34f48df4..485be37967 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/Starter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -21,7 +21,8 @@ import java.io.InputStreamReader import java.io.PrintWriter import java.util.zip.ZipFile -internal object Starter { +// TODO clean up this code and move it to SystemBridgeManager, and if a lot of starter code in there then move it to a StarterDelegate +internal object SystemBridgeStarter { private var commandInternal = arrayOfNulls(2) @@ -29,10 +30,13 @@ internal object Starter { val sdcardCommand get() = commandInternal[1]!! - fun writeSdcardFiles(context: Context) { + /** + * @return the path to the script file. + */ + fun writeSdcardFiles(context: Context): String? { if (commandInternal[1] != null) { logd("already written") - return + return null } val um = context.getSystemService(UserManager::class.java)!! @@ -50,10 +54,19 @@ internal object Starter { val libPath = context.applicationInfo.nativeLibraryDir val packageName = context.applicationInfo.packageName - commandInternal[1] = "sh $sh --apk=$apkPath --lib=$libPath --package=$packageName" + commandInternal[1] = buildStartCommand(sh, apkPath, libPath, packageName) logd(commandInternal[1]!!) + + return sh } + fun buildStartCommand( + sh: String, + apkPath: String, + libPath: String, + packageName: String + ): String = "sh $sh --apk=$apkPath --lib=$libPath --package=$packageName" + fun writeDataFiles(context: Context, permission: Boolean = false) { if (commandInternal[0] != null && !permission) { logd("already written") From 8ffcba114a9c0685fe4bf6acf62958516c470e16 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 01:06:33 +0100 Subject: [PATCH 120/215] #1394 detect root properly --- .../keymapper/base/promode/ProModeScreen.kt | 9 ++---- .../base/promode/ProModeViewModel.kt | 4 --- .../base/promode/SystemBridgeSetupUseCase.kt | 2 -- base/src/main/res/values/strings.xml | 1 - .../sds100/keymapper/system/root/SuAdapter.kt | 30 +++++++++++-------- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 923e6dd0ea..654f648770 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -192,7 +192,7 @@ private fun SetupSection( ) is ProModeSetupState.Stopped -> { - if (state.isRootDetected) { + if (state.isRootGranted) { SetupCard( modifier = Modifier .fillMaxWidth() @@ -212,11 +212,7 @@ private fun SetupSection( style = MaterialTheme.typography.bodyMedium, ) }, - buttonText = if (state.isRootGranted) { - stringResource(R.string.pro_mode_root_detected_button_start_service) - } else { - stringResource(R.string.pro_mode_root_detected_button_request_permission) - }, + buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service), ) Spacer(modifier = Modifier.height(8.dp)) @@ -465,7 +461,6 @@ private fun Preview() { warningState = ProModeWarningState.Understood, setupState = State.Data( ProModeSetupState.Stopped( - isRootDetected = true, isRootGranted = false, shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED, setupProgress = 0.5f diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index b66477d333..a5b2e39e57 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -52,7 +52,6 @@ class ProModeViewModel @Inject constructor( combine( useCase.isSystemBridgeConnected, useCase.setupProgress, - useCase.isRootDetected, useCase.isRootGranted, useCase.shizukuSetupState, ::buildSetupState @@ -106,7 +105,6 @@ class ProModeViewModel @Inject constructor( private fun buildSetupState( isSystemBridgeConnected: Boolean, setupProgress: Float, - isRootDetected: Boolean, isRootGranted: Boolean, shizukuSetupState: ShizukuSetupState ): State { @@ -115,7 +113,6 @@ class ProModeViewModel @Inject constructor( } else { return State.Data( ProModeSetupState.Stopped( - isRootDetected = isRootDetected, isRootGranted = isRootGranted, shizukuSetupState = shizukuSetupState, setupProgress = setupProgress @@ -133,7 +130,6 @@ sealed class ProModeWarningState { sealed class ProModeSetupState { data class Stopped( - val isRootDetected: Boolean, val isRootGranted: Boolean, val shizukuSetupState: ShizukuSetupState, val setupProgress: Float diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index cb2e26517f..a633e0d80f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -46,7 +46,6 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( step.stepIndex.toFloat() / SystemBridgeSetupStep.entries.size } - override val isRootDetected: Flow = suAdapter.isRootDetected override val isRootGranted: Flow = suAdapter.isRootGranted override val shizukuSetupState: Flow = combine( @@ -107,7 +106,6 @@ interface SystemBridgeSetupUseCase { val nextSetupStep: Flow val setupProgress: Flow - val isRootDetected: Flow val isRootGranted: Flow val shizukuSetupState: Flow diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 22a32665a8..cc1b03a031 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1596,7 +1596,6 @@ 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. - Request permission Start PRO mode Shizuku detected You can skip the set up process by giving Key Mapper Shizuku permission. diff --git a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt index 09ef470771..f65f7cb74b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt @@ -10,23 +10,21 @@ import io.github.sds100.keymapper.system.permissions.Permission import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.flow.updateAndGet +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class SuAdapterImpl @Inject constructor() : SuAdapter { override val isRootGranted: MutableStateFlow = MutableStateFlow(false) - override val isRootDetected: MutableStateFlow = MutableStateFlow(false) init { Shell.getShell() invalidateIsRooted() } - override fun requestPermission(): Boolean { - Shell.cmd("su").exec() - return isRootGranted.updateAndGet { Shell.isAppGrantedRoot() ?: false } + override fun requestPermission() { + invalidateIsRooted() } override fun execute(command: String, block: Boolean): KMResult<*> { @@ -48,18 +46,26 @@ class SuAdapterImpl @Inject constructor() : SuAdapter { } fun invalidateIsRooted() { - isRootDetected.update { Shell.cmd("su").exec().isSuccess } - isRootGranted.update { Shell.isAppGrantedRoot() ?: false } + try { + // Close the shell so a new one is started without root permission. + Shell.getShell().waitAndClose() + val isRooted = Shell.isAppGrantedRoot() ?: false + isRootGranted.update { isRooted } + + if (isRooted) { + Timber.i("Root access granted") + } else { + Timber.i("Root access denied") + } + } catch (e: Exception) { + Timber.e("Exception invalidating root detection: $e") + } } } interface SuAdapter { val isRootGranted: StateFlow - val isRootDetected: StateFlow - /** - * @return whether root permission was granted successfully - */ - fun requestPermission(): Boolean + fun requestPermission() fun execute(command: String, block: Boolean = false): KMResult<*> } From 960a18e24c8633490d9925165b4ebc24409c67ad Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 01:14:41 +0100 Subject: [PATCH 121/215] #1394 starting system bridge with root works --- .../keymapper/base/promode/ProModeScreen.kt | 9 ++++- .../base/promode/ProModeViewModel.kt | 4 ++ sysbridge/build.gradle.kts | 2 +- .../service/SystemBridgeSetupController.kt | 37 ++++++++++++++++--- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 654f648770..928608a28c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -73,7 +73,8 @@ fun ProModeScreen( setupState = proModeSetupState, onWarningButtonClick = viewModel::onWarningButtonClick, onStopServiceClick = viewModel::onStopServiceClick, - onShizukuButtonClick = viewModel::onShizukuButtonClick + onShizukuButtonClick = viewModel::onShizukuButtonClick, + onRootButtonClick = viewModel::onRootButtonClick, ) } } @@ -128,6 +129,7 @@ private fun Content( onWarningButtonClick: () -> Unit = {}, onShizukuButtonClick: () -> Unit = {}, onStopServiceClick: () -> Unit = {}, + onRootButtonClick: () -> Unit = {}, ) { Column(modifier = modifier.verticalScroll(rememberScrollState())) { WarningCard( @@ -151,7 +153,8 @@ private fun Content( modifier = Modifier.fillMaxWidth(), state = setupState.data, onShizukuButtonClick = onShizukuButtonClick, - onStopServiceClick = onStopServiceClick + onStopServiceClick = onStopServiceClick, + onRootButtonClick = onRootButtonClick, ) } } @@ -171,6 +174,7 @@ private fun Content( private fun SetupSection( modifier: Modifier, state: ProModeSetupState, + onRootButtonClick: () -> Unit = {}, onShizukuButtonClick: () -> Unit, onStopServiceClick: () -> Unit ) { @@ -213,6 +217,7 @@ private fun SetupSection( ) }, buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service), + onButtonClick = onRootButtonClick ) Spacer(modifier = Modifier.height(8.dp)) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index a5b2e39e57..336adb8843 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -79,6 +79,10 @@ class ProModeViewModel @Inject constructor( useCase.stopSystemBridge() } + fun onRootButtonClick() { + useCase.startSystemBridge() + } + fun onShizukuButtonClick() { viewModelScope.launch { val shizukuState = useCase.shizukuSetupState.first() diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index 79b7c2374a..ad955d24ac 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -91,7 +91,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.dagger.hilt.android) ksp(libs.dagger.hilt.android.compiler) - + implementation(libs.github.topjohnwu.libsu) implementation(libs.rikka.shizuku.api) implementation(libs.rikka.shizuku.provider) implementation(libs.rikka.hidden.compat) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index c278a695a3..b6a60ca2d1 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -10,6 +10,7 @@ import android.os.RemoteException import android.preference.PreferenceManager import android.util.Log import androidx.annotation.RequiresApi +import com.topjohnwu.superuser.Shell import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.sysbridge.BuildConfig @@ -90,14 +91,32 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } - private fun startWithShizuku() { - if (!Shizuku.pingBinder()) { - Timber.e("Unable to start System Bridge with Shizuku. Shizuku Binder is not connected.") - return - } + /** + * @return Whether it was started with root successfully. + */ + private fun tryStartWithRoot(): Boolean { + try { + if (Shell.isAppGrantedRoot() != true) { + return false + } - preStart() + if (scriptPath == null) { + return false + } + + val command = + SystemBridgeStarter.buildStartCommand(scriptPath!!, apkPath, libPath, packageName) + Timber.i("Starting System Bridge with root") + return Shell.cmd(command).exec().isSuccess + + } catch (e: Exception) { + Timber.e("Exception when starting System Bridge with Root: $e") + return false + } + } + + private fun startWithShizuku() { // Shizuku will start a service which will then start the System Bridge. Shizuku won't be // used to start the System Bridge directly because native libraries need to be used // and we want to limit the dependency on Shizuku as much as possible. Also, the System @@ -123,6 +142,12 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) override fun startService() { + preStart() + + if (tryStartWithRoot()) { + return + } + // TODO check if shizuku permission is granted, and its running and start it that way if (Shizuku.pingBinder()) { startWithShizuku() From ccafc17e25655afe247bb60ea2509786cd99b5e6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 01:21:34 +0100 Subject: [PATCH 122/215] #1394 refactor SystemBridgeConnectionManager and starting the system bridge multiple times works --- .../github/sds100/keymapper/base/input/InputEventHub.kt | 6 +++--- .../keymapper/base/promode/SystemBridgeSetupUseCase.kt | 8 ++++---- .../sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt | 6 +++--- ...BridgeManager.kt => SystemBridgeConnectionManager.kt} | 8 ++------ .../sysbridge/provider/SystemBridgeBinderProvider.kt | 6 +++--- .../sysbridge/service/SystemBridgeSetupController.kt | 9 ++------- .../keymapper/sysbridge/starter/SystemBridgeStarter.kt | 8 ++------ 7 files changed, 19 insertions(+), 32 deletions(-) rename sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/{SystemBridgeManager.kt => SystemBridgeConnectionManager.kt} (92%) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 1d0eb71810..b53a2d9c71 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -14,7 +14,7 @@ import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent @@ -40,7 +40,7 @@ import javax.inject.Singleton @Singleton class InputEventHubImpl @Inject constructor( private val coroutineScope: CoroutineScope, - private val systemBridgeManager: SystemBridgeManager, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, private val imeInputEventInjector: ImeInputEventInjector, private val preferenceRepository: PreferenceRepository, private val devicesAdapter: DevicesAdapter, @@ -93,7 +93,7 @@ class InputEventHubImpl @Inject constructor( init { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - systemBridgeManager.registerConnection(systemBridgeConnection) + systemBridgeConnectionManager.registerConnection(systemBridgeConnection) } startKeyEventProcessingLoop() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index a633e0d80f..2cc4226878 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -5,7 +5,7 @@ import androidx.annotation.RequiresApi import dagger.hilt.android.scopes.ViewModelScoped import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter @@ -25,7 +25,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( private val preferences: PreferenceRepository, private val suAdapter: SuAdapter, private val systemBridgeSetupController: SystemBridgeSetupController, - private val systemBridgeManager: SystemBridgeManager, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, private val shizukuAdapter: ShizukuAdapter, private val permissionAdapter: PermissionAdapter, private val accessibilityServiceAdapter: AccessibilityServiceAdapter @@ -37,7 +37,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( preferences.set(Keys.isProModeWarningUnderstood, true) } - override val isSystemBridgeConnected: Flow = systemBridgeManager.isConnected + override val isSystemBridgeConnected: Flow = systemBridgeConnectionManager.isConnected override val nextSetupStep: Flow = systemBridgeSetupController.nextSetupStep @@ -70,7 +70,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } override fun stopSystemBridge() { - systemBridgeManager.stopSystemBridge() + systemBridgeConnectionManager.stopSystemBridge() } override fun enableAccessibilityService() { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt index dda80eeb70..9c2de4a473 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt @@ -4,8 +4,8 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManager -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManagerImpl +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl import javax.inject.Singleton @@ -20,5 +20,5 @@ abstract class SystemBridgeHiltModule { @Singleton @Binds - abstract fun bindSystemBridgeManager(impl: SystemBridgeManagerImpl): SystemBridgeManager + abstract fun bindSystemBridgeManager(impl: SystemBridgeConnectionManagerImpl): SystemBridgeConnectionManager } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt similarity index 92% rename from sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt rename to sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 1ca9c9ba7c..98ef622e7a 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -1,13 +1,11 @@ package io.github.sds100.keymapper.sysbridge.manager import android.annotation.SuppressLint -import android.content.Context import android.os.Build import android.os.IBinder import android.os.IBinder.DeathRecipient import android.os.RemoteException import androidx.annotation.RequiresApi -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.sysbridge.ISystemBridge import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -20,9 +18,7 @@ import javax.inject.Singleton * This class handles starting, stopping and (dis)connecting to the system bridge. */ @Singleton -class SystemBridgeManagerImpl @Inject constructor( - @ApplicationContext private val ctx: Context -) : SystemBridgeManager { +class SystemBridgeConnectionManagerImpl @Inject constructor() : SystemBridgeConnectionManager { private val systemBridgeLock: Any = Any() private var systemBridge: MutableStateFlow = MutableStateFlow(null) @@ -94,7 +90,7 @@ class SystemBridgeManagerImpl @Inject constructor( @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) -interface SystemBridgeManager { +interface SystemBridgeConnectionManager { val isConnected: Flow fun registerConnection(connection: SystemBridgeConnection) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt index e7304f48fe..5183ab3640 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt @@ -10,7 +10,7 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeManagerImpl +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl import timber.log.Timber /** @@ -27,7 +27,7 @@ internal class SystemBridgeBinderProvider : ContentProvider() { const val EXTRA_BINDER = "io.github.sds100.keymapper.sysbridge.EXTRA_BINDER" } - private val systemBridgeManager: SystemBridgeManagerImpl by lazy { + private val systemBridgeManager: SystemBridgeConnectionManagerImpl by lazy { val appContext = context?.applicationContext ?: throw IllegalStateException() val hiltEntryPoint = EntryPointAccessors.fromApplication( @@ -111,6 +111,6 @@ internal class SystemBridgeBinderProvider : ContentProvider() { @EntryPoint @InstallIn(SingletonComponent::class) interface SystemBridgeProviderEntryPoint { - fun systemBridgeManager(): SystemBridgeManagerImpl + fun systemBridgeManager(): SystemBridgeConnectionManagerImpl } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index b6a60ca2d1..f9e8847f10 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -54,7 +54,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override val nextSetupStep: Flow = flowOf(SystemBridgeSetupStep.ACCESSIBILITY_SERVICE) - private var scriptPath: String? = null + private val scriptPath: String by lazy { SystemBridgeStarter.writeSdcardFiles(ctx) } private val apkPath = ctx.applicationInfo.sourceDir private val libPath = ctx.applicationInfo.nativeLibraryDir private val packageName = ctx.applicationInfo.packageName @@ -142,13 +142,12 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) override fun startService() { - preStart() + // TODO kill the current service before starting it? if (tryStartWithRoot()) { return } - // TODO check if shizuku permission is granted, and its running and start it that way if (Shizuku.pingBinder()) { startWithShizuku() return @@ -308,10 +307,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // } // } } - - private fun preStart() { - scriptPath = SystemBridgeStarter.writeSdcardFiles(ctx) - } } @SuppressLint("ObsoleteSdkInt") diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index 485be37967..246d5a1646 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -21,7 +21,7 @@ import java.io.InputStreamReader import java.io.PrintWriter import java.util.zip.ZipFile -// TODO clean up this code and move it to SystemBridgeManager, and if a lot of starter code in there then move it to a StarterDelegate +// TODO clean up this code and move it to SystemBridgeConnectionManager, and if a lot of starter code in there then move it to a StarterDelegate internal object SystemBridgeStarter { private var commandInternal = arrayOfNulls(2) @@ -33,11 +33,7 @@ internal object SystemBridgeStarter { /** * @return the path to the script file. */ - fun writeSdcardFiles(context: Context): String? { - if (commandInternal[1] != null) { - logd("already written") - return null - } + fun writeSdcardFiles(context: Context): String { val um = context.getSystemService(UserManager::class.java)!! val unlocked = Build.VERSION.SDK_INT < 24 || um.isUserUnlocked From 2669ad000c341ae38b0647749142b2fae6f59b15 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 01:31:57 +0100 Subject: [PATCH 123/215] #1394 use separate methods for starting system bridge with root, shizuku, or adb --- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 2 + .../keymapper/base/promode/ProModeScreen.kt | 8 +++- .../base/promode/ProModeViewModel.kt | 8 +++- .../base/promode/SystemBridgeSetupUseCase.kt | 16 ++++++-- .../manager/SystemBridgeConnectionManager.kt | 2 + .../service/SystemBridgeSetupController.kt | 39 +++++++------------ 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index 96026e6a79..af0367334b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -138,6 +138,8 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { private fun init() { Log.i(tag, "KeyMapperApp: Init") + // TODO if autostart for PRO mode is turned on then start it here from boot. + settingsRepository.get(Keys.darkTheme) .map { it?.toIntOrNull() } .map { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 928608a28c..5af60564b6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -75,6 +75,7 @@ fun ProModeScreen( onStopServiceClick = viewModel::onStopServiceClick, onShizukuButtonClick = viewModel::onShizukuButtonClick, onRootButtonClick = viewModel::onRootButtonClick, + onSetupWithKeyMapperClick = viewModel::onSetupWithKeyMapperClick, ) } } @@ -130,6 +131,7 @@ private fun Content( onShizukuButtonClick: () -> Unit = {}, onStopServiceClick: () -> Unit = {}, onRootButtonClick: () -> Unit = {}, + onSetupWithKeyMapperClick: () -> Unit = {}, ) { Column(modifier = modifier.verticalScroll(rememberScrollState())) { WarningCard( @@ -155,6 +157,7 @@ private fun Content( onShizukuButtonClick = onShizukuButtonClick, onStopServiceClick = onStopServiceClick, onRootButtonClick = onRootButtonClick, + onSetupWithKeyMapperClick = onSetupWithKeyMapperClick, ) } } @@ -176,7 +179,8 @@ private fun SetupSection( state: ProModeSetupState, onRootButtonClick: () -> Unit = {}, onShizukuButtonClick: () -> Unit, - onStopServiceClick: () -> Unit + onStopServiceClick: () -> Unit, + onSetupWithKeyMapperClick: () -> Unit ) { Column(modifier) { OptionsHeaderRow( @@ -284,7 +288,7 @@ private fun SetupSection( } }, buttonText = setupKeyMapperText, - onButtonClick = onShizukuButtonClick, + onButtonClick = onSetupWithKeyMapperClick, enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index 336adb8843..db15703543 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -80,7 +80,7 @@ class ProModeViewModel @Inject constructor( } fun onRootButtonClick() { - useCase.startSystemBridge() + useCase.startSystemBridgeWithRoot() } fun onShizukuButtonClick() { @@ -100,12 +100,16 @@ class ProModeViewModel @Inject constructor( } ShizukuSetupState.PERMISSION_GRANTED -> { - useCase.startSystemBridge() + useCase.startSystemBridgeWithShizuku() } } } } + fun onSetupWithKeyMapperClick() { + useCase.startSystemBridgeWithAdb() + } + private fun buildSetupState( isSystemBridgeConnected: Boolean, setupProgress: Float, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 2cc4226878..9f6ad5756b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -93,8 +93,16 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( TODO("Not yet implemented") } - override fun startSystemBridge() { - systemBridgeSetupController.startService() + override fun startSystemBridgeWithRoot() { + systemBridgeSetupController.startWithRoot() + } + + override fun startSystemBridgeWithShizuku() { + systemBridgeSetupController.startWithShizuku() + } + + override fun startSystemBridgeWithAdb() { + systemBridgeSetupController.startWithAdb() } } @@ -118,5 +126,7 @@ interface SystemBridgeSetupUseCase { fun connectWifiNetwork() fun enableWirelessDebugging() fun pairAdb() - fun startSystemBridge() + fun startSystemBridgeWithRoot() + fun startSystemBridgeWithShizuku() + fun startSystemBridgeWithAdb() } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 98ef622e7a..95656610eb 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -20,6 +20,8 @@ import javax.inject.Singleton @Singleton class SystemBridgeConnectionManagerImpl @Inject constructor() : SystemBridgeConnectionManager { + // TODO if auto start is turned on, subscribe to Shizuku Binder listener and when bound, start the service. But only do this once per app process session. If the user stops the service it should remain stopped until key mapper is killed, + private val systemBridgeLock: Any = Any() private var systemBridge: MutableStateFlow = MutableStateFlow(null) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index f9e8847f10..6ae3cb84ab 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -91,32 +91,30 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } - /** - * @return Whether it was started with root successfully. - */ - private fun tryStartWithRoot(): Boolean { + override fun startWithRoot() { try { if (Shell.isAppGrantedRoot() != true) { - return false - } - - if (scriptPath == null) { - return false + Timber.w("Root is not granted. Cannot start System Bridge with Root.") + return } val command = - SystemBridgeStarter.buildStartCommand(scriptPath!!, apkPath, libPath, packageName) + SystemBridgeStarter.buildStartCommand(scriptPath, apkPath, libPath, packageName) Timber.i("Starting System Bridge with root") - return Shell.cmd(command).exec().isSuccess + Shell.cmd(command).exec().isSuccess } catch (e: Exception) { Timber.e("Exception when starting System Bridge with Root: $e") - return false } } - private fun startWithShizuku() { + override fun startWithShizuku() { + if (!Shizuku.pingBinder()) { + Timber.w("Shizuku is not running. Cannot start System Bridge with Shizuku.") + return + } + // Shizuku will start a service which will then start the System Bridge. Shizuku won't be // used to start the System Bridge directly because native libraries need to be used // and we want to limit the dependency on Shizuku as much as possible. Also, the System @@ -141,18 +139,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // TODO clean up // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) - override fun startService() { + override fun startWithAdb() { // TODO kill the current service before starting it? - if (tryStartWithRoot()) { - return - } - - if (Shizuku.pingBinder()) { - startWithShizuku() - return - } - if (adbConnectMdns == null) { return } @@ -317,5 +306,7 @@ interface SystemBridgeSetupController { @RequiresApi(Build.VERSION_CODES.R) fun pairWirelessAdb(port: Int, code: Int) - fun startService() + fun startWithRoot() + fun startWithShizuku() + fun startWithAdb() } \ No newline at end of file From 38c912d629db2933f51117f6fbce14138fdeda37 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 02:58:17 +0100 Subject: [PATCH 124/215] #1394 create setup wizard screen with content for each step --- .../keymapper/base/promode/ProModeFragment.kt | 45 +- .../keymapper/base/promode/ProModeScreen.kt | 15 +- .../base/promode/ProModeSetupScreen.kt | 383 ++++++++++++++++++ .../base/promode/ProModeSetupViewModel.kt | 68 ++++ .../base/promode/ProModeViewModel.kt | 16 +- .../base/utils/navigation/NavDestination.kt | 6 + .../compose/icons/SignalWifiNotConnected.kt | 91 +++++ base/src/main/res/values/strings.xml | 27 ++ .../service/SystemBridgeSetupStep.kt | 3 +- 9 files changed, 627 insertions(+), 27 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt index 877269d203..30980c3c25 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add @@ -16,11 +17,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.databinding.FragmentComposeBinding +import io.github.sds100.keymapper.base.utils.navigation.NavDestination +// TODO delete because settings will be composable @AndroidEntryPoint class ProModeFragment : Fragment() { @@ -36,16 +42,35 @@ class ProModeFragment : Fragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { KeyMapperTheme { - ProModeScreen( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding( - WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) - .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), - ), - viewModel = hiltViewModel(), - onNavigateBack = findNavController()::navigateUp, - ) + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = NavDestination.ID_PRO_MODE, + enterTransition = { slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Left) }, + exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, + popEnterTransition = { slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, + popExitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, + ) { + composable(NavDestination.ID_PRO_MODE) { + ProModeScreen( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ), + viewModel = hiltViewModel(), + onNavigateBack = { findNavController().navigateUp() }, + onNavigateToSetup = { navController.navigate(NavDestination.ProModeSetup.ID_PRO_MODE_SETUP) } + ) + } + composable(NavDestination.ProModeSetup.ID_PRO_MODE_SETUP) { + ProModeSetupScreen( + viewModel = hiltViewModel(), + onNavigateBack = { navController.navigateUp() } + ) + } + } } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 5af60564b6..475c338b4e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -63,6 +63,7 @@ fun ProModeScreen( modifier: Modifier = Modifier, viewModel: ProModeViewModel, onNavigateBack: () -> Unit, + onNavigateToSetup: () -> Unit ) { val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle() @@ -75,7 +76,7 @@ fun ProModeScreen( onStopServiceClick = viewModel::onStopServiceClick, onShizukuButtonClick = viewModel::onShizukuButtonClick, onRootButtonClick = viewModel::onRootButtonClick, - onSetupWithKeyMapperClick = viewModel::onSetupWithKeyMapperClick, + onSetupWithKeyMapperClick = onNavigateToSetup, ) } } @@ -126,7 +127,7 @@ private fun ProModeScreen( private fun Content( modifier: Modifier = Modifier, warningState: ProModeWarningState, - setupState: State, + setupState: State, onWarningButtonClick: () -> Unit = {}, onShizukuButtonClick: () -> Unit = {}, onStopServiceClick: () -> Unit = {}, @@ -176,7 +177,7 @@ private fun Content( @Composable private fun SetupSection( modifier: Modifier, - state: ProModeSetupState, + state: ProModeState, onRootButtonClick: () -> Unit = {}, onShizukuButtonClick: () -> Unit, onStopServiceClick: () -> Unit, @@ -192,14 +193,14 @@ private fun SetupSection( Spacer(modifier = Modifier.height(8.dp)) when (state) { - ProModeSetupState.Started -> ProModeStartedCard( + ProModeState.Started -> ProModeStartedCard( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), onStopClick = onStopServiceClick ) - is ProModeSetupState.Stopped -> { + is ProModeState.Stopped -> { if (state.isRootGranted) { SetupCard( modifier = Modifier @@ -469,7 +470,7 @@ private fun Preview() { Content( warningState = ProModeWarningState.Understood, setupState = State.Data( - ProModeSetupState.Stopped( + ProModeState.Stopped( isRootGranted = false, shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED, setupProgress = 0.5f @@ -487,7 +488,7 @@ private fun PreviewDark() { ProModeScreen { Content( warningState = ProModeWarningState.Understood, - setupState = State.Data(ProModeSetupState.Started) + setupState = State.Data(ProModeState.Started) ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt new file mode 100644 index 0000000000..420396b9d6 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -0,0 +1,383 @@ +package io.github.sds100.keymapper.base.promode + +import android.content.res.Configuration +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.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.Accessibility +import androidx.compose.material.icons.rounded.BugReport +import androidx.compose.material.icons.rounded.Build +import androidx.compose.material.icons.rounded.Link +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.vector.ImageVector +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.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.SignalWifiNotConnected +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep + +@Composable +fun ProModeSetupScreen( + viewModel: ProModeSetupViewModel, + onNavigateBack: () -> Unit, +) { + val state by viewModel.setupState.collectAsStateWithLifecycle() + + ProModeSetupScreen( + state = state, + onNavigateBack = onNavigateBack, + onButtonClick = viewModel::onButtonClick, + onAssistantClick = viewModel::onAssistantClick, + onWatchTutorialClick = { } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProModeSetupScreen( + state: State, + onNavigateBack: () -> Unit = {}, + onButtonClick: () -> Unit = {}, + onAssistantClick: () -> Unit = {}, + onWatchTutorialClick: () -> Unit = {} +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.pro_mode_setup_wizard_title)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(id = R.string.action_go_back) + ) + } + } + ) + } + ) { paddingValues -> + when (state) { + State.Loading -> { + CircularProgressIndicator() + } + + is State.Data -> { + val stepContent = getStepContent(state.data.step) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(vertical = 16.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // TODO animate it when it changes + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + // Add an extra step because its never done until you click start on the last step + progress = { state.data.stepNumber.toFloat() / (state.data.stepCount + 1) } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource( + R.string.pro_mode_setup_wizard_step_n, + state.data.stepNumber, + state.data.stepCount + ), + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = stringResource(R.string.pro_mode_app_bar_title), + style = MaterialTheme.typography.bodyLarge + ) + } + Spacer(modifier = Modifier.height(16.dp)) + + AssistantCheckBoxRow(onAssistantClick) + + StepContent( + modifier = Modifier + .weight(1f) + .padding(horizontal = 16.dp), + stepContent, + onWatchTutorialClick, + onButtonClick + ) + } + } + } + } +} + +@Composable +private fun StepContent( + modifier: Modifier = Modifier, + stepContent: StepContent, + onWatchTutorialClick: () -> Unit, + onButtonClick: () -> Unit +) { + Column( + modifier, + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(64.dp), + imageVector = stepContent.icon, + contentDescription = null, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stepContent.title, + style = MaterialTheme.typography.headlineSmall + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stepContent.message, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + } + + Spacer(Modifier.height(32.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton(onClick = onWatchTutorialClick) { + Text(text = stringResource(R.string.pro_mode_setup_wizard_watch_tutorial_button)) + } + Button(onClick = onButtonClick) { + Text(text = stepContent.buttonText) + } + } + } +} + +@Composable +private fun AssistantCheckBoxRow(onAssistantClick: () -> Unit) { + Surface(onClick = onAssistantClick) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = false, onCheckedChange = { onAssistantClick() }) + Column { + Text(text = stringResource(R.string.pro_mode_setup_wizard_use_assistant)) + Text( + text = stringResource(R.string.pro_mode_setup_wizard_use_assistant_enable_accessibility_service), + style = MaterialTheme.typography.bodySmall + ) + } + } + } +} + +@Composable +private fun getStepContent(step: SystemBridgeSetupStep): StepContent { + return when (step) { + SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_title), + message = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_description), + icon = Icons.Rounded.Accessibility, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + ) + + SystemBridgeSetupStep.DEVELOPER_OPTIONS -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_title), + message = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_description), + icon = Icons.Rounded.Build, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + ) + + SystemBridgeSetupStep.WIFI_NETWORK -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_connect_wifi_title), + message = stringResource(R.string.pro_mode_setup_wizard_connect_wifi_description), + icon = KeyMapperIcons.SignalWifiNotConnected, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + ) + + SystemBridgeSetupStep.WIRELESS_DEBUGGING -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_enable_wireless_debugging_title), + message = stringResource(R.string.pro_mode_setup_wizard_enable_wireless_debugging_description), + icon = Icons.Rounded.BugReport, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + ) + + SystemBridgeSetupStep.ADB_PAIRING -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_pair_wireless_debugging_title), + message = stringResource(R.string.pro_mode_setup_wizard_pair_wireless_debugging_description), + icon = Icons.Rounded.Link, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + ) + + SystemBridgeSetupStep.START_SERVICE -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_start_service_title), + message = stringResource(R.string.pro_mode_setup_wizard_start_service_description), + icon = Icons.Rounded.PlayArrow, + buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service) + ) + } +} + +private data class StepContent( + val title: String, + val message: String, + val icon: ImageVector, + val buttonText: String, +) + +// Previews for each setup step +@Preview(name = "Accessibility Service Step") +@Composable +private fun ProModeSetupScreenAccessibilityServicePreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 1, + stepCount = 6, + step = SystemBridgeSetupStep.ACCESSIBILITY_SERVICE, + isSetupAssistantEnabled = false + ) + ) + ) + } +} + +@Preview(name = "Developer Options Step") +@Composable +private fun ProModeSetupScreenDeveloperOptionsPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 2, + stepCount = 6, + step = SystemBridgeSetupStep.DEVELOPER_OPTIONS, + isSetupAssistantEnabled = false + ) + ) + ) + } +} + +@Preview(name = "WiFi Network Step") +@Composable +private fun ProModeSetupScreenWifiNetworkPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 3, + stepCount = 6, + step = SystemBridgeSetupStep.WIFI_NETWORK, + isSetupAssistantEnabled = false + ) + ) + ) + } +} + +@Preview(name = "Wireless Debugging Step") +@Composable +private fun ProModeSetupScreenWirelessDebuggingPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 4, + stepCount = 6, + step = SystemBridgeSetupStep.WIRELESS_DEBUGGING, + isSetupAssistantEnabled = false + ) + ) + ) + } +} + +@Preview(name = "ADB Pairing Step", widthDp = 400, heightDp = 400) +@Composable +private fun ProModeSetupScreenAdbPairingPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 5, + stepCount = 6, + step = SystemBridgeSetupStep.ADB_PAIRING, + isSetupAssistantEnabled = true + ) + ) + ) + } +} + +@Preview(name = "Start Service Step", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ProModeSetupScreenStartServicePreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 6, + stepCount = 6, + step = SystemBridgeSetupStep.START_SERVICE, + isSetupAssistantEnabled = true + ) + ) + ) + } +} + diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt new file mode 100644 index 0000000000..33e6e7324e --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt @@ -0,0 +1,68 @@ +package io.github.sds100.keymapper.base.promode + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ProModeSetupViewModel @Inject constructor( + private val useCase: SystemBridgeSetupUseCase, + navigationProvider: NavigationProvider, + resourceProvider: ResourceProvider +) : ViewModel(), NavigationProvider by navigationProvider, ResourceProvider by resourceProvider { + val setupState: StateFlow> = + useCase.nextSetupStep.map { step -> + State.Data( + ProModeSetupState( + stepNumber = step.stepIndex + 1, + stepCount = SystemBridgeSetupStep.entries.size, + step = step, + isSetupAssistantEnabled = step == SystemBridgeSetupStep.ADB_PAIRING || + step == SystemBridgeSetupStep.ADB_CONNECT || + step == SystemBridgeSetupStep.START_SERVICE + ) + ) + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, + State.Loading + ) + + fun onButtonClick() { + viewModelScope.launch { + val currentStep = useCase.nextSetupStep.first() + + when (currentStep) { + SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> useCase.enableAccessibilityService() + SystemBridgeSetupStep.DEVELOPER_OPTIONS -> useCase.openDeveloperOptions() + SystemBridgeSetupStep.WIFI_NETWORK -> useCase.connectWifiNetwork() + SystemBridgeSetupStep.WIRELESS_DEBUGGING -> useCase.enableWirelessDebugging() + SystemBridgeSetupStep.ADB_PAIRING, SystemBridgeSetupStep.ADB_CONNECT -> useCase.pairAdb() + SystemBridgeSetupStep.START_SERVICE -> useCase.startSystemBridgeWithAdb() + } + } + } + + fun onAssistantClick() { + // TODO: Implement setup assistant logic + // This could toggle a preference, show additional UI, or handle assistant-specific actions + } +} + +data class ProModeSetupState( + val stepNumber: Int, + val stepCount: Int, + val step: SystemBridgeSetupStep, + val isSetupAssistantEnabled: Boolean +) \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index db15703543..8a9914dbaf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -48,7 +48,7 @@ class ProModeViewModel @Inject constructor( ), ) - val setupState: StateFlow> = + val setupState: StateFlow> = combine( useCase.isSystemBridgeConnected, useCase.setupProgress, @@ -107,7 +107,7 @@ class ProModeViewModel @Inject constructor( } fun onSetupWithKeyMapperClick() { - useCase.startSystemBridgeWithAdb() + // TODO Settings screen will be refactored into compose and so NavigationProvider will work } private fun buildSetupState( @@ -115,12 +115,12 @@ class ProModeViewModel @Inject constructor( setupProgress: Float, isRootGranted: Boolean, shizukuSetupState: ShizukuSetupState - ): State { + ): State { if (isSystemBridgeConnected) { - return State.Data(ProModeSetupState.Started) + return State.Data(ProModeState.Started) } else { return State.Data( - ProModeSetupState.Stopped( + ProModeState.Stopped( isRootGranted = isRootGranted, shizukuSetupState = shizukuSetupState, setupProgress = setupProgress @@ -136,12 +136,12 @@ sealed class ProModeWarningState { data object Understood : ProModeWarningState() } -sealed class ProModeSetupState { +sealed class ProModeState { data class Stopped( val isRootGranted: Boolean, val shizukuSetupState: ShizukuSetupState, val setupProgress: Float - ) : ProModeSetupState() + ) : ProModeState() - data object Started : ProModeSetupState() + data object Started : ProModeState() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt index 92237ab8e1..b1999dec3c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt @@ -157,4 +157,10 @@ abstract class NavDestination(val isCompose: Boolean = false) { data object ProMode : NavDestination(isCompose = false) { override val id: String = ID_PRO_MODE } + + @Serializable + data object ProModeSetup : NavDestination(isCompose = true) { + const val ID_PRO_MODE_SETUP = "pro_mode_setup_wizard" + override val id: String = ID_PRO_MODE_SETUP + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt new file mode 100644 index 0000000000..f04f9321f6 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt @@ -0,0 +1,91 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.SignalWifiNotConnected: ImageVector + get() { + if (_SignalWifiNotConnected != null) { + return _SignalWifiNotConnected!! + } + _SignalWifiNotConnected = ImageVector.Builder( + name = "SignalWifiNotConnected", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(423f, 783f) + lineTo(61f, 421f) + quadToRelative(-13f, -13f, -18.5f, -28f) + reflectiveQuadTo(38f, 361f) + quadToRelative(1f, -17f, 7f, -32f) + reflectiveQuadToRelative(20f, -26f) + quadToRelative(81f, -71f, 194.5f, -107f) + reflectiveQuadTo(480f, 160f) + quadToRelative(125f, 0f, 234f, 41f) + reflectiveQuadToRelative(203f, 122f) + quadToRelative(9f, 8f, 13.5f, 17.5f) + reflectiveQuadTo(935f, 361f) + quadToRelative(0f, 11f, -3.5f, 21f) + reflectiveQuadTo(920f, 400f) + quadToRelative(-28f, -36f, -69.5f, -58f) + reflectiveQuadTo(760f, 320f) + quadToRelative(-83f, 0f, -141.5f, 58.5f) + reflectiveQuadTo(560f, 520f) + quadToRelative(0f, 49f, 22f, 90.5f) + reflectiveQuadToRelative(58f, 69.5f) + lineTo(537f, 783f) + quadToRelative(-12f, 12f, -26.5f, 18f) + reflectiveQuadToRelative(-30.5f, 6f) + quadToRelative(-16f, 0f, -30.5f, -6f) + reflectiveQuadTo(423f, 783f) + close() + moveTo(760f, 800f) + quadToRelative(-17f, 0f, -29.5f, -12.5f) + reflectiveQuadTo(718f, 758f) + quadToRelative(0f, -17f, 12.5f, -29.5f) + reflectiveQuadTo(760f, 716f) + quadToRelative(17f, 0f, 29.5f, 12.5f) + reflectiveQuadTo(802f, 758f) + quadToRelative(0f, 17f, -12.5f, 29.5f) + reflectiveQuadTo(760f, 800f) + close() + moveTo(876f, 503f) + quadToRelative(0f, 23f, -10f, 41f) + reflectiveQuadToRelative(-38f, 46f) + quadToRelative(-17f, 17f, -24.5f, 28f) + reflectiveQuadToRelative(-9.5f, 25f) + quadToRelative(-2f, 12f, -11.5f, 20.5f) + reflectiveQuadTo(761f, 672f) + quadToRelative(-13f, 0f, -22f, -9f) + reflectiveQuadToRelative(-7f, -21f) + quadToRelative(3f, -23f, 14f, -40f) + reflectiveQuadToRelative(37f, -43f) + quadToRelative(21f, -21f, 27f, -31.5f) + reflectiveQuadToRelative(6f, -26.5f) + quadToRelative(0f, -18f, -14f, -31.5f) + reflectiveQuadTo(765f, 456f) + quadToRelative(-15f, 0f, -29f, 7f) + reflectiveQuadToRelative(-24f, 20f) + quadToRelative(-7f, 9f, -17.5f, 13f) + reflectiveQuadToRelative(-21.5f, -1f) + quadToRelative(-11f, -5f, -16.5f, -15f) + reflectiveQuadToRelative(0.5f, -20f) + quadToRelative(16f, -28f, 44.5f, -44f) + reflectiveQuadToRelative(63.5f, -16f) + quadToRelative(49f, 0f, 80f, 29f) + reflectiveQuadToRelative(31f, 74f) + close() + } + }.build() + + return _SignalWifiNotConnected!! + } + +@Suppress("ObjectPropertyName") +private var _SignalWifiNotConnected: ImageVector? = null diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index cc1b03a031..1e097d20d2 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1612,4 +1612,31 @@ PRO mode service is running Stop + Setup wizard + Step %d of %d + Use setup assistant + Enable accessibility service first + Watch tutorial + Go to settings + Start service + + Enable accessibility service + Key Mapper uses a service to help you set up PRO mode. It\'s also useful for ordinary key maps. + + Enable developer options + Key Mapper needs to use Android Debug Bridge to start PRO mode, and you need to enable developer options for that. + + Connect to a WiFi network + Key Mapper needs a WiFi network to enable ADB. You do not need an internet connection. + + Enable wireless debugging + Key Mapper uses wireless debugging to launch its remapping and input service. + + Pair wireless debugging + Key Mapper needs to pair with wireless debugging before it can launch its remapping and input service. + + Start service + Key Mapper needs to connect to the Android Debug Bridge to start the PRO mode service. + + diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt index e8e3488bbd..3785f9ed0c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt @@ -6,6 +6,5 @@ enum class SystemBridgeSetupStep(val stepIndex: Int) { WIFI_NETWORK(stepIndex = 2), WIRELESS_DEBUGGING(stepIndex = 3), ADB_PAIRING(stepIndex = 4), - ADB_CONNECT(stepIndex = 5), - START_SERVICE(stepIndex = 6) + START_SERVICE(stepIndex = 5) } \ No newline at end of file From ff961e1d7c055ff677e79f67b3f506d7c5078b83 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 03:21:43 +0100 Subject: [PATCH 125/215] #1394 switching to developer options step and toggling setup assistant works --- .../base/promode/ProModeSetupScreen.kt | 77 ++++++++++++++----- .../base/promode/ProModeSetupViewModel.kt | 34 ++++---- .../base/promode/SystemBridgeSetupUseCase.kt | 41 +++++++++- base/src/main/res/values/strings.xml | 1 + .../io/github/sds100/keymapper/data/Keys.kt | 3 + .../service/SystemBridgeSetupController.kt | 7 -- 6 files changed, 116 insertions(+), 47 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index 420396b9d6..988e36e4a0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -33,6 +34,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -133,7 +135,12 @@ fun ProModeSetupScreen( } Spacer(modifier = Modifier.height(16.dp)) - AssistantCheckBoxRow(onAssistantClick) + AssistantCheckBoxRow( + modifier = Modifier.fillMaxWidth(), + isEnabled = state.data.step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE, + isChecked = state.data.isSetupAssistantChecked, + onAssistantClick = onAssistantClick + ) StepContent( modifier = Modifier @@ -206,19 +213,49 @@ private fun StepContent( } @Composable -private fun AssistantCheckBoxRow(onAssistantClick: () -> Unit) { - Surface(onClick = onAssistantClick) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox(checked = false, onCheckedChange = { onAssistantClick() }) - Column { - Text(text = stringResource(R.string.pro_mode_setup_wizard_use_assistant)) - Text( - text = stringResource(R.string.pro_mode_setup_wizard_use_assistant_enable_accessibility_service), - style = MaterialTheme.typography.bodySmall - ) +private fun AssistantCheckBoxRow( + modifier: Modifier, + isEnabled: Boolean, + isChecked: Boolean, + onAssistantClick: () -> Unit +) { + Surface( + modifier = modifier, shape = MaterialTheme.shapes.medium, + enabled = isEnabled, + onClick = onAssistantClick + ) { + val contentColor = if (isEnabled) { + LocalContentColor.current + } else { + LocalContentColor.current.copy(alpha = 0.5f) + } + + CompositionLocalProvider(LocalContentColor provides contentColor) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + enabled = isEnabled, + checked = isChecked, + onCheckedChange = { onAssistantClick() }) + Column { + Text( + text = stringResource(R.string.pro_mode_setup_wizard_use_assistant), + style = MaterialTheme.typography.titleMedium + ) + + val text = if (isEnabled) { + stringResource(R.string.pro_mode_setup_wizard_use_assistant_description) + } else { + stringResource(R.string.pro_mode_setup_wizard_use_assistant_enable_accessibility_service) + } + + Text( + text = text, + style = MaterialTheme.typography.bodyMedium + ) + } } } } @@ -289,7 +326,7 @@ private fun ProModeSetupScreenAccessibilityServicePreview() { stepNumber = 1, stepCount = 6, step = SystemBridgeSetupStep.ACCESSIBILITY_SERVICE, - isSetupAssistantEnabled = false + isSetupAssistantChecked = false ) ) ) @@ -306,7 +343,7 @@ private fun ProModeSetupScreenDeveloperOptionsPreview() { stepNumber = 2, stepCount = 6, step = SystemBridgeSetupStep.DEVELOPER_OPTIONS, - isSetupAssistantEnabled = false + isSetupAssistantChecked = false ) ) ) @@ -323,7 +360,7 @@ private fun ProModeSetupScreenWifiNetworkPreview() { stepNumber = 3, stepCount = 6, step = SystemBridgeSetupStep.WIFI_NETWORK, - isSetupAssistantEnabled = false + isSetupAssistantChecked = false ) ) ) @@ -340,7 +377,7 @@ private fun ProModeSetupScreenWirelessDebuggingPreview() { stepNumber = 4, stepCount = 6, step = SystemBridgeSetupStep.WIRELESS_DEBUGGING, - isSetupAssistantEnabled = false + isSetupAssistantChecked = false ) ) ) @@ -357,7 +394,7 @@ private fun ProModeSetupScreenAdbPairingPreview() { stepNumber = 5, stepCount = 6, step = SystemBridgeSetupStep.ADB_PAIRING, - isSetupAssistantEnabled = true + isSetupAssistantChecked = true ) ) ) @@ -374,7 +411,7 @@ private fun ProModeSetupScreenStartServicePreview() { stepNumber = 6, stepCount = 6, step = SystemBridgeSetupStep.START_SERVICE, - isSetupAssistantEnabled = true + isSetupAssistantChecked = true ) ) ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt index 33e6e7324e..79bdf99383 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt @@ -9,8 +9,8 @@ import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -22,18 +22,7 @@ class ProModeSetupViewModel @Inject constructor( resourceProvider: ResourceProvider ) : ViewModel(), NavigationProvider by navigationProvider, ResourceProvider by resourceProvider { val setupState: StateFlow> = - useCase.nextSetupStep.map { step -> - State.Data( - ProModeSetupState( - stepNumber = step.stepIndex + 1, - stepCount = SystemBridgeSetupStep.entries.size, - step = step, - isSetupAssistantEnabled = step == SystemBridgeSetupStep.ADB_PAIRING || - step == SystemBridgeSetupStep.ADB_CONNECT || - step == SystemBridgeSetupStep.START_SERVICE - ) - ) - }.stateIn( + combine(useCase.nextSetupStep, useCase.isSetupAssistantEnabled, ::buildState).stateIn( viewModelScope, SharingStarted.Eagerly, State.Loading @@ -48,15 +37,26 @@ class ProModeSetupViewModel @Inject constructor( SystemBridgeSetupStep.DEVELOPER_OPTIONS -> useCase.openDeveloperOptions() SystemBridgeSetupStep.WIFI_NETWORK -> useCase.connectWifiNetwork() SystemBridgeSetupStep.WIRELESS_DEBUGGING -> useCase.enableWirelessDebugging() - SystemBridgeSetupStep.ADB_PAIRING, SystemBridgeSetupStep.ADB_CONNECT -> useCase.pairAdb() + SystemBridgeSetupStep.ADB_PAIRING -> useCase.pairAdb() SystemBridgeSetupStep.START_SERVICE -> useCase.startSystemBridgeWithAdb() } } } + private fun buildState( + step: SystemBridgeSetupStep, + isSetupAssistantEnabled: Boolean + ): State.Data = State.Data( + ProModeSetupState( + stepNumber = step.stepIndex + 1, + stepCount = SystemBridgeSetupStep.entries.size, + step = step, + isSetupAssistantChecked = isSetupAssistantEnabled + ) + ) + fun onAssistantClick() { - // TODO: Implement setup assistant logic - // This could toggle a preference, show additional UI, or handle assistant-specific actions + useCase.toggleSetupAssistant() } } @@ -64,5 +64,5 @@ data class ProModeSetupState( val stepNumber: Int, val stepCount: Int, val step: SystemBridgeSetupStep, - val isSetupAssistantEnabled: Boolean + val isSetupAssistantChecked: Boolean ) \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 9f6ad5756b..48a7587bca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -9,6 +9,7 @@ import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManage import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter +import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceState import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.root.SuAdapter @@ -28,7 +29,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( private val systemBridgeConnectionManager: SystemBridgeConnectionManager, private val shizukuAdapter: ShizukuAdapter, private val permissionAdapter: PermissionAdapter, - private val accessibilityServiceAdapter: AccessibilityServiceAdapter + private val accessibilityServiceAdapter: AccessibilityServiceAdapter, ) : SystemBridgeSetupUseCase { override val isWarningUnderstood: Flow = preferences.get(Keys.isProModeWarningUnderstood).map { it ?: false } @@ -37,10 +38,26 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( preferences.set(Keys.isProModeWarningUnderstood, true) } + override val isSetupAssistantEnabled: Flow = + preferences.get(Keys.isProModeSetupAssistantEnabled).map { it ?: false } + + override fun toggleSetupAssistant() { + preferences.update(Keys.isProModeSetupAssistantEnabled) { + if (it == null) { + true + } else { + !it + } + } + } + override val isSystemBridgeConnected: Flow = systemBridgeConnectionManager.isConnected - override val nextSetupStep: Flow = - systemBridgeSetupController.nextSetupStep + override val nextSetupStep: Flow = combine( + accessibilityServiceAdapter.state, + systemBridgeConnectionManager.isConnected, + ::getNextStep + ) override val setupProgress: Flow = nextSetupStep.map { step -> step.stepIndex.toFloat() / SystemBridgeSetupStep.entries.size @@ -104,12 +121,30 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( override fun startSystemBridgeWithAdb() { systemBridgeSetupController.startWithAdb() } + + private fun getNextStep( + accessibilityServiceState: AccessibilityServiceState, + isSystemBridgeStarted: Boolean + ): SystemBridgeSetupStep = + when { + accessibilityServiceState != AccessibilityServiceState.ENABLED -> { + SystemBridgeSetupStep.ACCESSIBILITY_SERVICE + } + + else -> { + SystemBridgeSetupStep.DEVELOPER_OPTIONS + } + } + } interface SystemBridgeSetupUseCase { val isWarningUnderstood: Flow fun onUnderstoodWarning() + val isSetupAssistantEnabled: Flow + fun toggleSetupAssistant() + val isSystemBridgeConnected: Flow val nextSetupStep: Flow val setupProgress: Flow diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 1e097d20d2..e615ebba18 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1615,6 +1615,7 @@ Setup wizard Step %d of %d Use setup assistant + Automatically interact with settings Enable accessibility service first Watch tutorial Go to settings diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index cdc95f350e..df46a9cb7c 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -113,4 +113,7 @@ object Keys { val isProModeWarningUnderstood = booleanPreferencesKey("key_is_pro_mode_warning_understood") + + val isProModeSetupAssistantEnabled = + booleanPreferencesKey("key_is_pro_mode_setup_assistant_enabled") } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 6ae3cb84ab..ee8930b908 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -26,9 +26,7 @@ import io.github.sds100.keymapper.sysbridge.shizuku.ShizukuStarterService import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import rikka.shizuku.Shizuku @@ -51,9 +49,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private val adbConnectMdns: AdbMdns? - override val nextSetupStep: Flow = - flowOf(SystemBridgeSetupStep.ACCESSIBILITY_SERVICE) - private val scriptPath: String by lazy { SystemBridgeStarter.writeSdcardFiles(ctx) } private val apkPath = ctx.applicationInfo.sourceDir private val libPath = ctx.applicationInfo.nativeLibraryDir @@ -301,8 +296,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeSetupController { - val nextSetupStep: Flow - @RequiresApi(Build.VERSION_CODES.R) fun pairWirelessAdb(port: Int, code: Int) From 854c29d2f6b3eba77c2614eab308a4b774102dcc Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 03:26:05 +0100 Subject: [PATCH 126/215] #1394 change text of start accessibility service button --- .../sds100/keymapper/base/promode/ProModeSetupScreen.kt | 6 +++--- base/src/main/res/values/strings.xml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index 988e36e4a0..f59466bcef 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -126,11 +126,11 @@ fun ProModeSetupScreen( state.data.stepNumber, state.data.stepCount ), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.titleLarge ) Text( text = stringResource(R.string.pro_mode_app_bar_title), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.titleLarge ) } Spacer(modifier = Modifier.height(16.dp)) @@ -268,7 +268,7 @@ private fun getStepContent(step: SystemBridgeSetupStep): StepContent { title = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_title), message = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_description), icon = Icons.Rounded.Accessibility, - buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_button) ) SystemBridgeSetupStep.DEVELOPER_OPTIONS -> StepContent( diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index e615ebba18..3d252dc784 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1618,6 +1618,7 @@ Automatically interact with settings Enable accessibility service first Watch tutorial + Start service Go to settings Start service From 2e9c54b4d9447f7af056041b68e861465faa11f9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 03:30:29 +0100 Subject: [PATCH 127/215] #1394 animate setup progress indicator --- .../base/promode/ProModeSetupScreen.kt | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index f59466bcef..e91c906ee9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -1,6 +1,9 @@ package io.github.sds100.keymapper.base.promode import android.content.res.Configuration +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.EaseInOut +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -35,7 +38,9 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -99,6 +104,32 @@ fun ProModeSetupScreen( is State.Data -> { val stepContent = getStepContent(state.data.step) + // Create animated progress for entrance and updates + val progressAnimatable = remember { Animatable(0f) } + val targetProgress = state.data.stepNumber.toFloat() / (state.data.stepCount + 1) + + // Animate progress when it changes + LaunchedEffect(targetProgress) { + progressAnimatable.animateTo( + targetValue = targetProgress, + animationSpec = tween( + durationMillis = 800, + easing = EaseInOut + ) + ) + } + + // Animate entrance when screen opens + LaunchedEffect(Unit) { + progressAnimatable.animateTo( + targetValue = targetProgress, + animationSpec = tween( + durationMillis = 1000, + easing = EaseInOut + ) + ) + } + Column( modifier = Modifier .fillMaxSize() @@ -106,11 +137,9 @@ fun ProModeSetupScreen( .padding(vertical = 16.dp, horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // TODO animate it when it changes LinearProgressIndicator( modifier = Modifier.fillMaxWidth(), - // Add an extra step because its never done until you click start on the last step - progress = { state.data.stepNumber.toFloat() / (state.data.stepCount + 1) } + progress = { progressAnimatable.value } ) Spacer(modifier = Modifier.height(16.dp)) From fe8a9d4a8a095eb3a86b4d51bff5706f01c1535d Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 04:18:51 +0100 Subject: [PATCH 128/215] #1394 launching developer options and connecting to a wifi network steps work --- .../sds100/keymapper/base/BaseMainActivity.kt | 10 ++- .../base/promode/SystemBridgeSetupUseCase.kt | 23 +++--- base/src/main/res/values/strings.xml | 2 +- .../service/SystemBridgeSetupController.kt | 53 ++++++++++++- .../sds100/keymapper/system/SettingsUtils.kt | 20 ----- .../system/network/AndroidNetworkAdapter.kt | 78 ++++++++++++++++++- .../system/network/NetworkAdapter.kt | 3 + 7 files changed, 153 insertions(+), 36 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 190dfaeb47..0a269f0107 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -33,10 +33,11 @@ import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider -import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.network.AndroidNetworkAdapter import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.root.SuAdapterImpl @@ -86,7 +87,7 @@ abstract class BaseMainActivity : AppCompatActivity() { lateinit var buildConfigProvider: BuildConfigProvider @Inject - lateinit var privServiceSetup: SystemBridgeSetupController + lateinit var systemBridgeSetupController: SystemBridgeSetupControllerImpl @Inject lateinit var suAdapter: SuAdapterImpl @@ -94,6 +95,9 @@ abstract class BaseMainActivity : AppCompatActivity() { @Inject lateinit var devicesAdapter: AndroidDevicesAdapter + @Inject + lateinit var networkAdapter: AndroidNetworkAdapter + private lateinit var requestPermissionDelegate: RequestPermissionDelegate private val currentNightMode: Int @@ -206,6 +210,8 @@ abstract class BaseMainActivity : AppCompatActivity() { permissionAdapter.onPermissionsChanged() serviceAdapter.invalidateState() suAdapter.invalidateIsRooted() + systemBridgeSetupController.updateDeveloperOptionsEnabled() + networkAdapter.invalidateState() } override fun onDestroy() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 48a7587bca..8bbbf7778e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -10,6 +10,7 @@ import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceState +import io.github.sds100.keymapper.system.network.NetworkAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.root.SuAdapter @@ -30,6 +31,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( private val shizukuAdapter: ShizukuAdapter, private val permissionAdapter: PermissionAdapter, private val accessibilityServiceAdapter: AccessibilityServiceAdapter, + private val networkAdapter: NetworkAdapter ) : SystemBridgeSetupUseCase { override val isWarningUnderstood: Flow = preferences.get(Keys.isProModeWarningUnderstood).map { it ?: false } @@ -55,7 +57,8 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( override val nextSetupStep: Flow = combine( accessibilityServiceAdapter.state, - systemBridgeConnectionManager.isConnected, + systemBridgeSetupController.isDeveloperOptionsEnabled, + networkAdapter.isWifiConnected, ::getNextStep ) @@ -95,11 +98,11 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } override fun openDeveloperOptions() { - TODO("Not yet implemented") + systemBridgeSetupController.enableDeveloperOptions() } override fun connectWifiNetwork() { - TODO("Not yet implemented") + networkAdapter.connectWifiNetwork() } override fun enableWirelessDebugging() { @@ -124,16 +127,14 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( private fun getNextStep( accessibilityServiceState: AccessibilityServiceState, - isSystemBridgeStarted: Boolean + isDeveloperOptionsEnabled: Boolean, + isWifiConnected: Boolean, ): SystemBridgeSetupStep = when { - accessibilityServiceState != AccessibilityServiceState.ENABLED -> { - SystemBridgeSetupStep.ACCESSIBILITY_SERVICE - } - - else -> { - SystemBridgeSetupStep.DEVELOPER_OPTIONS - } + accessibilityServiceState != AccessibilityServiceState.ENABLED -> SystemBridgeSetupStep.ACCESSIBILITY_SERVICE + !isDeveloperOptionsEnabled -> SystemBridgeSetupStep.DEVELOPER_OPTIONS + !isWifiConnected -> SystemBridgeSetupStep.WIFI_NETWORK + else -> SystemBridgeSetupStep.WIRELESS_DEBUGGING } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3d252dc784..9f440ea9e7 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1629,7 +1629,7 @@ Key Mapper needs to use Android Debug Bridge to start PRO mode, and you need to enable developer options for that. Connect to a WiFi network - Key Mapper needs a WiFi network to enable ADB. You do not need an internet connection. + Key Mapper needs a WiFi network to enable ADB. You do not need an internet connection.\n\nNo WiFi network? Use a hotspot from someone else\'s phone. Enable wireless debugging Key Mapper uses wireless debugging to launch its remapping and input service. diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index ee8930b908..308aaa869b 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,15 +1,19 @@ package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint +import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context +import android.content.Intent import android.content.ServiceConnection import android.os.Build import android.os.IBinder import android.os.RemoteException import android.preference.PreferenceManager +import android.provider.Settings import android.util.Log import androidx.annotation.RequiresApi +import androidx.core.os.bundleOf import com.topjohnwu.superuser.Shell import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider @@ -26,7 +30,10 @@ import io.github.sds100.keymapper.sysbridge.shizuku.ShizukuStarterService import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import rikka.shizuku.Shizuku @@ -45,7 +52,14 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private val buildConfigProvider: BuildConfigProvider ) : SystemBridgeSetupController { - private val sb = StringBuilder() + companion object { + private const val DEVELOPER_OPTIONS_SETTING = "development_settings_enabled" + } + + override val isDeveloperOptionsEnabled: MutableStateFlow = + MutableStateFlow(getDeveloperOptionsEnabled()) + + val sb = StringBuilder() private val adbConnectMdns: AdbMdns? @@ -291,11 +305,48 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // } // } } + + override fun enableDeveloperOptions() { + // TODO show notification after the actvitiy is to tap the Build Number repeatedly + + val intent = 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) + + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + + try { + ctx.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Timber.e("Failed to start About Phone activity: $e") + } + } + + fun updateDeveloperOptionsEnabled() { + isDeveloperOptionsEnabled.update { getDeveloperOptionsEnabled() } + } + + private fun getDeveloperOptionsEnabled(): Boolean { + try { + return Settings.Global.getInt(ctx.contentResolver, DEVELOPER_OPTIONS_SETTING) == 1 + } catch (e: Settings.SettingNotFoundException) { + return false + } + } } @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeSetupController { + val isDeveloperOptionsEnabled: Flow + fun enableDeveloperOptions() + @RequiresApi(Build.VERSION_CODES.R) fun pairWirelessAdb(port: Int, code: Int) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt index 6fbe786ccd..a38747af20 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt @@ -5,7 +5,6 @@ import android.content.Context import android.provider.Settings import androidx.annotation.RequiresPermission - object SettingsUtils { /** @@ -111,23 +110,4 @@ object SettingsUtils { } } } - - /** - * @return whether the setting was changed successfully - */ - @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) - inline fun putGlobalSetting(ctx: Context, name: String, value: T): Boolean { - val contentResolver = ctx.contentResolver - - return when (T::class) { - Int::class -> Settings.Global.putInt(contentResolver, name, value as Int) - String::class -> Settings.Global.putString(contentResolver, name, value as String) - Float::class -> Settings.Global.putFloat(contentResolver, name, value as Float) - Long::class -> Settings.Global.putLong(contentResolver, name, value as Long) - - else -> { - throw Exception("Setting type ${T::class} is not supported") - } - } - } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 8c05233836..58604ca38a 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -1,14 +1,21 @@ package io.github.sds100.keymapper.system.network +import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.net.wifi.WifiManager import android.os.Build +import android.provider.Settings import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success @@ -25,7 +32,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import okio.IOException import okio.use import timber.log.Timber -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -37,6 +43,8 @@ class AndroidNetworkAdapter @Inject constructor( private val ctx = context.applicationContext private val wifiManager: WifiManager by lazy { ctx.getSystemService()!! } private val telephonyManager: TelephonyManager by lazy { ctx.getSystemService()!! } + private val connectivityManager: ConnectivityManager by lazy { ctx.getSystemService()!! } + private val httpClient: OkHttpClient by lazy { OkHttpClient() } private val broadcastReceiver = object : BroadcastReceiver() { @@ -67,8 +75,36 @@ class AndroidNetworkAdapter @Inject constructor( } override val connectedWifiSSIDFlow = MutableStateFlow(connectedWifiSSID) + override val isWifiConnected: MutableStateFlow = MutableStateFlow(getIsWifiConnected()) + private val isWifiEnabled = MutableStateFlow(isWifiEnabled()) + private val networkCallback: ConnectivityManager.NetworkCallback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + isWifiConnected.update { getIsWifiConnected() } + } + + override fun onLost(network: Network) { + super.onLost(network) + // A network was lost. Check if we are still connected to *any* Wi-Fi. + // This is important because onLost is called for a specific network. + // If multiple Wi-Fi networks were available and one is lost, + // another might still be active. + isWifiConnected.update { getIsWifiConnected() } + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + super.onCapabilitiesChanged(network, networkCapabilities) + + isWifiConnected.update { getIsWifiConnected() } + } + } + init { IntentFilter().apply { addAction(WifiManager.WIFI_STATE_CHANGED_ACTION) @@ -81,6 +117,12 @@ class AndroidNetworkAdapter @Inject constructor( ContextCompat.RECEIVER_EXPORTED, ) } + + val networkRequest = NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build() + + connectivityManager.registerNetworkCallback(networkRequest, networkCallback) } override fun isWifiEnabled(): Boolean = wifiManager.isWifiEnabled @@ -175,4 +217,38 @@ class AndroidNetworkAdapter @Inject constructor( return KMError.MalformedUrl } } + + override fun connectWifiNetwork() { + val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Intent(Settings.Panel.ACTION_WIFI) + } else { + Intent(Settings.ACTION_WIFI_SETTINGS) + } + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + try { + ctx.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Timber.e(e, "Failed to start Wi-Fi settings activity") + } + } + + private fun getIsWifiConnected(): Boolean { // Add this to your NetworkAdapter interface too + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + } else { + @Suppress("DEPRECATION") + val networkInfo = connectivityManager.activeNetworkInfo ?: return false + @Suppress("DEPRECATION") + return networkInfo.isConnected && networkInfo.type == ConnectivityManager.TYPE_WIFI + } + } + + fun invalidateState() { + connectedWifiSSIDFlow.update { connectedWifiSSID } + isWifiConnected.update { getIsWifiConnected() } + } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt index d7a1f04a7c..62480ffa9c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt @@ -8,11 +8,14 @@ interface NetworkAdapter { val connectedWifiSSID: String? val connectedWifiSSIDFlow: Flow + val isWifiConnected: Flow + fun isWifiEnabled(): Boolean fun isWifiEnabledFlow(): Flow fun enableWifi(): KMResult<*> fun disableWifi(): KMResult<*> + fun connectWifiNetwork() fun isMobileDataEnabled(): Boolean From 5a077763671bd747f5355647f51120e5b43c2537 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 15:34:12 +0100 Subject: [PATCH 129/215] #1394 launching developer options and highlighting wireless debugging works --- .../base/promode/SystemBridgeSetupUseCase.kt | 2 +- .../AccessibilityServiceAdapterImpl.kt | 2 +- common/build.gradle.kts | 1 + .../keymapper/common/utils}/SettingsUtils.kt | 30 ++++++++++++++- .../service/SystemBridgeSetupController.kt | 38 +++++++++---------- .../AndroidAirplaneModeAdapter.kt | 4 +- .../system/display/AndroidDisplayAdapter.kt | 4 +- .../inputmethod/AndroidInputMethodAdapter.kt | 2 +- 8 files changed, 53 insertions(+), 30 deletions(-) rename {system/src/main/java/io/github/sds100/keymapper/system => common/src/main/java/io/github/sds100/keymapper/common/utils}/SettingsUtils.kt (81%) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 8bbbf7778e..a824bb7c50 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -106,7 +106,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } override fun enableWirelessDebugging() { - TODO("Not yet implemented") + systemBridgeSetupController.enableWirelessDebugging() } override fun pairAdb() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt index bb08d91614..a601d51b87 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt @@ -14,11 +14,11 @@ import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.KeyMapperClassProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.system.JobSchedulerHelper -import io.github.sds100.keymapper.system.SettingsUtils import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceState diff --git a/common/build.gradle.kts b/common/build.gradle.kts index d438004789..820f93c8fc 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { // kotlin stuff implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) + implementation(libs.jakewharton.timber) implementation(libs.androidx.core.ktx) implementation(libs.dagger.hilt.android) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt similarity index 81% rename from system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt rename to common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt index a38747af20..01e6c6af27 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt @@ -1,9 +1,13 @@ -package io.github.sds100.keymapper.system +package io.github.sds100.keymapper.common.utils import android.Manifest +import android.content.ActivityNotFoundException import android.content.Context +import android.content.Intent import android.provider.Settings import androidx.annotation.RequiresPermission +import androidx.core.os.bundleOf +import timber.log.Timber object SettingsUtils { @@ -110,4 +114,26 @@ object SettingsUtils { } } } -} + + fun launchSettingsScreen(ctx: Context, action: String, fragmentArg: String?) { + val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply { + if (fragmentArg != null) { + val fragmentArgKey = ":settings:fragment_args_key" + val showFragmentArgsKey = ":settings:show_fragment_args" + + putExtra(fragmentArgKey, fragmentArg) + + val bundle = bundleOf(fragmentArgKey to fragmentArg) + putExtra(showFragmentArgsKey, bundle) + + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + } + + try { + ctx.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Timber.e("Failed to start Settings activity: $e") + } + } +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 308aaa869b..11c4de548c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,10 +1,8 @@ package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint -import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context -import android.content.Intent import android.content.ServiceConnection import android.os.Build import android.os.IBinder @@ -13,10 +11,10 @@ import android.preference.PreferenceManager import android.provider.Settings import android.util.Log import androidx.annotation.RequiresApi -import androidx.core.os.bundleOf import com.topjohnwu.superuser.Shell import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.sysbridge.BuildConfig import io.github.sds100.keymapper.sysbridge.IShizukuStarterService import io.github.sds100.keymapper.sysbridge.adb.AdbClient @@ -309,23 +307,19 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override fun enableDeveloperOptions() { // TODO show notification after the actvitiy is to tap the Build Number repeatedly - val intent = 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) - - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } + SettingsUtils.launchSettingsScreen( + ctx, + Settings.ACTION_DEVICE_INFO_SETTINGS, + "build_number" + ) + } - try { - ctx.startActivity(intent) - } catch (e: ActivityNotFoundException) { - Timber.e("Failed to start About Phone activity: $e") - } + override fun enableWirelessDebugging() { + SettingsUtils.launchSettingsScreen( + ctx, + Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS, + "toggle_adb_wireless" + ) } fun updateDeveloperOptionsEnabled() { @@ -334,8 +328,8 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private fun getDeveloperOptionsEnabled(): Boolean { try { - return Settings.Global.getInt(ctx.contentResolver, DEVELOPER_OPTIONS_SETTING) == 1 - } catch (e: Settings.SettingNotFoundException) { + return SettingsUtils.getGlobalSetting(ctx, DEVELOPER_OPTIONS_SETTING) == 1 + } catch (_: Settings.SettingNotFoundException) { return false } } @@ -347,6 +341,8 @@ interface SystemBridgeSetupController { val isDeveloperOptionsEnabled: Flow fun enableDeveloperOptions() + fun enableWirelessDebugging() + @RequiresApi(Build.VERSION_CODES.R) fun pairWirelessAdb(port: Int, code: Int) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt index df7c4a7c5c..4cba36171f 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt @@ -2,11 +2,11 @@ package io.github.sds100.keymapper.system.airplanemode import android.content.Context import android.provider.Settings +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.onSuccess -import io.github.sds100.keymapper.system.SettingsUtils import io.github.sds100.keymapper.system.root.SuAdapter -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt index 3c4eaf1c08..2319903011 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt @@ -11,12 +11,12 @@ import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError -import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Orientation +import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.getRealDisplaySize -import io.github.sds100.keymapper.system.SettingsUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt index 5f3598ae29..f748cd831d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt @@ -17,6 +17,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess @@ -24,7 +25,6 @@ import io.github.sds100.keymapper.common.utils.otherwise import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.system.JobSchedulerHelper -import io.github.sds100.keymapper.system.SettingsUtils import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent From 1630a7935225b52abc6d4b67f161dad1948f9513 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 15:46:15 +0100 Subject: [PATCH 130/215] #1394 launching wireless debugging directly works --- .../service/SystemBridgeSetupController.kt | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 11c4de548c..e64fb3a6f9 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,14 +1,17 @@ package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint +import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context +import android.content.Intent import android.content.ServiceConnection import android.os.Build import android.os.IBinder import android.os.RemoteException import android.preference.PreferenceManager import android.provider.Settings +import android.service.quicksettings.TileService import android.util.Log import androidx.annotation.RequiresApi import com.topjohnwu.superuser.Shell @@ -39,6 +42,7 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton + /** * This starter code is taken from the Shizuku project. */ @@ -315,11 +319,32 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } override fun enableWirelessDebugging() { - SettingsUtils.launchSettingsScreen( - ctx, - Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS, - "toggle_adb_wireless" - ) + // This is the intent sent by the quick settings tile. Not all devices support this. + val quickSettingsIntent = Intent(TileService.ACTION_QS_TILE_PREFERENCES).apply { + // Set the package name because this action can also resolve to a "Permission Controller" activity. + val packageName = "com.android.settings" + setPackage(packageName) + + putExtra( + Intent.EXTRA_COMPONENT_NAME, + ComponentName( + packageName, + "com.android.settings.development.qstile.DevelopmentTiles\$WirelessDebugging" + ) + ) + + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + + try { + ctx.startActivity(quickSettingsIntent) + } catch (e: ActivityNotFoundException) { + SettingsUtils.launchSettingsScreen( + ctx, + Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS, + "toggle_adb_wireless" + ) + } } fun updateDeveloperOptionsEnabled() { From 2f9d42b872a4b5ded4c35d53b66fbba2c2fac0d1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 12 Aug 2025 16:02:51 +0100 Subject: [PATCH 131/215] #1394 add set up step requesting notification permission --- .../base/promode/ProModeSetupScreen.kt | 31 +++++++++++++++++-- .../base/promode/ProModeSetupViewModel.kt | 3 +- .../base/promode/SystemBridgeSetupUseCase.kt | 8 +++++ .../SystemBridgeSetupAssistantController.kt | 3 ++ base/src/main/res/values/strings.xml | 3 ++ .../service/SystemBridgeSetupStep.kt | 11 ++++--- 6 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index e91c906ee9..c8d72013b5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.rounded.Accessibility import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.Build import androidx.compose.material.icons.rounded.Link +import androidx.compose.material.icons.rounded.Notifications import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material3.Button import androidx.compose.material3.Checkbox @@ -66,7 +67,7 @@ fun ProModeSetupScreen( ProModeSetupScreen( state = state, onNavigateBack = onNavigateBack, - onButtonClick = viewModel::onButtonClick, + onStepButtonClick = viewModel::onStepButtonClick, onAssistantClick = viewModel::onAssistantClick, onWatchTutorialClick = { } ) @@ -77,7 +78,7 @@ fun ProModeSetupScreen( fun ProModeSetupScreen( state: State, onNavigateBack: () -> Unit = {}, - onButtonClick: () -> Unit = {}, + onStepButtonClick: () -> Unit = {}, onAssistantClick: () -> Unit = {}, onWatchTutorialClick: () -> Unit = {} ) { @@ -177,7 +178,7 @@ fun ProModeSetupScreen( .padding(horizontal = 16.dp), stepContent, onWatchTutorialClick, - onButtonClick + onStepButtonClick ) } } @@ -300,6 +301,13 @@ private fun getStepContent(step: SystemBridgeSetupStep): StepContent { buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_button) ) + SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_title), + message = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_description), + icon = Icons.Rounded.Notifications, + buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_button) + ) + SystemBridgeSetupStep.DEVELOPER_OPTIONS -> StepContent( title = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_title), message = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_description), @@ -362,6 +370,23 @@ private fun ProModeSetupScreenAccessibilityServicePreview() { } } +@Preview(name = "Notification Permission Step") +@Composable +private fun ProModeSetupScreenNotificationPermissionPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 2, + stepCount = 6, + step = SystemBridgeSetupStep.NOTIFICATION_PERMISSION, + isSetupAssistantChecked = false + ) + ) + ) + } +} + @Preview(name = "Developer Options Step") @Composable private fun ProModeSetupScreenDeveloperOptionsPreview() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt index 79bdf99383..b1dc220ed6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt @@ -28,12 +28,13 @@ class ProModeSetupViewModel @Inject constructor( State.Loading ) - fun onButtonClick() { + fun onStepButtonClick() { viewModelScope.launch { val currentStep = useCase.nextSetupStep.first() when (currentStep) { SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> useCase.enableAccessibilityService() + SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> useCase.requestNotificationPermission() SystemBridgeSetupStep.DEVELOPER_OPTIONS -> useCase.openDeveloperOptions() SystemBridgeSetupStep.WIFI_NETWORK -> useCase.connectWifiNetwork() SystemBridgeSetupStep.WIRELESS_DEBUGGING -> useCase.enableWirelessDebugging() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index a824bb7c50..3f23327092 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -57,6 +57,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( override val nextSetupStep: Flow = combine( accessibilityServiceAdapter.state, + permissionAdapter.isGrantedFlow(Permission.POST_NOTIFICATIONS), systemBridgeSetupController.isDeveloperOptionsEnabled, networkAdapter.isWifiConnected, ::getNextStep @@ -89,6 +90,10 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( permissionAdapter.request(Permission.SHIZUKU) } + override fun requestNotificationPermission() { + permissionAdapter.request(Permission.POST_NOTIFICATIONS) + } + override fun stopSystemBridge() { systemBridgeConnectionManager.stopSystemBridge() } @@ -127,11 +132,13 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( private fun getNextStep( accessibilityServiceState: AccessibilityServiceState, + isNotificationPermissionGranted: Boolean, isDeveloperOptionsEnabled: Boolean, isWifiConnected: Boolean, ): SystemBridgeSetupStep = when { accessibilityServiceState != AccessibilityServiceState.ENABLED -> SystemBridgeSetupStep.ACCESSIBILITY_SERVICE + !isNotificationPermissionGranted -> SystemBridgeSetupStep.NOTIFICATION_PERMISSION !isDeveloperOptionsEnabled -> SystemBridgeSetupStep.DEVELOPER_OPTIONS !isWifiConnected -> SystemBridgeSetupStep.WIFI_NETWORK else -> SystemBridgeSetupStep.WIRELESS_DEBUGGING @@ -155,6 +162,7 @@ interface SystemBridgeSetupUseCase { val shizukuSetupState: Flow fun openShizukuApp() fun requestShizukuPermission() + fun requestNotificationPermission() fun stopSystemBridge() fun enableAccessibilityService() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt new file mode 100644 index 0000000000..cec0a08839 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt @@ -0,0 +1,3 @@ +package io.github.sds100.keymapper.base.system.accessibility + +class SystemBridgeSetupAssistantController() \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 9f440ea9e7..ca0be72bc5 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1639,6 +1639,9 @@ Start service Key Mapper needs to connect to the Android Debug Bridge to start the PRO mode service. + Notification permission + Key Mapper will use notifications to help guide you through the process + Give permission diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt index 3785f9ed0c..ac7725b459 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt @@ -2,9 +2,10 @@ package io.github.sds100.keymapper.sysbridge.service enum class SystemBridgeSetupStep(val stepIndex: Int) { ACCESSIBILITY_SERVICE(stepIndex = 0), - DEVELOPER_OPTIONS(stepIndex = 1), - WIFI_NETWORK(stepIndex = 2), - WIRELESS_DEBUGGING(stepIndex = 3), - ADB_PAIRING(stepIndex = 4), - START_SERVICE(stepIndex = 5) + NOTIFICATION_PERMISSION(stepIndex = 1), + DEVELOPER_OPTIONS(stepIndex = 2), + WIFI_NETWORK(stepIndex = 3), + WIRELESS_DEBUGGING(stepIndex = 4), + ADB_PAIRING(stepIndex = 5), + START_SERVICE(stepIndex = 6) } \ No newline at end of file From addbaf9a70acef991c956ffff05a707a3d5559c2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 13 Aug 2025 00:14:22 +0100 Subject: [PATCH 132/215] #1394 show notification on pro mode starting --- .../AccessibilityServiceController.kt | 8 +- .../base/promode/SystemBridgeSetupUseCase.kt | 9 +- .../BaseAccessibilityServiceController.kt | 53 ++--- .../SystemBridgeSetupAssistantController.kt | 225 +++++++++++++++++- .../AndroidNotificationAdapter.kt | 12 +- .../notifications/NotificationController.kt | 2 + base/src/main/res/drawable/pro_mode.xml | 36 +++ base/src/main/res/values/strings.xml | 5 +- .../io/github/sds100/keymapper/data/Keys.kt | 2 +- .../keymapper/data/PreferenceDefaults.kt | 2 + .../sds100/keymapper/sysbridge/adb/AdbMdns.kt | 2 +- .../service/SystemBridgeSetupController.kt | 68 +++--- .../system/notifications/NotificationModel.kt | 10 +- 13 files changed, 354 insertions(+), 80 deletions(-) create mode 100644 base/src/main/res/drawable/pro_mode.xml diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index aa34bd7212..25676281fc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -5,16 +5,16 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.github.sds100.keymapper.base.actions.PerformActionsUseCaseImpl import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCaseImpl +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase -import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeRecorder import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController +import io.github.sds100.keymapper.base.system.accessibility.SystemBridgeSetupAssistantController import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper class AccessibilityServiceController @AssistedInject constructor( @@ -28,10 +28,10 @@ class AccessibilityServiceController @AssistedInject constructor( fingerprintGesturesSupported: FingerprintGesturesSupportedUseCase, pauseKeyMapsUseCase: PauseKeyMapsUseCase, settingsRepository: PreferenceRepository, - systemBridgeSetupController: SystemBridgeSetupController, keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, inputEventHub: InputEventHub, recordTriggerController: RecordTriggerController, + setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory ) : BaseAccessibilityServiceController( service = service, rerouteKeyEventsControllerFactory = rerouteKeyEventsControllerFactory, @@ -42,10 +42,10 @@ class AccessibilityServiceController @AssistedInject constructor( fingerprintGesturesSupported = fingerprintGesturesSupported, pauseKeyMapsUseCase = pauseKeyMapsUseCase, settingsRepository = settingsRepository, - systemBridgeSetupController = systemBridgeSetupController, keyEventRelayServiceWrapper = keyEventRelayServiceWrapper, inputEventHub = inputEventHub, recordTriggerController = recordTriggerController, + setupAssistantControllerFactory = setupAssistantControllerFactory ) { @AssistedFactory interface Factory { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 3f23327092..f828f7101d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -4,6 +4,7 @@ import android.os.Build import androidx.annotation.RequiresApi import dagger.hilt.android.scopes.ViewModelScoped import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController @@ -41,12 +42,14 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } override val isSetupAssistantEnabled: Flow = - preferences.get(Keys.isProModeSetupAssistantEnabled).map { it ?: false } + preferences.get(Keys.isProModeInteractiveSetupAssistantEnabled).map { + it ?: PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT + } override fun toggleSetupAssistant() { - preferences.update(Keys.isProModeSetupAssistantEnabled) { + preferences.update(Keys.isProModeInteractiveSetupAssistantEnabled) { if (it == null) { - true + !PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT } else { !it } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 1102a3c2b6..273a16a6f2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -30,7 +30,6 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent @@ -63,10 +62,10 @@ abstract class BaseAccessibilityServiceController( private val fingerprintGesturesSupported: FingerprintGesturesSupportedUseCase, private val pauseKeyMapsUseCase: PauseKeyMapsUseCase, private val settingsRepository: PreferenceRepository, - private val systemBridgeSetupController: SystemBridgeSetupController, private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, private val inputEventHub: InputEventHub, private val recordTriggerController: RecordTriggerController, + private val setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory ) { companion object { private const val DEFAULT_NOTIFICATION_TIMEOUT = 200L @@ -108,6 +107,13 @@ abstract class BaseAccessibilityServiceController( val accessibilityNodeRecorder = accessibilityNodeRecorderFactory.create(service) + private val setupAssistantController: SystemBridgeSetupAssistantController? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setupAssistantControllerFactory.create(service.lifecycleScope, service) + } else { + null + } + val isPaused: StateFlow = pauseKeyMapsUseCase.isPaused .stateIn(service.lifecycleScope, SharingStarted.Eagerly, false) @@ -342,6 +348,10 @@ abstract class BaseAccessibilityServiceController( CALLBACK_ID_ACCESSIBILITY_SERVICE, relayServiceCallback, ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setupAssistantController?.onServiceConnected() + } } open fun onDestroy() { @@ -349,6 +359,10 @@ abstract class BaseAccessibilityServiceController( keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_ACCESSIBILITY_SERVICE) accessibilityNodeRecorder.teardown() rerouteKeyEventsController.teardown() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setupAssistantController?.teardown() + } } open fun onConfigurationChanged(newConfig: Configuration) { @@ -389,6 +403,10 @@ abstract class BaseAccessibilityServiceController( open fun onAccessibilityEvent(event: AccessibilityEvent) { accessibilityNodeRecorder.onAccessibilityEvent(event) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setupAssistantController?.onAccessibilityEvent(event) + } + if (changeImeOnInputFocusFlow.value) { val focussedNode = service.findFocussedNode(AccessibilityNodeInfo.FOCUS_INPUT) @@ -405,37 +423,6 @@ abstract class BaseAccessibilityServiceController( } } } - - // TODO only run this code, and listen for these events if searching for a pairing code. Check that package name is settings app. -// if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { -// val pairingCodeRegex = Regex("^\\d{6}$") -// val portRegex = Regex(".*:([0-9]{1,5})") -// val pairingCodeNode = -// service.rootInActiveWindow.findNodeRecursively { -// it.text != null && pairingCodeRegex.matches(it.text) -// } -// -// val portNode = service.rootInActiveWindow.findNodeRecursively { -// it.text != null && portRegex.matches(it.text) -// } -// -// if (pairingCodeNode != null && portNode != null) { -// val pairingCode = pairingCodeNode.text?.toString()?.toIntOrNull() -// val port = portNode.text?.split(":")?.last()?.toIntOrNull() -// Timber.e("PAIRING CODE = $pairingCode") -// Timber.e("PORT = $port") -// -// if (pairingCode != null && port != null) { -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { -// service.lifecycleScope.launch { -// systemBridgeSetupController.pairWirelessAdb(port, pairingCode) -// delay(1000) -// systemBridgeSetupController.startWithAdb() -// } -// } -// } -// } -// } } fun onFingerprintGesture(type: FingerprintGestureType) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt index cec0a08839..dc193c30f0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt @@ -1,3 +1,226 @@ package io.github.sds100.keymapper.base.system.accessibility -class SystemBridgeSetupAssistantController() \ No newline at end of file +import android.os.Build +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationManagerCompat +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsUseCase +import io.github.sds100.keymapper.base.system.notifications.NotificationController +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.PreferenceDefaults +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep +import io.github.sds100.keymapper.system.notifications.NotificationChannelModel +import io.github.sds100.keymapper.system.notifications.NotificationModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import timber.log.Timber + +@Suppress("KotlinConstantConditions") +@RequiresApi(Build.VERSION_CODES.Q) +class SystemBridgeSetupAssistantController @AssistedInject constructor( + @Assisted + private val coroutineScope: CoroutineScope, + + @Assisted + private val accessibilityService: BaseAccessibilityService, + + private val manageNotifications: ManageNotificationsUseCase, + private val setupController: SystemBridgeSetupController, + private val preferenceRepository: PreferenceRepository, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + resourceProvider: ResourceProvider +) : ResourceProvider by resourceProvider { + @AssistedFactory + interface Factory { + fun create( + coroutineScope: CoroutineScope, + accessibilityService: BaseAccessibilityService + ): SystemBridgeSetupAssistantController + } + + companion object { + /** + * The max time to spend searching for an accessibility node. + */ + const val NODE_SEARCH_TIMEOUT = 30000L + + private val PAIRING_CODE_REGEX = Regex("^\\d{6}$") + private val PORT_REGEX = Regex(".*:([0-9]{1,5})") + } + + private enum class InteractionStep { + WIRELESS_DEBUGGING_SWITCH, + PAIR_DEVICE, + } + + private val isInteractive: StateFlow = + preferenceRepository.get(Keys.isProModeInteractiveSetupAssistantEnabled) + .map { it ?: PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT } + .stateIn( + coroutineScope, + SharingStarted.Lazily, + PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT + ) + + private var interactionStep: InteractionStep? = null + + /** + * This job will wait for the interaction timeout and then + * ask the user to do the steps manually if it failed to do them automatically. + */ + private var interactionTimeoutJob: Job? = null + + fun onServiceConnected() { + createNotificationChannel() + + coroutineScope.launch { + setupController.startSetupAssistantRequest.collect(::startSetupStep) + } + } + + private fun createNotificationChannel() { + val notificationChannel = NotificationChannelModel( + id = NotificationController.CHANNEL_SETUP_ASSISTANT, + name = getString(R.string.pro_mode_setup_assistant_notification_channel), + importance = NotificationManagerCompat.IMPORTANCE_MAX + ) + manageNotifications.createChannel(notificationChannel) + } + + fun teardown() { + // TODO stop showing any notifications + interactionStep = null + interactionTimeoutJob?.cancel() + interactionTimeoutJob = null + } + + fun onAccessibilityEvent(event: AccessibilityEvent) { + // Do not do anything if there is no node to find. + if (interactionStep == null) { + return + } + + // Do not do anything if the interactive setup assistant is disabled + if (!isInteractive.value) { + return + } + + if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { + val step = interactionStep ?: return + val rootNode = accessibilityService.rootInActiveWindow ?: return + + if (rootNode.packageName != "com.android.settings") { + return + } + + doInteractiveStep(step, rootNode) + } + } + + private fun doInteractiveStep(step: InteractionStep, rootNode: AccessibilityNodeInfo) { + when (step) { + InteractionStep.WIRELESS_DEBUGGING_SWITCH -> TODO() + InteractionStep.PAIR_DEVICE -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + doPairingInteractiveStep(rootNode) + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun doPairingInteractiveStep(rootNode: AccessibilityNodeInfo) { + val pairingCodeText = findPairingCodeText(rootNode) + val portText = findPortText(rootNode) + + if (pairingCodeText != null && portText != null) { + val pairingCode = pairingCodeText.toIntOrNull() + val port = portText.split(":").last().toIntOrNull() + + if (pairingCode != null && port != null) { + coroutineScope.launch { + setupController.pairWirelessAdb(port, pairingCode).onSuccess { + setupController.startWithAdb() + + val isStarted = try { + withTimeout(3000L) { + systemBridgeConnectionManager.isConnected.first { it } + } + } catch (e: TimeoutCancellationException) { + false + } + + if (isStarted) { + val notification = NotificationModel( + id = NotificationController.ID_SETUP_ASSISTANT, + channel = NotificationController.CHANNEL_SETUP_ASSISTANT, + title = getString(R.string.pro_mode_setup_notification_started_success_title), + text = getString(R.string.pro_mode_setup_notification_started_success_text), + icon = R.drawable.pro_mode, + onGoing = false, + showOnLockscreen = false, + autoCancel = true, + bigTextStyle = true + ) + manageNotifications.show(notification) + } else { + // TODO Show notification + Timber.w("Failed to start system bridge after pairing.") + } + } + } + } + } + } + + private fun findPairingCodeText(rootNode: AccessibilityNodeInfo): String? { + return rootNode.findNodeRecursively { + it.text != null && PAIRING_CODE_REGEX.matches(it.text) + }?.text?.toString() + } + + private fun findPortText(rootNode: AccessibilityNodeInfo): String? { + return rootNode.findNodeRecursively { + it.text != null && PORT_REGEX.matches(it.text) + }?.text?.toString() + } + + private fun startSetupStep(step: SystemBridgeSetupStep) { + Timber.d("Starting setup assistant step: $step") + + when (step) { + SystemBridgeSetupStep.DEVELOPER_OPTIONS -> { + + } + + SystemBridgeSetupStep.WIRELESS_DEBUGGING -> {} + + SystemBridgeSetupStep.ADB_PAIRING -> interactionStep = InteractionStep.PAIR_DEVICE + + SystemBridgeSetupStep.START_SERVICE -> {} + else -> {} // Do nothing + } + + + // TODO if finding pairing node does not work, show a notification asking for the pairing code. + // TODO do this in the timeout job too + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt index dbae0c9ee3..0ae0f6286e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt @@ -85,13 +85,13 @@ class AndroidNotificationAdapter @Inject constructor( override fun createChannel(channel: NotificationChannelModel) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - manager.createNotificationChannel( - NotificationChannel( - channel.id, - channel.name, - channel.importance, - ), + val androidChannel = NotificationChannel( + channel.id, + channel.name, + channel.importance, ) + + manager.createNotificationChannel(androidChannel) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 4a25132dae..aabd26a6f3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -55,6 +55,7 @@ class NotificationController @Inject constructor( private const val ID_KEYBOARD_HIDDEN = 747 private const val ID_TOGGLE_MAPPINGS = 231 private const val ID_TOGGLE_KEYBOARD = 143 + const val ID_SETUP_ASSISTANT = 144 // private const val ID_FEATURE_ASSISTANT_TRIGGER = 900 private const val ID_FEATURE_FLOATING_BUTTONS = 901 @@ -64,6 +65,7 @@ class NotificationController @Inject constructor( const val CHANNEL_KEYBOARD_HIDDEN = "channel_warning_keyboard_hidden" const val CHANNEL_TOGGLE_KEYBOARD = "channel_toggle_keymapper_keyboard" const val CHANNEL_NEW_FEATURES = "channel_new_features" + const val CHANNEL_SETUP_ASSISTANT = "channel_setup_assistant" @Deprecated("Removed in 2.0. This channel shouldn't exist") private const val CHANNEL_ID_WARNINGS = "channel_warnings" diff --git a/base/src/main/res/drawable/pro_mode.xml b/base/src/main/res/drawable/pro_mode.xml new file mode 100644 index 0000000000..a0062fc0ed --- /dev/null +++ b/base/src/main/res/drawable/pro_mode.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index ca0be72bc5..52c001493b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1614,7 +1614,7 @@ Setup wizard Step %d of %d - Use setup assistant + Use interactive setup assistant Automatically interact with settings Enable accessibility service first Watch tutorial @@ -1643,5 +1643,8 @@ Key Mapper will use notifications to help guide you through the process Give permission + Setup assistant + PRO mode started! + PRO mode is now running and you can remap more buttons diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index df46a9cb7c..be74b2a00f 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -114,6 +114,6 @@ object Keys { val isProModeWarningUnderstood = booleanPreferencesKey("key_is_pro_mode_warning_understood") - val isProModeSetupAssistantEnabled = + val isProModeInteractiveSetupAssistantEnabled = booleanPreferencesKey("key_is_pro_mode_setup_assistant_enabled") } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt index 187f4ef34e..cc52283ee7 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt @@ -14,4 +14,6 @@ object PreferenceDefaults { const val REPEAT_RATE = 50 const val SEQUENCE_TRIGGER_TIMEOUT = 1000 const val HOLD_DOWN_DURATION = 1000 + + const val PRO_MODE_INTERACTIVE_SETUP_ASSISTANT = true } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt index fed0ac66a7..06477219b1 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt @@ -29,7 +29,7 @@ internal class AdbMdns( private var serviceName: String? = null private val nsdManager: NsdManager = ctx.getSystemService(NsdManager::class.java) - private val _port: MutableStateFlow = MutableStateFlow(null) + private val _port: MutableStateFlow = MutableStateFlow(null) val port: StateFlow = _port.asStateFlow() private val discoveryListener: NsdManager.DiscoveryListener = diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index e64fb3a6f9..faeb62e853 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -17,7 +17,10 @@ import androidx.annotation.RequiresApi import com.topjohnwu.superuser.Shell import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils +import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.sysbridge.BuildConfig import io.github.sds100.keymapper.sysbridge.IShizukuStarterService import io.github.sds100.keymapper.sysbridge.adb.AdbClient @@ -32,10 +35,12 @@ import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import rikka.shizuku.Shizuku import timber.log.Timber @@ -61,9 +66,12 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override val isDeveloperOptionsEnabled: MutableStateFlow = MutableStateFlow(getDeveloperOptionsEnabled()) + override val startSetupAssistantRequest: MutableSharedFlow = + MutableSharedFlow() + val sb = StringBuilder() - private val adbConnectMdns: AdbMdns? + private var adbConnectMdns: AdbMdns? = null private val scriptPath: String by lazy { SystemBridgeStarter.writeSdcardFiles(ctx) } private val apkPath = ctx.applicationInfo.sourceDir @@ -94,14 +102,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } - init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - adbConnectMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) - } else { - adbConnectMdns = null - } - } - override fun startWithRoot() { try { if (Shell.isAppGrantedRoot() != true) { @@ -151,18 +151,17 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) override fun startWithAdb() { - // TODO kill the current service before starting it? - + // TODO kill the current system bridge before starting it? if (adbConnectMdns == null) { - return + adbConnectMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) } coroutineScope.launch(Dispatchers.IO) { - adbConnectMdns.start() + adbConnectMdns!!.start() val host = "127.0.0.1" - val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } + val port = withTimeout(1000L) { adbConnectMdns!!.port.first { it != null } } if (port == null) { return@launch @@ -229,7 +228,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } - adbConnectMdns.stop() + adbConnectMdns!!.stop() } } @@ -253,9 +252,10 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } @RequiresApi(Build.VERSION_CODES.R) - override fun pairWirelessAdb(port: Int, code: Int) { - // TODO move this to AdbManager class - coroutineScope.launch(Dispatchers.IO) { + override suspend fun pairWirelessAdb(port: Int, code: Int): KMResult { + // TODO move this to AdbManager class and only allow one job at a time. + // TODO if a job is already running then this should return an error + return withContext(Dispatchers.IO) { val host = "127.0.0.1" val key = try { @@ -263,19 +263,19 @@ class SystemBridgeSetupControllerImpl @Inject constructor( PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), "keymapper", ) - } catch (e: Throwable) { + } catch (e: Exception) { e.printStackTrace() - return@launch + return@withContext KMError.Exception(e) } - AdbPairingClient(host, port, code.toString(), key).runCatching { - start() - }.onFailure { - Timber.d("Pairing failed: $it") -// handleResult(false, it) - }.onSuccess { - Timber.d("Pairing success") -// handleResult(it, null) + try { + AdbPairingClient(host, port, code.toString(), key).start() + Timber.i("Successfully paired with wireless ADB on port $port with code $code") + return@withContext Success(Unit) + } catch (e: Exception) { + e.printStackTrace() + Timber.e("Failed to pair with wireless ADB on port $port with code $code: $e") + return@withContext KMError.Exception(e) } } @@ -345,6 +345,11 @@ class SystemBridgeSetupControllerImpl @Inject constructor( "toggle_adb_wireless" ) } + + // TODO send correct step + coroutineScope.launch { + startSetupAssistantRequest.emit(SystemBridgeSetupStep.ADB_PAIRING) + } } fun updateDeveloperOptionsEnabled() { @@ -363,13 +368,18 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeSetupController { + /** + * The setup assistant should be launched for the given step. + */ + val startSetupAssistantRequest: Flow + val isDeveloperOptionsEnabled: Flow fun enableDeveloperOptions() fun enableWirelessDebugging() @RequiresApi(Build.VERSION_CODES.R) - fun pairWirelessAdb(port: Int, code: Int) + suspend fun pairWirelessAdb(port: Int, code: Int): KMResult fun startWithRoot() fun startWithShizuku() diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt index 30bd5c1cb3..88867fe560 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.system.notifications import androidx.annotation.DrawableRes +import androidx.core.app.NotificationCompat data class NotificationModel( @@ -15,8 +16,15 @@ data class NotificationModel( val onClickAction: NotificationIntentType? = null, val showOnLockscreen: Boolean, val onGoing: Boolean, - val priority: Int, + /** + * On Android Oreo and newer this does nothing because the channel priority is used. + */ + val priority: Int = NotificationCompat.PRIORITY_DEFAULT, val actions: List = emptyList(), + + /** + * Clicking on the notification will automatically dismiss it. + */ val autoCancel: Boolean = false, val bigTextStyle: Boolean = false, ) { From 1c3fed3eea0292bec7b6e60e8aa13f8c0d6c8e14 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 13 Aug 2025 00:16:18 +0100 Subject: [PATCH 133/215] add field to create a silent notification --- .../base/system/notifications/AndroidNotificationAdapter.kt | 2 ++ .../sds100/keymapper/system/notifications/NotificationModel.kt | 1 + 2 files changed, 3 insertions(+) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt index 0ae0f6286e..1fa74b2a49 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt @@ -74,6 +74,8 @@ class AndroidNotificationAdapter @Inject constructor( ), ) } + + setSilent(notification.silent) } manager.notify(notification.id, builder.build()) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt index 88867fe560..e49dadbb2d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt @@ -27,6 +27,7 @@ data class NotificationModel( */ val autoCancel: Boolean = false, val bigTextStyle: Boolean = false, + val silent: Boolean = false ) { data class Action(val text: String, val intentType: NotificationIntentType) } From df26089989a9bfaa28e84e8bba7c9b0b46770159 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 13 Aug 2025 00:18:33 +0100 Subject: [PATCH 134/215] #1394 auto dismiss success notification --- .../accessibility/SystemBridgeSetupAssistantController.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt index dc193c30f0..e4d589ab19 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt @@ -24,6 +24,7 @@ import io.github.sds100.keymapper.system.notifications.NotificationModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first @@ -181,6 +182,10 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( bigTextStyle = true ) manageNotifications.show(notification) + + delay(5000) + + manageNotifications.dismiss(notification.id) } else { // TODO Show notification Timber.w("Failed to start system bridge after pairing.") From 1d21aac1d21181398a781f316231cca75116c429 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 13 Aug 2025 01:15:31 +0100 Subject: [PATCH 135/215] #1394 open key mapper after successfully starting system bridge --- .../AccessibilityServiceController.kt | 2 +- .../SystemBridgeSetupAssistantController.kt | 79 ++++++++++++++----- .../BaseAccessibilityServiceController.kt | 1 + 3 files changed, 61 insertions(+), 21 deletions(-) rename base/src/main/java/io/github/sds100/keymapper/base/{system/accessibility => promode}/SystemBridgeSetupAssistantController.kt (74%) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 25676281fc..292078944d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -9,10 +9,10 @@ import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase +import io.github.sds100.keymapper.base.promode.SystemBridgeSetupAssistantController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeRecorder import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController -import io.github.sds100.keymapper.base.system.accessibility.SystemBridgeSetupAssistantController import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt similarity index 74% rename from base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index e4d589ab19..8039eabe8b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -1,17 +1,22 @@ -package io.github.sds100.keymapper.base.system.accessibility +package io.github.sds100.keymapper.base.promode +import android.app.ActivityManager import android.os.Build import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import androidx.annotation.RequiresApi import androidx.core.app.NotificationManagerCompat +import androidx.core.content.getSystemService import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityService +import io.github.sds100.keymapper.base.system.accessibility.findNodeRecursively import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsUseCase import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.KeyMapperClassProvider import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults @@ -42,11 +47,11 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( @Assisted private val accessibilityService: BaseAccessibilityService, - private val manageNotifications: ManageNotificationsUseCase, private val setupController: SystemBridgeSetupController, private val preferenceRepository: PreferenceRepository, private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val keyMapperClassProvider: KeyMapperClassProvider, resourceProvider: ResourceProvider ) : ResourceProvider by resourceProvider { @AssistedFactory @@ -67,17 +72,20 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private val PORT_REGEX = Regex(".*:([0-9]{1,5})") } + private enum class InteractionStep { WIRELESS_DEBUGGING_SWITCH, PAIR_DEVICE, } + private val activityManager: ActivityManager = accessibilityService.getSystemService()!! + private val isInteractive: StateFlow = preferenceRepository.get(Keys.isProModeInteractiveSetupAssistantEnabled) .map { it ?: PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT } .stateIn( coroutineScope, - SharingStarted.Lazily, + SharingStarted.Companion.Lazily, PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT ) @@ -99,7 +107,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private fun createNotificationChannel() { val notificationChannel = NotificationChannelModel( - id = NotificationController.CHANNEL_SETUP_ASSISTANT, + id = NotificationController.Companion.CHANNEL_SETUP_ASSISTANT, name = getString(R.string.pro_mode_setup_assistant_notification_channel), importance = NotificationManagerCompat.IMPORTANCE_MAX ) @@ -107,7 +115,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } fun teardown() { - // TODO stop showing any notifications + dismissNotification() interactionStep = null interactionTimeoutJob?.cancel() interactionTimeoutJob = null @@ -149,6 +157,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( @RequiresApi(Build.VERSION_CODES.R) private fun doPairingInteractiveStep(rootNode: AccessibilityNodeInfo) { + val pairingCodeText = findPairingCodeText(rootNode) val portText = findPortText(rootNode) @@ -170,25 +179,19 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } if (isStarted) { - val notification = NotificationModel( - id = NotificationController.ID_SETUP_ASSISTANT, - channel = NotificationController.CHANNEL_SETUP_ASSISTANT, - title = getString(R.string.pro_mode_setup_notification_started_success_title), - text = getString(R.string.pro_mode_setup_notification_started_success_text), - icon = R.drawable.pro_mode, - onGoing = false, - showOnLockscreen = false, - autoCancel = true, - bigTextStyle = true + showNotification( + getString(R.string.pro_mode_setup_notification_started_success_title), + getString(R.string.pro_mode_setup_notification_started_success_text) ) - manageNotifications.show(notification) + + getKeyMapperAppTask()?.moveToFront() delay(5000) - manageNotifications.dismiss(notification.id) + dismissNotification() } else { // TODO Show notification - Timber.w("Failed to start system bridge after pairing.") + Timber.Forest.w("Failed to start system bridge after pairing.") } } } @@ -196,6 +199,28 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } } + private fun showNotification(title: String, text: String) { + val notification = NotificationModel( + // Use the same notification id for all so they overwrite each other. + id = NotificationController.Companion.ID_SETUP_ASSISTANT, + channel = NotificationController.Companion.CHANNEL_SETUP_ASSISTANT, + title = title, + text = text, + icon = R.drawable.pro_mode, + onGoing = false, + showOnLockscreen = false, + autoCancel = true, + bigTextStyle = true, + // Must not be silent so it is shown as a heads up notification + silent = false + ) + manageNotifications.show(notification) + } + + private fun dismissNotification() { + manageNotifications.dismiss(NotificationController.Companion.ID_SETUP_ASSISTANT) + } + private fun findPairingCodeText(rootNode: AccessibilityNodeInfo): String? { return rootNode.findNodeRecursively { it.text != null && PAIRING_CODE_REGEX.matches(it.text) @@ -209,7 +234,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } private fun startSetupStep(step: SystemBridgeSetupStep) { - Timber.d("Starting setup assistant step: $step") + Timber.i("Starting setup assistant step: $step") when (step) { SystemBridgeSetupStep.DEVELOPER_OPTIONS -> { @@ -218,7 +243,15 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( SystemBridgeSetupStep.WIRELESS_DEBUGGING -> {} - SystemBridgeSetupStep.ADB_PAIRING -> interactionStep = InteractionStep.PAIR_DEVICE + SystemBridgeSetupStep.ADB_PAIRING -> { + showNotification( + "Pairing automatically", + "Searching for pairing code and port..." + ) + + + interactionStep = InteractionStep.PAIR_DEVICE + } SystemBridgeSetupStep.START_SERVICE -> {} else -> {} // Do nothing @@ -228,4 +261,10 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( // TODO if finding pairing node does not work, show a notification asking for the pairing code. // TODO do this in the timeout job too } + + private fun getKeyMapperAppTask(): ActivityManager.AppTask? { + val task = activityManager.appTasks + .firstOrNull { it.taskInfo.topActivity?.className == keyMapperClassProvider.getMainActivity().name } + return task + } } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 273a16a6f2..b7e54b5b4f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -21,6 +21,7 @@ import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent +import io.github.sds100.keymapper.base.promode.SystemBridgeSetupAssistantController import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.common.utils.firstBlocking From ea55c7dc79d05a9d0db335a2285c3f4013643d9e Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 13 Aug 2025 01:44:48 +0100 Subject: [PATCH 136/215] #1394 automatically click the button to pair with pairing code --- .../SystemBridgeSetupAssistantController.kt | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index 8039eabe8b..a4cc9ce981 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -116,9 +116,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( fun teardown() { dismissNotification() - interactionStep = null - interactionTimeoutJob?.cancel() - interactionTimeoutJob = null + stopInteracting() } fun onAccessibilityEvent(event: AccessibilityEvent) { @@ -157,7 +155,6 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( @RequiresApi(Build.VERSION_CODES.R) private fun doPairingInteractiveStep(rootNode: AccessibilityNodeInfo) { - val pairingCodeText = findPairingCodeText(rootNode) val portText = findPortText(rootNode) @@ -167,38 +164,52 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( if (pairingCode != null && port != null) { coroutineScope.launch { - setupController.pairWirelessAdb(port, pairingCode).onSuccess { - setupController.startWithAdb() - - val isStarted = try { - withTimeout(3000L) { - systemBridgeConnectionManager.isConnected.first { it } - } - } catch (e: TimeoutCancellationException) { - false - } - - if (isStarted) { - showNotification( - getString(R.string.pro_mode_setup_notification_started_success_title), - getString(R.string.pro_mode_setup_notification_started_success_text) - ) - - getKeyMapperAppTask()?.moveToFront() - - delay(5000) - - dismissNotification() - } else { - // TODO Show notification - Timber.Forest.w("Failed to start system bridge after pairing.") - } - } + onPairingCodeFound(port, pairingCode) } } + } else { + clickPairWithCodeButton(rootNode) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private suspend fun onPairingCodeFound(port: Int, pairingCode: Int) { + setupController.pairWirelessAdb(port, pairingCode).onSuccess { + setupController.startWithAdb() + + stopInteracting() + + val isStarted = try { + withTimeout(3000L) { + systemBridgeConnectionManager.isConnected.first { it } + } + } catch (e: TimeoutCancellationException) { + false + } + + if (isStarted) { + showNotification( + getString(R.string.pro_mode_setup_notification_started_success_title), + getString(R.string.pro_mode_setup_notification_started_success_text) + ) + + getKeyMapperAppTask()?.moveToFront() + + delay(5000) + + dismissNotification() + } else { + // TODO Show notification + Timber.w("Failed to start system bridge after pairing.") + } } } + private fun clickPairWithCodeButton(rootNode: AccessibilityNodeInfo) { + rootNode.findNodeRecursively { it.className == "androidx.recyclerview.widget.RecyclerView" } + ?.getChild(3)?.performAction(AccessibilityNodeInfo.ACTION_CLICK) + } + private fun showNotification(title: String, text: String) { val notification = NotificationModel( // Use the same notification id for all so they overwrite each other. @@ -262,6 +273,12 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( // TODO do this in the timeout job too } + private fun stopInteracting() { + interactionStep = null + interactionTimeoutJob?.cancel() + interactionTimeoutJob = null + } + private fun getKeyMapperAppTask(): ActivityManager.AppTask? { val task = activityManager.appTasks .firstOrNull { it.taskInfo.topActivity?.className == keyMapperClassProvider.getMainActivity().name } From 6709a8fc55786e247765e0694e00850b9fd607f1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 23 Aug 2025 17:14:25 +0100 Subject: [PATCH 137/215] #1394 refactor adb starter code --- .../sds100/keymapper/base/BaseMainActivity.kt | 2 +- .../sds100/keymapper/base/logging/LogUtils.kt | 5 +- .../SystemBridgeSetupAssistantController.kt | 6 +- .../base/promode/SystemBridgeSetupUseCase.kt | 5 +- data/build.gradle.kts | 1 - .../sysbridge/IShizukuStarterService.aidl | 3 +- .../sysbridge/SystemBridgeHiltModule.kt | 6 + .../keymapper/sysbridge/adb/AdbClient.kt | 18 +- .../keymapper/sysbridge/adb/AdbError.kt | 11 + .../keymapper/sysbridge/adb/AdbManager.kt | 120 ++++++++ .../sds100/keymapper/sysbridge/ktx/Context.kt | 12 - .../service/SystemBridgeSetupController.kt | 277 ++--------------- .../shizuku/ShizukuStarterService.kt | 30 +- .../sysbridge/starter/SystemBridgeStarter.kt | 289 +++++++++++++----- 14 files changed, 415 insertions(+), 370 deletions(-) create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt create mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt delete mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Context.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 0a269f0107..af493bfc97 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -210,7 +210,7 @@ abstract class BaseMainActivity : AppCompatActivity() { permissionAdapter.onPermissionsChanged() serviceAdapter.invalidateState() suAdapter.invalidateIsRooted() - systemBridgeSetupController.updateDeveloperOptionsEnabled() + systemBridgeSetupController.invalidateSettings() networkAdapter.invalidateState() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt index 93b19ffaad..e927604002 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.logging +import android.annotation.SuppressLint import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.system.files.FileUtils import java.text.SimpleDateFormat @@ -7,8 +8,8 @@ import java.util.Date import java.util.Locale object LogUtils { - val DATE_FORMAT - get() = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) + @SuppressLint("ConstantLocale") + val DATE_FORMAT = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) fun createLogFileName(): String { val formattedDate = FileUtils.createFileDate() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index a4cc9ce981..d97b64f8ef 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -206,8 +206,10 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } private fun clickPairWithCodeButton(rootNode: AccessibilityNodeInfo) { - rootNode.findNodeRecursively { it.className == "androidx.recyclerview.widget.RecyclerView" } - ?.getChild(3)?.performAction(AccessibilityNodeInfo.ACTION_CLICK) + rootNode + .findNodeRecursively { it.className == "androidx.recyclerview.widget.RecyclerView" } + ?.getChild(3) + ?.performAction(AccessibilityNodeInfo.ACTION_CLICK) } private fun showNotification(title: String, text: String) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index f828f7101d..1cccb84e08 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -63,6 +63,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( permissionAdapter.isGrantedFlow(Permission.POST_NOTIFICATIONS), systemBridgeSetupController.isDeveloperOptionsEnabled, networkAdapter.isWifiConnected, + systemBridgeSetupController.isWirelessDebuggingEnabled, ::getNextStep ) @@ -138,13 +139,15 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( isNotificationPermissionGranted: Boolean, isDeveloperOptionsEnabled: Boolean, isWifiConnected: Boolean, + isWirelessDebuggingEnabled: Boolean ): SystemBridgeSetupStep = when { accessibilityServiceState != AccessibilityServiceState.ENABLED -> SystemBridgeSetupStep.ACCESSIBILITY_SERVICE !isNotificationPermissionGranted -> SystemBridgeSetupStep.NOTIFICATION_PERMISSION !isDeveloperOptionsEnabled -> SystemBridgeSetupStep.DEVELOPER_OPTIONS !isWifiConnected -> SystemBridgeSetupStep.WIFI_NETWORK - else -> SystemBridgeSetupStep.WIRELESS_DEBUGGING + !isWirelessDebuggingEnabled -> SystemBridgeSetupStep.WIRELESS_DEBUGGING + else -> SystemBridgeSetupStep.ADB_PAIRING } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index ca419f4ad6..422f713ae5 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -36,7 +36,6 @@ android { } room { - schemaDirectory("$projectDir/schemas") } } diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl index ee6c6b6bd3..14e94f98ce 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl @@ -3,6 +3,5 @@ package io.github.sds100.keymapper.sysbridge; interface IShizukuStarterService { void destroy() = 16777114; // Destroy method defined by Shizuku server - // Make it oneway so that an exception isn't thrown when the method kills itself at the end - oneway void startSystemBridge(String scriptPath, String apkPath, String libPath, String packageName) = 1; + String executeCommand(String command) = 1; } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt index 9c2de4a473..d5bd0430b8 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt @@ -4,6 +4,8 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import io.github.sds100.keymapper.sysbridge.adb.AdbManager +import io.github.sds100.keymapper.sysbridge.adb.AdbManagerImpl import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController @@ -21,4 +23,8 @@ abstract class SystemBridgeHiltModule { @Singleton @Binds abstract fun bindSystemBridgeManager(impl: SystemBridgeConnectionManagerImpl): SystemBridgeConnectionManager + + @Singleton + @Binds + abstract fun bindAdbManager(impl: AdbManagerImpl): AdbManager } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt index a0839d3841..adb27cb8dd 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt @@ -4,7 +4,6 @@ import android.os.Build import androidx.annotation.RequiresApi import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_SIGNATURE -import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_TOKEN import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_AUTH import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CLSE import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CNXN @@ -53,9 +52,6 @@ internal class AdbClient(private val host: String, private val port: Int, privat 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 @@ -69,7 +65,6 @@ internal class AdbClient(private val host: String, private val port: Int, privat 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() @@ -79,10 +74,12 @@ internal class AdbClient(private val host: String, private val port: Int, privat } } - if (message.command != A_CNXN) error("not A_CNXN") + if (message.command != A_CNXN) { + error("not A_CNXN") + } } - fun shellCommand(command: String, listener: ((ByteArray) -> Unit)?) { + fun shellCommand(command: String): ByteArray { val localId = 1 write(A_OPEN, localId, 0, "shell:$command") @@ -94,7 +91,7 @@ internal class AdbClient(private val host: String, private val port: Int, privat val remoteId = message.arg0 if (message.command == A_WRTE) { if (message.data_length > 0) { - listener?.invoke(message.data!!) + return message.data!! } write(A_OKAY, localId, remoteId) } else if (message.command == A_CLSE) { @@ -115,6 +112,8 @@ internal class AdbClient(private val host: String, private val port: Int, privat error("not A_OKAY or A_CLSE") } } + + error("No response from adb?") } private fun write(command: Int, arg0: Int, arg1: Int, data: ByteArray? = null) = write( @@ -137,7 +136,8 @@ internal class AdbClient(private val host: String, private val port: Int, privat } private fun read(): AdbMessage { - val buffer = ByteBuffer.allocate(AdbMessage.Companion.HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN) + val buffer = + ByteBuffer.allocate(AdbMessage.Companion.HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN) inputStream.readFully(buffer.array(), 0, 24) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt new file mode 100644 index 0000000000..501953b469 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt @@ -0,0 +1,11 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import io.github.sds100.keymapper.common.utils.KMError + +sealed class AdbError : KMError() { + data object Unpaired : AdbError() + data object PairingError : AdbError() + data object ServerNotFound : AdbError() + data object KeyCreationError : AdbError() + data class Unknown(val exception: kotlin.Exception) : AdbError() +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt new file mode 100644 index 0000000000..f896c3c9a8 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt @@ -0,0 +1,120 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import android.content.Context +import android.os.Build +import android.preference.PreferenceManager +import androidx.annotation.RequiresApi +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.success +import io.github.sds100.keymapper.common.utils.then +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + + +@RequiresApi(Build.VERSION_CODES.R) +@Singleton +class AdbManagerImpl @Inject constructor( + @ApplicationContext private val ctx: Context, +) : AdbManager { + companion object { + private const val LOCALHOST = "127.0.0.1" + } + + private val commandMutex: Mutex = Mutex() + private val pairMutex: Mutex = Mutex() + private val adbConnectMdns: AdbMdns by lazy { AdbMdns(ctx, AdbServiceType.TLS_CONNECT) } + private var client: AdbClient? = null + + override suspend fun executeCommand(command: String): KMResult { + val result = withContext(Dispatchers.IO) { + return@withContext pairMutex.withLock { + adbConnectMdns.start() + + if (client == null) { + + val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } + + if (port == null) { + return@withLock AdbError.ServerNotFound + } + + val adbKey = getAdbKey() + + when (adbKey) { + is KMError -> return@withLock adbKey + is Success -> client = AdbClient(LOCALHOST, port, adbKey.value) + } + } + + return@withLock with(client!!) { + connect() + try { + client!!.shellCommand(command).success() + } catch (e: Exception) { + Timber.e(e) + AdbError.Unknown(e) + } + }.then { String(it).success() } + } + } + + adbConnectMdns.stop() + + return result + } + + override suspend fun pair(port: Int, code: Int): KMResult { + return pairMutex.withLock { + return@withLock getAdbKey().then { key -> + val pairingClient = AdbPairingClient(LOCALHOST, port, code.toString(), key) + + with(pairingClient) { + try { + withContext(Dispatchers.IO) { + start() + } + Timber.i("Successfully paired with wireless ADB on port $port with code $code") + Success(Unit) + } catch (e: Exception) { + e.printStackTrace() + Timber.e("Failed to pair with wireless ADB on port $port with code $code: $e") + AdbError.PairingError + } + } + } + } + } + + private fun getAdbKey(): KMResult { + try { + return AdbKey( + PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), + "keymapper", + ).success() + } catch (e: Throwable) { + Timber.e(e) + return AdbError.KeyCreationError + } + } +} + +interface AdbManager { + /** + * Execute an ADB command + */ + @RequiresApi(Build.VERSION_CODES.R) + suspend fun executeCommand(command: String): KMResult + + @RequiresApi(Build.VERSION_CODES.R) + suspend fun pair(port: Int, code: Int): KMResult +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Context.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Context.kt deleted file mode 100644 index 201818b49a..0000000000 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Context.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.sds100.keymapper.sysbridge.ktx - -import android.content.Context -import android.os.Build - -fun Context.createDeviceProtectedStorageContextCompat(): Context { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - createDeviceProtectedStorageContext() - } else { - this - } -} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index faeb62e853..d31aeb43d8 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -5,45 +5,22 @@ import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.ServiceConnection import android.os.Build -import android.os.IBinder -import android.os.RemoteException -import android.preference.PreferenceManager import android.provider.Settings import android.service.quicksettings.TileService -import android.util.Log import androidx.annotation.RequiresApi -import com.topjohnwu.superuser.Shell import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider -import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils -import io.github.sds100.keymapper.common.utils.Success -import io.github.sds100.keymapper.sysbridge.BuildConfig -import io.github.sds100.keymapper.sysbridge.IShizukuStarterService -import io.github.sds100.keymapper.sysbridge.adb.AdbClient -import io.github.sds100.keymapper.sysbridge.adb.AdbKey -import io.github.sds100.keymapper.sysbridge.adb.AdbKeyException -import io.github.sds100.keymapper.sysbridge.adb.AdbMdns -import io.github.sds100.keymapper.sysbridge.adb.AdbPairingClient -import io.github.sds100.keymapper.sysbridge.adb.AdbServiceType -import io.github.sds100.keymapper.sysbridge.adb.PreferenceAdbKeyStore -import io.github.sds100.keymapper.sysbridge.shizuku.ShizukuStarterService +import io.github.sds100.keymapper.sysbridge.adb.AdbManager import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout -import rikka.shizuku.Shizuku -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -56,256 +33,48 @@ import javax.inject.Singleton class SystemBridgeSetupControllerImpl @Inject constructor( @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, - private val buildConfigProvider: BuildConfigProvider + private val buildConfigProvider: BuildConfigProvider, + private val adbManager: AdbManager ) : SystemBridgeSetupController { companion object { private const val DEVELOPER_OPTIONS_SETTING = "development_settings_enabled" + private const val ADB_WIRELESS_SETTING = "adb_wifi_enabled" + } + + private val starter: SystemBridgeStarter by lazy { + SystemBridgeStarter(ctx, adbManager, buildConfigProvider) } override val isDeveloperOptionsEnabled: MutableStateFlow = MutableStateFlow(getDeveloperOptionsEnabled()) + override val isWirelessDebuggingEnabled: MutableStateFlow = + MutableStateFlow(getWirelessDebuggingEnabled()) + override val startSetupAssistantRequest: MutableSharedFlow = MutableSharedFlow() - val sb = StringBuilder() - - private var adbConnectMdns: AdbMdns? = null - - private val scriptPath: String by lazy { SystemBridgeStarter.writeSdcardFiles(ctx) } - private val apkPath = ctx.applicationInfo.sourceDir - private val libPath = ctx.applicationInfo.nativeLibraryDir - private val packageName = ctx.applicationInfo.packageName - - private val shizukuStarterConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected( - name: ComponentName?, - binder: IBinder? - ) { - Timber.i("Shizuku starter service connected") - - val service = IShizukuStarterService.Stub.asInterface(binder) - - Timber.i("Starting System Bridge with Shizuku starter service") - try { - service.startSystemBridge(scriptPath, apkPath, libPath, packageName) - - } catch (e: RemoteException) { - Timber.e("Exception starting with Shizuku starter service: $e") - } - } - - override fun onServiceDisconnected(name: ComponentName?) { - // Do nothing. The service is supposed to immediately kill itself - // after starting the command. - } - } - override fun startWithRoot() { - try { - if (Shell.isAppGrantedRoot() != true) { - Timber.w("Root is not granted. Cannot start System Bridge with Root.") - return - } - - val command = - SystemBridgeStarter.buildStartCommand(scriptPath, apkPath, libPath, packageName) - - Timber.i("Starting System Bridge with root") - Shell.cmd(command).exec().isSuccess - - } catch (e: Exception) { - Timber.e("Exception when starting System Bridge with Root: $e") + coroutineScope.launch { + starter.startWithRoot() } } override fun startWithShizuku() { - if (!Shizuku.pingBinder()) { - Timber.w("Shizuku is not running. Cannot start System Bridge with Shizuku.") - return - } - - // Shizuku will start a service which will then start the System Bridge. Shizuku won't be - // used to start the System Bridge directly because native libraries need to be used - // and we want to limit the dependency on Shizuku as much as possible. Also, the System - // Bridge should still be running even if Shizuku dies. - val serviceComponentName = ComponentName(ctx, ShizukuStarterService::class.java) - val args = Shizuku.UserServiceArgs(serviceComponentName) - .daemon(false) - .processNameSuffix("service") - .debuggable(BuildConfig.DEBUG) - .version(buildConfigProvider.versionCode) - - try { - Shizuku.bindUserService( - args, - shizukuStarterConnection - ) - } catch (e: Exception) { - Timber.e("Exception when starting System Bridge with Shizuku. $e") - } + starter.startWithShizuku() } - // TODO clean up - // TODO have lock so can only launch one start job at a time @RequiresApi(Build.VERSION_CODES.R) override fun startWithAdb() { - // TODO kill the current system bridge before starting it? - if (adbConnectMdns == null) { - adbConnectMdns = AdbMdns(ctx, AdbServiceType.TLS_CONNECT) - } - - coroutineScope.launch(Dispatchers.IO) { - - adbConnectMdns!!.start() - - val host = "127.0.0.1" - val port = withTimeout(1000L) { adbConnectMdns!!.port.first { it != null } } - - if (port == null) { - return@launch - } - - writeStarterFiles() - - sb.append("Starting with wireless adb...").append('\n').append('\n') - postResult() - - val key = try { - val adbKey = AdbKey( - PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), - "keymapper", - ) - adbKey - } catch (e: Throwable) { - e.printStackTrace() - sb.append('\n').append(Log.getStackTraceString(e)) - - postResult(AdbKeyException(e)) - return@launch - } - - AdbClient(host, port, key).runCatching { - connect() - shellCommand(SystemBridgeStarter.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/${ctx.packageName}/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() - - SystemBridgeStarter.writeDataFiles(ctx, true) - - AdbClient(host, port, key).runCatching { - connect() - shellCommand(SystemBridgeStarter.dataCommand) { - sb.append(String(it)) - postResult() - } - close() - }.onFailure { - it.printStackTrace() - - sb.append('\n').append(Log.getStackTraceString(it)) - postResult(it) - } - } - - adbConnectMdns!!.stop() - - } - } - - private fun writeStarterFiles() { - coroutineScope.launch(Dispatchers.IO) { - try { - SystemBridgeStarter.writeSdcardFiles(ctx) - } catch (e: Throwable) { - // TODO show error message if fails to start - } - } - } - - fun postResult(throwable: Throwable? = null) { - if (throwable == null) { - Timber.e(sb.toString()) - } else { - Timber.e(throwable) + coroutineScope.launch { + starter.startWithAdb() } } @RequiresApi(Build.VERSION_CODES.R) override suspend fun pairWirelessAdb(port: Int, code: Int): KMResult { - // TODO move this to AdbManager class and only allow one job at a time. - // TODO if a job is already running then this should return an error - return withContext(Dispatchers.IO) { - val host = "127.0.0.1" - - val key = try { - AdbKey( - PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), - "keymapper", - ) - } catch (e: Exception) { - e.printStackTrace() - return@withContext KMError.Exception(e) - } - - try { - AdbPairingClient(host, port, code.toString(), key).start() - Timber.i("Successfully paired with wireless ADB on port $port with code $code") - return@withContext Success(Unit) - } catch (e: Exception) { - e.printStackTrace() - Timber.e("Failed to pair with wireless ADB on port $port with code $code: $e") - return@withContext KMError.Exception(e) - } - } - -// val intent = AdbPairingService.startIntent(ctx) -// try { -// ctx.startForegroundService(intent) -// } catch (e: Throwable) { -// Timber.e("start ADB pairing service failed: $e") -// -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && -// e is ForegroundServiceStartNotAllowedException -// ) { -// val mode = ctx.getSystemService(AppOpsManager::class.java) -// .noteOpNoThrow( -// "android:start_foreground", -// android.os.Process.myUid(), -// ctx.packageName, -// null, -// null, -// ) -// if (mode == AppOpsManager.MODE_ERRORED) { -// Toast.makeText( -// ctx, -// "OP_START_FOREGROUND is denied. What are you doing?", -// Toast.LENGTH_LONG, -// ).show() -// } -// ctx.startService(intent) -// } -// } + return adbManager.pair(port, code) } override fun enableDeveloperOptions() { @@ -352,8 +121,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } - fun updateDeveloperOptionsEnabled() { + fun invalidateSettings() { isDeveloperOptionsEnabled.update { getDeveloperOptionsEnabled() } + isWirelessDebuggingEnabled.update { getWirelessDebuggingEnabled() } } private fun getDeveloperOptionsEnabled(): Boolean { @@ -363,6 +133,14 @@ class SystemBridgeSetupControllerImpl @Inject constructor( return false } } + + private fun getWirelessDebuggingEnabled(): Boolean { + try { + return SettingsUtils.getGlobalSetting(ctx, ADB_WIRELESS_SETTING) == 1 + } catch (_: Settings.SettingNotFoundException) { + return false + } + } } @SuppressLint("ObsoleteSdkInt") @@ -376,6 +154,7 @@ interface SystemBridgeSetupController { val isDeveloperOptionsEnabled: Flow fun enableDeveloperOptions() + val isWirelessDebuggingEnabled: Flow fun enableWirelessDebugging() @RequiresApi(Build.VERSION_CODES.R) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt index 2af2802842..9797ebb4a7 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.sysbridge.shizuku import android.annotation.SuppressLint import android.util.Log import io.github.sds100.keymapper.sysbridge.IShizukuStarterService -import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import kotlin.system.exitProcess @SuppressLint("LogNotTimber") @@ -19,24 +18,21 @@ class ShizukuStarterService : IShizukuStarterService.Stub() { exitProcess(0) } - override fun startSystemBridge( - scriptPath: String?, - apkPath: String?, - libPath: String?, - packageName: String? - ) { - if (scriptPath == null || apkPath == null || libPath == null || packageName == null) { - return + override fun executeCommand(command: String?): String? { + command ?: return null + + val process = Runtime.getRuntime().exec(command) + + val out = with(process.inputStream.bufferedReader()) { + readText() } - try { - val command = - SystemBridgeStarter.buildStartCommand(scriptPath, apkPath, libPath, packageName) - Runtime.getRuntime().exec(command).waitFor() - } catch (e: Exception) { - Log.e(TAG, "Failed to start system bridge", e) - } finally { - destroy() + val err = with(process.errorStream.bufferedReader()) { + readText() } + + process.waitFor() + + return "$out\n$err" } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index 246d5a1646..3ced2b3ac9 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -1,15 +1,33 @@ package io.github.sds100.keymapper.sysbridge.starter +import android.content.ComponentName import android.content.Context +import android.content.ServiceConnection import android.os.Build +import android.os.IBinder +import android.os.RemoteException import android.os.UserManager import android.system.ErrnoException import android.system.Os +import androidx.annotation.RequiresApi +import com.topjohnwu.superuser.Shell +import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.then +import io.github.sds100.keymapper.sysbridge.BuildConfig +import io.github.sds100.keymapper.sysbridge.IShizukuStarterService import io.github.sds100.keymapper.sysbridge.R -import io.github.sds100.keymapper.sysbridge.ktx.createDeviceProtectedStorageContextCompat -import io.github.sds100.keymapper.sysbridge.ktx.logd +import io.github.sds100.keymapper.sysbridge.adb.AdbManager import io.github.sds100.keymapper.sysbridge.ktx.loge +import io.github.sds100.keymapper.sysbridge.shizuku.ShizukuStarterService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import rikka.core.os.FileUtils +import rikka.shizuku.Shizuku +import timber.log.Timber import java.io.BufferedReader import java.io.ByteArrayInputStream import java.io.DataInputStream @@ -21,123 +39,246 @@ import java.io.InputStreamReader import java.io.PrintWriter import java.util.zip.ZipFile -// TODO clean up this code and move it to SystemBridgeConnectionManager, and if a lot of starter code in there then move it to a StarterDelegate -internal object SystemBridgeStarter { +class SystemBridgeStarter( + private val ctx: Context, + private val adbManager: AdbManager, + private val buildConfigProvider: BuildConfigProvider +) { + private val userManager by lazy { ctx.getSystemService(UserManager::class.java)!! } - private var commandInternal = arrayOfNulls(2) + private val apkPath = ctx.applicationInfo.sourceDir + private val libPath = ctx.applicationInfo.nativeLibraryDir + private val packageName = ctx.applicationInfo.packageName - val dataCommand get() = commandInternal[0]!! + private val shizukuStarterConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected( + name: ComponentName?, + binder: IBinder? + ) { + Timber.i("Shizuku starter service connected") - val sdcardCommand get() = commandInternal[1]!! + val service = IShizukuStarterService.Stub.asInterface(binder) - /** - * @return the path to the script file. - */ - fun writeSdcardFiles(context: Context): String { + Timber.i("Starting System Bridge with Shizuku starter service") + try { + runBlocking { + startSystemBridge(executeCommand = { command -> + val output = service.executeCommand(command) + + if (output == null) { + KMError.UnknownIOError + } else { + Success(output) + } + }) + } + + } catch (e: RemoteException) { + Timber.e("Exception starting with Shizuku starter service: $e") + } finally { + service.destroy() + } + } - val um = context.getSystemService(UserManager::class.java)!! - val unlocked = Build.VERSION.SDK_INT < 24 || um.isUserUnlocked - if (!unlocked) { - throw IllegalStateException("User is locked") + override fun onServiceDisconnected(name: ComponentName?) { + // Do nothing. The service is supposed to immediately kill itself + // after starting the command. } + } - 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 - val packageName = context.applicationInfo.packageName + fun startWithShizuku() { + if (!Shizuku.pingBinder()) { + Timber.w("Shizuku is not running. Cannot start System Bridge with Shizuku.") + return + } - commandInternal[1] = buildStartCommand(sh, apkPath, libPath, packageName) - logd(commandInternal[1]!!) + // Shizuku will start a service which will then start the System Bridge. Shizuku won't be + // used to start the System Bridge directly because native libraries need to be used + // and we want to limit the dependency on Shizuku as much as possible. Also, the System + // Bridge should still be running even if Shizuku dies. + val serviceComponentName = ComponentName(ctx, ShizukuStarterService::class.java) + val args = Shizuku.UserServiceArgs(serviceComponentName) + .daemon(false) + .processNameSuffix("service") + .debuggable(BuildConfig.DEBUG) + .version(buildConfigProvider.versionCode) - return sh + try { + Shizuku.bindUserService( + args, + shizukuStarterConnection + ) + } catch (e: Exception) { + Timber.e("Exception when starting System Bridge with Shizuku. $e") + } } - fun buildStartCommand( - sh: String, - apkPath: String, - libPath: String, - packageName: String - ): String = "sh $sh --apk=$apkPath --lib=$libPath --package=$packageName" + @RequiresApi(Build.VERSION_CODES.R) + suspend fun startWithAdb(): KMResult { + if (!userManager.isUserUnlocked) { + return KMError.Exception(IllegalStateException("User is locked")) + } + + // Get the file that contains the external files + return startSystemBridge(executeCommand = adbManager::executeCommand) + } - fun writeDataFiles(context: Context, permission: Boolean = false) { - if (commandInternal[0] != null && !permission) { - logd("already written") + suspend fun startWithRoot() { + if (Shell.isAppGrantedRoot() != true) { + Timber.w("Root is not granted. Cannot start System Bridge with Root.") return } - val dir = context.createDeviceProtectedStorageContextCompat().filesDir?.parentFile ?: return + Timber.i("Starting System Bridge with root") + startSystemBridge(executeCommand = { command -> + val output = withContext(Dispatchers.IO) { + Shell.cmd(command).exec() + } - if (permission) { - try { - Os.chmod(dir.absolutePath, 457 /* 0711 */) - } catch (e: ErrnoException) { - e.printStackTrace() + if (output.isSuccess) { + Success(output.out.plus(output.err).joinToString("\n")) + } else { + KMError.UnknownIOError } + }) + } + + private suspend fun startSystemBridge(executeCommand: suspend (String) -> KMResult): KMResult { + val externalFilesParent = try { + ctx.getExternalFilesDir(null)?.parentFile + } catch (e: IOException) { + return KMError.UnknownIOError } - try { - val starter = copyStarter(context, File(dir, "starter")) - val sh = writeScript(context, File(dir, "start.sh"), starter) + val outputStarterBinary = File(externalFilesParent, "starter") + withContext(Dispatchers.IO) { + copyNativeLibrary(outputStarterBinary) + + // Create the start.sh shell script + writeStarterScript( + File(externalFilesParent, "start.sh"), + outputStarterBinary.absolutePath + ) + } + + var startCommand = + "sh ${outputStarterBinary.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName" - val apkPath = context.applicationInfo.sourceDir - val libPath = context.applicationInfo.nativeLibraryDir + return executeCommand(startCommand).then { output -> - commandInternal[0] = "sh $sh --apk=$apkPath --lib=$libPath" - logd(commandInternal[0]!!) + // According to Shizuku source code... + // Adb on MIUI Android 11 has no permission to access Android/data. + // Before MIUI Android 12, we can temporarily use /data/user_de. + if (output.contains("/Android/data/${ctx.packageName}/start.sh: Permission denied")) { + Timber.w( + "ADB has no permission to access Android/data/${ctx.packageName}/start.sh. Trying to use /data/user_de instead..." + ) + + val protectedStorageDir = + ctx.createDeviceProtectedStorageContext().filesDir.parentFile - if (permission) { try { - Os.chmod(starter, 420 /* 0644 */) + Os.chmod(protectedStorageDir.absolutePath, 457 /* 0711 */) } catch (e: ErrnoException) { e.printStackTrace() } + try { - Os.chmod(sh, 420 /* 0644 */) - } catch (e: ErrnoException) { - e.printStackTrace() + val outputStarterBinary = File(protectedStorageDir, "starter") + + withContext(Dispatchers.IO) { + copyNativeLibrary(outputStarterBinary) + + writeStarterScript( + File(protectedStorageDir, "start.sh"), + outputStarterBinary.absolutePath + ) + } + + startCommand = + "sh ${outputStarterBinary.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName" + + try { + Os.chmod(outputStarterBinary.absolutePath, 420 /* 0644 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + try { + Os.chmod(outputStarterBinary.absolutePath, 420 /* 0644 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + + + } catch (e: IOException) { + loge("write files", e) } + + executeCommand(startCommand) + + } else { + Success(output) } - } catch (e: IOException) { - loge("write files", e) } } - private fun copyStarter(context: Context, out: File): String { - val so = "lib/${Build.SUPPORTED_ABIS[0]}/libsysbridge.so" - val ai = context.applicationInfo + /** + * This extracts the library file from inside the apk and copies it to [out] File. + */ + private fun copyNativeLibrary(out: File) { + val expectedLibraryPath = "lib/${Build.SUPPORTED_ABIS[0]}/libsysbridge.so" - val fos = FileOutputStream(out) - val apk = ZipFile(ai.sourceDir) + // Open the apk so the library file can be found + val apk = ZipFile(apkPath) val entries = apk.entries() + + // Loop over all the file entries in the zip file while (entries.hasMoreElements()) { val entry = entries.nextElement() ?: break - if (entry.name != so) continue + + if (entry.name != expectedLibraryPath) { + continue + } val buf = ByteArray(entry.size.toInt()) - val dis = DataInputStream(apk.getInputStream(entry)) - dis.readFully(buf) - FileUtils.copy(ByteArrayInputStream(buf), fos) + + // Read the native library into the buffer + with(DataInputStream(apk.getInputStream(entry))) { + readFully(buf) + } + + // Copy the buffer to the output file + with(FileOutputStream(out)) { + FileUtils.copy(ByteArrayInputStream(buf), this) + } + break } - return out.absolutePath } - private fun writeScript(context: Context, out: File, starter: String): String { + /** + * Write the start.sh shell script to the specified [out] file. The path to the starter + * binary will be substituted in the script with the [starterPath]. + */ + private fun writeStarterScript(out: File, starterPath: 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)) + + val scriptInputStream = ctx.resources.openRawResource(R.raw.start) + + with(scriptInputStream) { + val reader = BufferedReader(InputStreamReader(this)) + + val outputWriter = PrintWriter(FileWriter(out)) + var line: String? + + while (reader.readLine().also { line = it } != null) { + outputWriter.println(line!!.replace("%%%STARTER_PATH%%%", starterPath)) + } + + outputWriter.flush() + outputWriter.close() } - os.flush() - os.close() - return out.absolutePath } } From 00474f5b627f89d05e98c2c2d8c3dc2aed41edf4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 23 Aug 2025 22:40:11 +0100 Subject: [PATCH 138/215] #1394 complete system bridge setup screen --- .../base/promode/ProModeSetupScreen.kt | 11 +- .../base/promode/ProModeSetupViewModel.kt | 3 +- .../SystemBridgeSetupAssistantController.kt | 6 +- .../base/promode/SystemBridgeSetupUseCase.kt | 42 ++++-- base/src/main/res/values/strings.xml | 3 + .../keymapper/sysbridge/adb/AdbClient.kt | 40 ++++-- .../keymapper/sysbridge/adb/AdbError.kt | 1 + .../keymapper/sysbridge/adb/AdbManager.kt | 43 +++--- .../service/SystemBridgeSetupController.kt | 46 +++++-- .../service/SystemBridgeSetupStep.kt | 3 +- .../sysbridge/starter/SystemBridgeStarter.kt | 127 ++++++++++-------- 11 files changed, 210 insertions(+), 115 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index c8d72013b5..8fc2c58355 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Accessibility import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.Build +import androidx.compose.material.icons.rounded.CheckCircleOutline import androidx.compose.material.icons.rounded.Link import androidx.compose.material.icons.rounded.Notifications import androidx.compose.material.icons.rounded.PlayArrow @@ -167,7 +168,8 @@ fun ProModeSetupScreen( AssistantCheckBoxRow( modifier = Modifier.fillMaxWidth(), - isEnabled = state.data.step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE, + isEnabled = state.data.step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE + && state.data.step != SystemBridgeSetupStep.STARTED, isChecked = state.data.isSetupAssistantChecked, onAssistantClick = onAssistantClick ) @@ -342,6 +344,13 @@ private fun getStepContent(step: SystemBridgeSetupStep): StepContent { icon = Icons.Rounded.PlayArrow, buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service) ) + + SystemBridgeSetupStep.STARTED -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_complete_title), + message = stringResource(R.string.pro_mode_setup_wizard_complete_text), + icon = Icons.Rounded.CheckCircleOutline, + buttonText = stringResource(R.string.pro_mode_setup_wizard_complete_button) + ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt index b1dc220ed6..7470909fe9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt @@ -38,8 +38,9 @@ class ProModeSetupViewModel @Inject constructor( SystemBridgeSetupStep.DEVELOPER_OPTIONS -> useCase.openDeveloperOptions() SystemBridgeSetupStep.WIFI_NETWORK -> useCase.connectWifiNetwork() SystemBridgeSetupStep.WIRELESS_DEBUGGING -> useCase.enableWirelessDebugging() - SystemBridgeSetupStep.ADB_PAIRING -> useCase.pairAdb() + SystemBridgeSetupStep.ADB_PAIRING -> useCase.pairWirelessAdb() SystemBridgeSetupStep.START_SERVICE -> useCase.startSystemBridgeWithAdb() + SystemBridgeSetupStep.STARTED -> popBackStack() } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index d97b64f8ef..5500563c7c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -17,6 +17,7 @@ import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsU import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.KeyMapperClassProvider +import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults @@ -44,7 +45,6 @@ import timber.log.Timber class SystemBridgeSetupAssistantController @AssistedInject constructor( @Assisted private val coroutineScope: CoroutineScope, - @Assisted private val accessibilityService: BaseAccessibilityService, private val manageNotifications: ManageNotificationsUseCase, @@ -175,6 +175,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( @RequiresApi(Build.VERSION_CODES.R) private suspend fun onPairingCodeFound(port: Int, pairingCode: Int) { setupController.pairWirelessAdb(port, pairingCode).onSuccess { + Timber.i("Pairing code found. Starting System Bridge with ADB...") setupController.startWithAdb() stopInteracting() @@ -202,6 +203,9 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( // TODO Show notification Timber.w("Failed to start system bridge after pairing.") } + }.onFailure { + Timber.e("Failed to pair with wireless ADB: $it") + // TODO show notification } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 1cccb84e08..f4a8239e9e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -18,6 +18,8 @@ import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -58,15 +60,24 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( override val isSystemBridgeConnected: Flow = systemBridgeConnectionManager.isConnected - override val nextSetupStep: Flow = combine( - accessibilityServiceAdapter.state, - permissionAdapter.isGrantedFlow(Permission.POST_NOTIFICATIONS), - systemBridgeSetupController.isDeveloperOptionsEnabled, - networkAdapter.isWifiConnected, - systemBridgeSetupController.isWirelessDebuggingEnabled, - ::getNextStep - ) + @RequiresApi(Build.VERSION_CODES.R) + override val nextSetupStep: Flow = + isSystemBridgeConnected.flatMapLatest { isConnected -> + if (isConnected) { + flowOf(SystemBridgeSetupStep.STARTED) + } else { + combine( + accessibilityServiceAdapter.state, + permissionAdapter.isGrantedFlow(Permission.POST_NOTIFICATIONS), + systemBridgeSetupController.isDeveloperOptionsEnabled, + networkAdapter.isWifiConnected, + systemBridgeSetupController.isWirelessDebuggingEnabled, + ::getNextStep + ) + } + } + @RequiresApi(Build.VERSION_CODES.R) override val setupProgress: Flow = nextSetupStep.map { step -> step.stepIndex.toFloat() / SystemBridgeSetupStep.entries.size } @@ -115,11 +126,12 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } override fun enableWirelessDebugging() { - systemBridgeSetupController.enableWirelessDebugging() + systemBridgeSetupController.launchEnableWirelessDebuggingAssistant() } - override fun pairAdb() { - TODO("Not yet implemented") + @RequiresApi(Build.VERSION_CODES.R) + override fun pairWirelessAdb() { + systemBridgeSetupController.launchPairingAssistant() } override fun startSystemBridgeWithRoot() { @@ -134,7 +146,8 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( systemBridgeSetupController.startWithAdb() } - private fun getNextStep( + @RequiresApi(Build.VERSION_CODES.R) + private suspend fun getNextStep( accessibilityServiceState: AccessibilityServiceState, isNotificationPermissionGranted: Boolean, isDeveloperOptionsEnabled: Boolean, @@ -147,7 +160,8 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( !isDeveloperOptionsEnabled -> SystemBridgeSetupStep.DEVELOPER_OPTIONS !isWifiConnected -> SystemBridgeSetupStep.WIFI_NETWORK !isWirelessDebuggingEnabled -> SystemBridgeSetupStep.WIRELESS_DEBUGGING - else -> SystemBridgeSetupStep.ADB_PAIRING + isWirelessDebuggingEnabled && !systemBridgeSetupController.isAdbPaired() -> SystemBridgeSetupStep.ADB_PAIRING + else -> SystemBridgeSetupStep.START_SERVICE } } @@ -175,7 +189,7 @@ interface SystemBridgeSetupUseCase { fun openDeveloperOptions() fun connectWifiNetwork() fun enableWirelessDebugging() - fun pairAdb() + fun pairWirelessAdb() fun startSystemBridgeWithRoot() fun startSystemBridgeWithShizuku() fun startSystemBridgeWithAdb() diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 52c001493b..d91fa48d31 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1646,5 +1646,8 @@ Setup assistant PRO mode started! PRO mode is now running and you can remap more buttons + PRO mode is running + You can now remap more buttons and use more actions. + Go back diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt index adb27cb8dd..89ee9c580b 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt @@ -1,7 +1,7 @@ package io.github.sds100.keymapper.sysbridge.adb -import android.os.Build -import androidx.annotation.RequiresApi +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_SIGNATURE import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_AUTH @@ -14,10 +14,12 @@ import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS_VERSION import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_VERSION import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_WRTE +import kotlinx.coroutines.delay import timber.log.Timber import java.io.Closeable import java.io.DataInputStream import java.io.DataOutputStream +import java.net.ConnectException import java.net.Socket import java.nio.ByteBuffer import java.nio.ByteOrder @@ -25,11 +27,10 @@ import javax.net.ssl.SSLSocket private const val TAG = "AdbClient" -@RequiresApi(Build.VERSION_CODES.M) internal class AdbClient(private val host: String, private val port: Int, private val key: AdbKey) : Closeable { - private lateinit var socket: Socket + private var socket: Socket? = null private lateinit var plainInputStream: DataInputStream private lateinit var plainOutputStream: DataOutputStream @@ -42,11 +43,28 @@ internal class AdbClient(private val host: String, private val port: Int, privat 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()) + suspend fun connect(): KMResult { + var connectAttemptCounter = 0 + + // Try to connect to the client multiple times in case the server hasn't started up + // yet + while (socket == null && connectAttemptCounter < 5) { + try { + socket = Socket(host, port) + } catch (_: ConnectException) { + delay(1000) + connectAttemptCounter++ + continue + } + } + + if (socket == null) { + return AdbError.ConnectionError + } + + socket!!.tcpNoDelay = true + plainInputStream = DataInputStream(socket!!.getInputStream()) + plainOutputStream = DataOutputStream(socket!!.getOutputStream()) write(A_CNXN, A_VERSION, A_MAXDATA, "host::") @@ -77,6 +95,8 @@ internal class AdbClient(private val host: String, private val port: Int, privat if (message.command != A_CNXN) { error("not A_CNXN") } + + return Success(Unit) } fun shellCommand(command: String): ByteArray { @@ -170,7 +190,7 @@ internal class AdbClient(private val host: String, private val port: Int, privat } catch (e: Throwable) { } try { - socket.close() + socket?.close() } catch (e: Exception) { } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt index 501953b469..2e8fd0c6ac 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt @@ -7,5 +7,6 @@ sealed class AdbError : KMError() { data object PairingError : AdbError() data object ServerNotFound : AdbError() data object KeyCreationError : AdbError() + data object ConnectionError : AdbError() data class Unknown(val exception: kotlin.Exception) : AdbError() } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt index f896c3c9a8..c07b84dcba 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt @@ -33,36 +33,37 @@ class AdbManagerImpl @Inject constructor( private val commandMutex: Mutex = Mutex() private val pairMutex: Mutex = Mutex() private val adbConnectMdns: AdbMdns by lazy { AdbMdns(ctx, AdbServiceType.TLS_CONNECT) } - private var client: AdbClient? = null override suspend fun executeCommand(command: String): KMResult { + Timber.i("Execute ADB command: $command") + val result = withContext(Dispatchers.IO) { - return@withContext pairMutex.withLock { + return@withContext commandMutex.withLock { adbConnectMdns.start() - if (client == null) { - - val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } + val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } - if (port == null) { - return@withLock AdbError.ServerNotFound - } + if (port == null) { + return@withLock AdbError.ServerNotFound + } - val adbKey = getAdbKey() + val adbKey = getAdbKey() - when (adbKey) { - is KMError -> return@withLock adbKey - is Success -> client = AdbClient(LOCALHOST, port, adbKey.value) - } + // Recreate a new client every time in case the port changes during the lifetime + // of AdbManager + val client: AdbClient = when (adbKey) { + is KMError -> return@withLock adbKey + is Success -> AdbClient(LOCALHOST, port, adbKey.value) } - return@withLock with(client!!) { - connect() - try { - client!!.shellCommand(command).success() - } catch (e: Exception) { - Timber.e(e) - AdbError.Unknown(e) + return@withLock with(client) { + connect().then { + try { + client.shellCommand(command).success() + } catch (e: Exception) { + Timber.e(e) + AdbError.Unknown(e) + } } }.then { String(it).success() } } @@ -70,6 +71,8 @@ class AdbManagerImpl @Inject constructor( adbConnectMdns.stop() + Timber.i("Execute command result: $result") + return result } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index d31aeb43d8..7f7129aab9 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -13,6 +13,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils +import io.github.sds100.keymapper.common.utils.isSuccess import io.github.sds100.keymapper.sysbridge.adb.AdbManager import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import kotlinx.coroutines.CoroutineScope @@ -72,13 +73,22 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } + @RequiresApi(Build.VERSION_CODES.R) + override fun launchPairingAssistant() { + launchWirelessDebuggingActivity() + + coroutineScope.launch { + startSetupAssistantRequest.emit(SystemBridgeSetupStep.ADB_PAIRING) + } + } + @RequiresApi(Build.VERSION_CODES.R) override suspend fun pairWirelessAdb(port: Int, code: Int): KMResult { return adbManager.pair(port, code) } override fun enableDeveloperOptions() { - // TODO show notification after the actvitiy is to tap the Build Number repeatedly + // TODO show notification after the activity is to tap the Build Number repeatedly SettingsUtils.launchSettingsScreen( ctx, @@ -87,8 +97,26 @@ class SystemBridgeSetupControllerImpl @Inject constructor( ) } - override fun enableWirelessDebugging() { + override fun launchEnableWirelessDebuggingAssistant() { // This is the intent sent by the quick settings tile. Not all devices support this. + launchWirelessDebuggingActivity() + + coroutineScope.launch { + startSetupAssistantRequest.emit(SystemBridgeSetupStep.WIRELESS_DEBUGGING) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + override suspend fun isAdbPaired(): Boolean { + if (!getWirelessDebuggingEnabled()) { + return false + } + + // Try running a command to see if the pairing is working correctly. + return adbManager.executeCommand("sh").isSuccess + } + + private fun launchWirelessDebuggingActivity() { val quickSettingsIntent = Intent(TileService.ACTION_QS_TILE_PREFERENCES).apply { // Set the package name because this action can also resolve to a "Permission Controller" activity. val packageName = "com.android.settings" @@ -107,18 +135,13 @@ class SystemBridgeSetupControllerImpl @Inject constructor( try { ctx.startActivity(quickSettingsIntent) - } catch (e: ActivityNotFoundException) { + } catch (_: ActivityNotFoundException) { SettingsUtils.launchSettingsScreen( ctx, Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS, "toggle_adb_wireless" ) } - - // TODO send correct step - coroutineScope.launch { - startSetupAssistantRequest.emit(SystemBridgeSetupStep.ADB_PAIRING) - } } fun invalidateSettings() { @@ -155,7 +178,12 @@ interface SystemBridgeSetupController { fun enableDeveloperOptions() val isWirelessDebuggingEnabled: Flow - fun enableWirelessDebugging() + fun launchEnableWirelessDebuggingAssistant() + + fun launchPairingAssistant() + + @RequiresApi(Build.VERSION_CODES.R) + suspend fun isAdbPaired(): Boolean @RequiresApi(Build.VERSION_CODES.R) suspend fun pairWirelessAdb(port: Int, code: Int): KMResult diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt index ac7725b459..395369036e 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt @@ -7,5 +7,6 @@ enum class SystemBridgeSetupStep(val stepIndex: Int) { WIFI_NETWORK(stepIndex = 3), WIRELESS_DEBUGGING(stepIndex = 4), ADB_PAIRING(stepIndex = 5), - START_SERVICE(stepIndex = 6) + START_SERVICE(stepIndex = 6), + STARTED(stepIndex = 7) } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index 3ced2b3ac9..4dcbf2f03b 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -15,6 +15,7 @@ import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.sysbridge.BuildConfig import io.github.sds100.keymapper.sysbridge.IShizukuStarterService @@ -120,7 +121,9 @@ class SystemBridgeStarter( } // Get the file that contains the external files - return startSystemBridge(executeCommand = adbManager::executeCommand) + return startSystemBridge(executeCommand = adbManager::executeCommand).onFailure { error -> + Timber.w("Failed to start system bridge with ADB: $error") + } } suspend fun startWithRoot() { @@ -151,18 +154,19 @@ class SystemBridgeStarter( } val outputStarterBinary = File(externalFilesParent, "starter") + val outputStarterScript = File(externalFilesParent, "start.sh") withContext(Dispatchers.IO) { copyNativeLibrary(outputStarterBinary) // Create the start.sh shell script writeStarterScript( - File(externalFilesParent, "start.sh"), + outputStarterScript, outputStarterBinary.absolutePath ) } - var startCommand = - "sh ${outputStarterBinary.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName" + val startCommand = + "sh ${outputStarterScript.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName" return executeCommand(startCommand).then { output -> @@ -174,51 +178,57 @@ class SystemBridgeStarter( "ADB has no permission to access Android/data/${ctx.packageName}/start.sh. Trying to use /data/user_de instead..." ) - val protectedStorageDir = - ctx.createDeviceProtectedStorageContext().filesDir.parentFile - - try { - Os.chmod(protectedStorageDir.absolutePath, 457 /* 0711 */) - } catch (e: ErrnoException) { - e.printStackTrace() - } + startSystemBridgeFromProtectedStorage(executeCommand) + } else { + Success(output) + } + } + } - try { - val outputStarterBinary = File(protectedStorageDir, "starter") + private suspend fun startSystemBridgeFromProtectedStorage( + executeCommand: suspend (String) -> KMResult + ): KMResult { + val protectedStorageDir = + ctx.createDeviceProtectedStorageContext().filesDir.parentFile - withContext(Dispatchers.IO) { - copyNativeLibrary(outputStarterBinary) + try { + Os.chmod(protectedStorageDir.absolutePath, 457 /* 0711 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } - writeStarterScript( - File(protectedStorageDir, "start.sh"), - outputStarterBinary.absolutePath - ) - } + try { + val outputStarterBinary = File(protectedStorageDir, "starter") + val outputStarterScript = File(protectedStorageDir, "start.sh") - startCommand = - "sh ${outputStarterBinary.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName" + withContext(Dispatchers.IO) { + copyNativeLibrary(outputStarterBinary) - try { - Os.chmod(outputStarterBinary.absolutePath, 420 /* 0644 */) - } catch (e: ErrnoException) { - e.printStackTrace() - } - try { - Os.chmod(outputStarterBinary.absolutePath, 420 /* 0644 */) - } catch (e: ErrnoException) { - e.printStackTrace() - } + writeStarterScript( + outputStarterScript, + outputStarterBinary.absolutePath + ) + } + val startCommand = + "sh ${outputStarterScript.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName" - } catch (e: IOException) { - loge("write files", e) - } + try { + Os.chmod(outputStarterBinary.absolutePath, 420 /* 0644 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + try { + Os.chmod(outputStarterBinary.absolutePath, 420 /* 0644 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } - executeCommand(startCommand) + return executeCommand(startCommand) - } else { - Success(output) - } + } catch (e: IOException) { + loge("write files", e) + return KMError.UnknownIOError } } @@ -229,30 +239,31 @@ class SystemBridgeStarter( val expectedLibraryPath = "lib/${Build.SUPPORTED_ABIS[0]}/libsysbridge.so" // Open the apk so the library file can be found - val apk = ZipFile(apkPath) - val entries = apk.entries() + with(ZipFile(apkPath)) { + val entries = entries() - // Loop over all the file entries in the zip file - while (entries.hasMoreElements()) { - val entry = entries.nextElement() ?: break + // Loop over all the file entries in the zip file + while (entries.hasMoreElements()) { + val entry = entries.nextElement() ?: break - if (entry.name != expectedLibraryPath) { - continue - } + if (entry.name != expectedLibraryPath) { + continue + } - val buf = ByteArray(entry.size.toInt()) + val buf = ByteArray(entry.size.toInt()) - // Read the native library into the buffer - with(DataInputStream(apk.getInputStream(entry))) { - readFully(buf) - } + // Read the native library into the buffer + with(DataInputStream(getInputStream(entry))) { + readFully(buf) + } - // Copy the buffer to the output file - with(FileOutputStream(out)) { - FileUtils.copy(ByteArrayInputStream(buf), this) - } + // Copy the buffer to the output file + with(FileOutputStream(out)) { + FileUtils.copy(ByteArrayInputStream(buf), this) + } - break + break + } } } From 61a8d7d40725e76f913e680af4d235196d347277 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 10:08:35 +0100 Subject: [PATCH 139/215] #1394 make empty settings screen --- .../github/sds100/keymapper/MainFragment.kt | 11 --- .../sds100/keymapper/base/BaseMainNavHost.kt | 48 ++++++++++ .../base/actions/ChooseActionScreen.kt | 60 ++++++------- .../base/actions/ConfigActionsViewModel.kt | 9 +- .../keymapper/base/promode/ProModeFragment.kt | 80 ----------------- .../keymapper/base/promode/ProModeScreen.kt | 25 +++--- .../base/promode/ProModeSetupScreen.kt | 9 +- .../base/promode/ProModeSetupViewModel.kt | 14 ++- .../base/promode/ProModeViewModel.kt | 12 ++- .../keymapper/base/settings/SettingsScreen.kt | 90 +++++++++++++++++++ .../base/settings/SettingsViewModel.kt | 11 ++- .../permissions/RequestPermissionDelegate.kt | 6 +- .../base/utils/navigation/NavDestination.kt | 11 +-- .../utils/navigation/NavigationProvider.kt | 5 -- .../base/utils/ui/compose/PageLinkButton.kt | 64 +++++++++++++ base/src/main/res/navigation/nav_base_app.xml | 34 ------- 16 files changed, 288 insertions(+), 201 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/PageLinkButton.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt b/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt index 4405f5e21f..1ef6d00efd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt @@ -25,8 +25,6 @@ import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.BaseMainNavHost import io.github.sds100.keymapper.base.actions.ActionsScreen -import io.github.sds100.keymapper.base.actions.ChooseActionScreen -import io.github.sds100.keymapper.base.actions.ChooseActionViewModel import io.github.sds100.keymapper.base.actions.ConfigActionsViewModel import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.constraints.ConfigConstraintsViewModel @@ -190,14 +188,5 @@ class MainFragment : Fragment() { }, ) } - - composable { - val viewModel: ChooseActionViewModel = hiltViewModel() - - ChooseActionScreen( - modifier = Modifier.fillMaxSize(), - viewModel = viewModel, - ) - } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt index 4589823952..574e89725b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt @@ -1,7 +1,14 @@ package io.github.sds100.keymapper.base import androidx.compose.animation.AnimatedContentTransitionScope +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.runtime.Composable import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -9,10 +16,16 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import io.github.sds100.keymapper.base.actions.ChooseActionScreen +import io.github.sds100.keymapper.base.actions.ChooseActionViewModel import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementScreen import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementViewModel import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel +import io.github.sds100.keymapper.base.promode.ProModeScreen +import io.github.sds100.keymapper.base.promode.ProModeSetupScreen +import io.github.sds100.keymapper.base.settings.SettingsScreen +import io.github.sds100.keymapper.base.settings.SettingsViewModel import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.handleRouteArgs import kotlinx.serialization.json.Json @@ -54,6 +67,41 @@ fun BaseMainNavHost( ) } + composable { + val viewModel: ChooseActionViewModel = hiltViewModel() + + ChooseActionScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + ) + } + + composable { + val viewModel: SettingsViewModel = hiltViewModel() + + SettingsScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + ) + } + + composable { + ProModeScreen( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ), + viewModel = hiltViewModel(), + ) + } + + composable { + ProModeSetupScreen( + viewModel = hiltViewModel(), + ) + } composableDestinations() } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt index fce1f2f42c..9ebd5b8010 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.actions import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding @@ -20,10 +19,12 @@ import androidx.compose.material.icons.rounded.Bluetooth import androidx.compose.material.icons.rounded.Wifi import androidx.compose.material3.BottomAppBar import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme 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 @@ -71,6 +72,7 @@ fun ChooseActionScreen( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChooseActionScreen( modifier: Modifier = Modifier, @@ -83,6 +85,11 @@ private fun ChooseActionScreen( ) { Scaffold( modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.choose_action_title)) }, + ) + }, bottomBar = { BottomAppBar( modifier = Modifier.imePadding(), @@ -112,34 +119,21 @@ private fun ChooseActionScreen( end = endPadding, ), - ) { - Column { - Text( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - top = 16.dp, - bottom = 8.dp, - ), - text = stringResource(R.string.choose_action_title), - style = MaterialTheme.typography.titleLarge, - ) - - when (state) { - State.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize()) + ) { + when (state) { + State.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize()) - is State.Data -> { - if (state.data.isEmpty()) { - EmptyScreen( - modifier = Modifier.fillMaxSize(), - ) - } else { - ListScreen( - modifier = Modifier.fillMaxSize(), - groups = state.data, - onClickAction = onClickAction, - ) - } + is State.Data -> { + if (state.data.isEmpty()) { + EmptyScreen( + modifier = Modifier.fillMaxSize(), + ) + } else { + ListScreen( + modifier = Modifier.fillMaxSize(), + groups = state.data, + onClickAction = onClickAction, + ) } } } @@ -230,7 +224,7 @@ private fun PreviewList() { icon = ComposeIconInfo.Vector(Icons.Rounded.Android), ), - ), + ), ), SimpleListItemGroup( header = "Connectivity", @@ -251,7 +245,7 @@ private fun PreviewList() { isEnabled = false, ), - ), + ), ), ), ), @@ -280,7 +274,7 @@ private fun PreviewGrid() { icon = ComposeIconInfo.Vector(Icons.Rounded.Android), ), - ), + ), ), SimpleListItemGroup( header = "Connectivity", @@ -310,10 +304,10 @@ private fun PreviewGrid() { isEnabled = false, ), - ), + ), ), - ), + ), ), ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index be2a3d94e6..236f682808 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -313,6 +313,7 @@ class ConfigActionsViewModel @Inject constructor( } } + // TODO stop advertising the GUI keyboard, and ask them to use PRO mode? private suspend fun promptToInstallShizukuOrGuiKeyboard() { if (onboarding.isTvDevice()) { val chooseSolutionDialog = DialogModel.Alert( @@ -329,8 +330,8 @@ class ConfigActionsViewModel @Inject constructor( when (chooseSolutionResponse) { // install shizuku DialogResponse.POSITIVE -> { - navigate("shizuku", NavDestination.ShizukuSettings) - onboarding.neverShowGuiKeyboardPromptsAgain() +// navigate("shizuku", NavDestination.ShizukuSettings) +// onboarding.neverShowGuiKeyboardPromptsAgain() return } @@ -374,8 +375,8 @@ class ConfigActionsViewModel @Inject constructor( when (chooseSolutionResponse) { // install shizuku DialogResponse.POSITIVE -> { - navigate("shizuku_error", NavDestination.ShizukuSettings) - onboarding.neverShowGuiKeyboardPromptsAgain() +// navigate("shizuku_error", NavDestination.ShizukuSettings) +// onboarding.neverShowGuiKeyboardPromptsAgain() return } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt deleted file mode 100644 index 30980c3c25..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeFragment.kt +++ /dev/null @@ -1,80 +0,0 @@ -package io.github.sds100.keymapper.base.promode - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.animation.AnimatedContentTransitionScope -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.hilt.navigation.compose.hiltViewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.findNavController -import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.base.compose.KeyMapperTheme -import io.github.sds100.keymapper.base.databinding.FragmentComposeBinding -import io.github.sds100.keymapper.base.utils.navigation.NavDestination - -// TODO delete because settings will be composable -@AndroidEntryPoint -class ProModeFragment : Fragment() { - - 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 { - val navController = rememberNavController() - NavHost( - navController = navController, - startDestination = NavDestination.ID_PRO_MODE, - enterTransition = { slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Left) }, - exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, - popEnterTransition = { slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, - popExitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, - ) { - composable(NavDestination.ID_PRO_MODE) { - ProModeScreen( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding( - WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) - .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), - ), - viewModel = hiltViewModel(), - onNavigateBack = { findNavController().navigateUp() }, - onNavigateToSetup = { navController.navigate(NavDestination.ProModeSetup.ID_PRO_MODE_SETUP) } - ) - } - composable(NavDestination.ProModeSetup.ID_PRO_MODE_SETUP) { - ProModeSetupScreen( - viewModel = hiltViewModel(), - onNavigateBack = { navController.navigateUp() } - ) - } - } - } - } - } - return this.root - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 475c338b4e..03e1c0210b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -24,6 +24,7 @@ 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.BottomAppBar import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -62,13 +63,11 @@ import io.github.sds100.keymapper.common.utils.State fun ProModeScreen( modifier: Modifier = Modifier, viewModel: ProModeViewModel, - onNavigateBack: () -> Unit, - onNavigateToSetup: () -> Unit ) { val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle() - ProModeScreen(modifier = modifier, onBackClick = onNavigateBack) { + ProModeScreen(modifier = modifier, onBackClick = viewModel::onBackClick) { Content( warningState = proModeWarningState, setupState = proModeSetupState, @@ -76,7 +75,7 @@ fun ProModeScreen( onStopServiceClick = viewModel::onStopServiceClick, onShizukuButtonClick = viewModel::onShizukuButtonClick, onRootButtonClick = viewModel::onRootButtonClick, - onSetupWithKeyMapperClick = onNavigateToSetup, + onSetupWithKeyMapperClick = viewModel::onSetupWithKeyMapperClick, ) } } @@ -93,16 +92,18 @@ private fun ProModeScreen( 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), - ) - } - }, ) }, + bottomBar = { + BottomAppBar { + 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) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index 8fc2c58355..af78c21e6d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -61,16 +61,15 @@ import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep @Composable fun ProModeSetupScreen( viewModel: ProModeSetupViewModel, - onNavigateBack: () -> Unit, ) { val state by viewModel.setupState.collectAsStateWithLifecycle() ProModeSetupScreen( state = state, - onNavigateBack = onNavigateBack, onStepButtonClick = viewModel::onStepButtonClick, onAssistantClick = viewModel::onAssistantClick, - onWatchTutorialClick = { } + onWatchTutorialClick = { }, //TODO + onBackClick = viewModel::onBackClick ) } @@ -78,7 +77,7 @@ fun ProModeSetupScreen( @Composable fun ProModeSetupScreen( state: State, - onNavigateBack: () -> Unit = {}, + onBackClick: () -> Unit = {}, onStepButtonClick: () -> Unit = {}, onAssistantClick: () -> Unit = {}, onWatchTutorialClick: () -> Unit = {} @@ -88,7 +87,7 @@ fun ProModeSetupScreen( TopAppBar( title = { Text(stringResource(R.string.pro_mode_setup_wizard_title)) }, navigationIcon = { - IconButton(onClick = onNavigateBack) { + IconButton(onClick = onBackClick) { Icon( imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource(id = R.string.action_go_back) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt index 7470909fe9..59cfc11557 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt @@ -45,6 +45,16 @@ class ProModeSetupViewModel @Inject constructor( } } + fun onBackClick() { + viewModelScope.launch { + popBackStack() + } + } + + fun onAssistantClick() { + useCase.toggleSetupAssistant() + } + private fun buildState( step: SystemBridgeSetupStep, isSetupAssistantEnabled: Boolean @@ -56,10 +66,6 @@ class ProModeSetupViewModel @Inject constructor( isSetupAssistantChecked = isSetupAssistantEnabled ) ) - - fun onAssistantClick() { - useCase.toggleSetupAssistant() - } } data class ProModeSetupState( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index 8a9914dbaf..d3fab1ca56 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -3,7 +3,9 @@ package io.github.sds100.keymapper.base.promode import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.State @@ -106,8 +108,16 @@ class ProModeViewModel @Inject constructor( } } + fun onBackClick() { + viewModelScope.launch { + popBackStack() + } + } + fun onSetupWithKeyMapperClick() { - // TODO Settings screen will be refactored into compose and so NavigationProvider will work + viewModelScope.launch { + navigate("setup_pro_mode_with_key_mapper", NavDestination.ProModeSetup) + } } private fun buildSetupState( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt new file mode 100644 index 0000000000..df2ab75a5a --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -0,0 +1,90 @@ +package io.github.sds100.keymapper.base.settings + +import androidx.compose.foundation.layout.Column +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.padding +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.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import io.github.sds100.keymapper.base.R + +@Composable +fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { + SettingsScreen(modifier, onBackClick = viewModel::onBackClick) { Content() } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SettingsScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit, + content: @Composable () -> Unit +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.action_settings)) }, + ) + }, + bottomBar = { + BottomAppBar { + 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) { + Column(modifier.verticalScroll(rememberScrollState())) { + + } +} + +@Preview +@Composable +private fun Preview() { + SettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { + Content() + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 81028e19d4..d39057bb19 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -107,7 +107,10 @@ class SettingsViewModel @Inject constructor( val soundFiles = useCase.getSoundFiles() if (soundFiles.isEmpty()) { - showDialog("no sound files", DialogModel.Toast(getString(R.string.toast_no_sound_files))) + showDialog( + "no sound files", + DialogModel.Toast(getString(R.string.toast_no_sound_files)) + ) return@launch } @@ -224,4 +227,10 @@ class SettingsViewModel @Inject constructor( navigate("pro_mode_settings", NavDestination.ProMode) } } + + fun onBackClick() { + viewModelScope.launch { + popBackStack() + } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt index 4b3c3d8dbd..5634a20398 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt @@ -14,7 +14,6 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.navigation.NavController -import io.github.sds100.keymapper.base.NavBaseAppDirections import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.utils.ui.str import io.github.sds100.keymapper.common.BuildConfigProvider @@ -190,6 +189,7 @@ class RequestPermissionDelegate( } } + // TODO show prompt requesting root permission. If not found then show a dialog explaining to grant permission manually in their root management app such as Magisk. private fun requestRootPermission(navController: NavController) { if (showDialogs) { activity.materialAlertDialog { @@ -198,7 +198,7 @@ class RequestPermissionDelegate( setIcon(R.drawable.ic_baseline_warning_24) okButton { - navController.navigate(NavBaseAppDirections.toSettingsFragment()) +// navController.navigate(NavBaseAppDirections.toSettingsFragment()) } negativeButton(R.string.neg_cancel) { it.cancel() } @@ -206,7 +206,7 @@ class RequestPermissionDelegate( show() } } else { - navController.navigate(NavBaseAppDirections.toSettingsFragment()) +// navController.navigate(NavBaseAppDirections.toSettingsFragment()) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt index b1999dec3c..e77b96dc31 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt @@ -33,7 +33,6 @@ abstract class NavDestination(val isCompose: Boolean = false) { const val ID_SETTINGS = "settings" const val ID_ABOUT = "about" const val ID_CONFIG_KEY_MAP = "config_key_map" - const val ID_SHIZUKU_SETTINGS = "shizuku_settings" const val ID_INTERACT_UI_ELEMENT_ACTION = "interact_ui_element_action" const val ID_PRO_MODE = "pro_mode" } @@ -119,7 +118,7 @@ abstract class NavDestination(val isCompose: Boolean = false) { } @Serializable - data object Settings : NavDestination() { + data object Settings : NavDestination(isCompose = true) { override val id: String = ID_SETTINGS } @@ -143,18 +142,14 @@ abstract class NavDestination(val isCompose: Boolean = false) { override val id: String = ID_CONFIG_KEY_MAP } - @Serializable - data object ShizukuSettings : NavDestination() { - override val id: String = ID_SHIZUKU_SETTINGS - } - @Serializable data class InteractUiElement(val actionJson: String?) : NavDestination(isCompose = true) { override val id: String = ID_INTERACT_UI_ELEMENT_ACTION } - data object ProMode : NavDestination(isCompose = false) { + @Serializable + data object ProMode : NavDestination(isCompose = true) { override val id: String = ID_PRO_MODE } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt index 4d2e4e7a53..402ace3c87 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt @@ -351,11 +351,6 @@ private fun getDirection(destination: NavDestination<*>, requestKey: String): Na ) NavDestination.About -> NavBaseAppDirections.actionGlobalAboutFragment() - NavDestination.Settings -> NavBaseAppDirections.toSettingsFragment() - - NavDestination.ShizukuSettings -> NavBaseAppDirections.toShizukuSettingsFragment() - - NavDestination.ProMode -> NavBaseAppDirections.toProModeFragment() else -> throw IllegalArgumentException("Can not find a direction for this destination: $destination") } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/PageLinkButton.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/PageLinkButton.kt new file mode 100644 index 0000000000..04bc78581d --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/PageLinkButton.kt @@ -0,0 +1,64 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme + +@Composable +fun PageLinkButton( + modifier: Modifier = Modifier, + title: String, + text: String, + onClick: () -> Unit, +) { + Surface(modifier = modifier, onClick = onClick, shape = MaterialTheme.shapes.medium) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Icon( + modifier = Modifier.align(Alignment.CenterVertically), + imageVector = Icons.Rounded.ChevronRight, + contentDescription = null, + ) + } + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + PageLinkButton( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.title_pref_pro_mode), + text = stringResource(R.string.summary_pref_pro_mode), + onClick = {}, + ) + } +} diff --git a/base/src/main/res/navigation/nav_base_app.xml b/base/src/main/res/navigation/nav_base_app.xml index 936a5d993e..469fe210d6 100644 --- a/base/src/main/res/navigation/nav_base_app.xml +++ b/base/src/main/res/navigation/nav_base_app.xml @@ -5,22 +5,6 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_base_app"> - - - - - - - - - - \ No newline at end of file From 133b63bc2e5b030d42dfafe9750a09659b9470fc Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 10:53:44 +0100 Subject: [PATCH 140/215] #1394 show new settings to change theme and toggle key maps notification --- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 10 +- .../base/settings/ConfigSettingsUseCase.kt | 24 +++- .../base/settings/MainSettingsFragment.kt | 27 ++--- .../keymapper/base/settings/SettingsScreen.kt | 104 +++++++++++++++++- .../base/settings/SettingsViewModel.kt | 39 +++++++ .../sds100/keymapper/base/settings/Theme.kt | 7 ++ .../keymapper/base/settings/ThemeUtils.kt | 9 -- .../AndroidNotificationAdapter.kt | 12 ++ .../ui/compose/KeyMapperSegmentedButtonRow.kt | 6 +- .../{PageLinkButton.kt => OptionButton.kt} | 14 +-- .../base/utils/ui/compose/OptionPageButton.kt | 80 ++++++++++++++ .../base/utils/ui/compose/icons/WandStars.kt | 99 +++++++++++++++++ base/src/main/res/values/strings.xml | 27 ++--- .../sds100/keymapper/data/DataHiltModule.kt | 4 +- ...ository.kt => PreferenceRepositoryImpl.kt} | 2 +- .../notifications/NotificationAdapter.kt | 1 + 16 files changed, 396 insertions(+), 69 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/ThemeUtils.kt rename base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/{PageLinkButton.kt => OptionButton.kt} (79%) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt rename data/src/main/java/io/github/sds100/keymapper/data/repositories/{SettingsPreferenceRepository.kt => PreferenceRepositoryImpl.kt} (97%) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index af0367334b..666b4a18d8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -14,7 +14,7 @@ import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDexApplication import io.github.sds100.keymapper.base.logging.KeyMapperLoggingTree -import io.github.sds100.keymapper.base.settings.ThemeUtils +import io.github.sds100.keymapper.base.settings.Theme import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.inputmethod.AutoSwitchImeController import io.github.sds100.keymapper.base.system.notifications.NotificationController @@ -22,7 +22,7 @@ import io.github.sds100.keymapper.base.system.permissions.AutoGrantPermissionCon import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.repositories.LogRepository -import io.github.sds100.keymapper.data.repositories.SettingsPreferenceRepository +import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl import io.github.sds100.keymapper.system.apps.AndroidPackageManagerAdapter import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl @@ -75,7 +75,7 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { lateinit var loggingTree: KeyMapperLoggingTree @Inject - lateinit var settingsRepository: SettingsPreferenceRepository + lateinit var settingsRepository: PreferenceRepositoryImpl @Inject lateinit var logRepository: LogRepository @@ -144,8 +144,8 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { .map { it?.toIntOrNull() } .map { when (it) { - ThemeUtils.DARK -> AppCompatDelegate.MODE_NIGHT_YES - ThemeUtils.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + Theme.DARK.value -> AppCompatDelegate.MODE_NIGHT_YES + Theme.LIGHT.value -> AppCompatDelegate.MODE_NIGHT_NO else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index 5a810e6361..1e6960dbad 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -15,6 +15,7 @@ import io.github.sds100.keymapper.system.apps.PackageManagerAdapter import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputmethod.ImeInfo import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter +import io.github.sds100.keymapper.system.notifications.NotificationAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.root.SuAdapter @@ -37,6 +38,7 @@ class ConfigSettingsUseCaseImpl @Inject constructor( private val shizukuAdapter: ShizukuAdapter, private val devicesAdapter: DevicesAdapter, private val buildConfigProvider: BuildConfigProvider, + private val notificationAdapter: NotificationAdapter ) : ConfigSettingsUseCase { private val imeHelper by lazy { @@ -46,6 +48,11 @@ class ConfigSettingsUseCaseImpl @Inject constructor( ) } + override val theme: Flow = + preferences.get(Keys.darkTheme).map { it ?: PreferenceDefaults.DARK_THEME }.map { value -> + Theme.entries.single { it.value == value.toInt() } + } + override val isRootGranted: Flow = suAdapter.isRootGranted override val isWriteSecureSettingsGranted: Flow = channelFlow { @@ -90,9 +97,11 @@ class ConfigSettingsUseCaseImpl @Inject constructor( imeHelper.enableCompatibleInputMethods() } - override suspend fun chooseCompatibleIme(): KMResult = imeHelper.chooseCompatibleInputMethod() + override suspend fun chooseCompatibleIme(): KMResult = + imeHelper.chooseCompatibleInputMethod() - override suspend fun showImePicker(): KMResult<*> = inputMethodAdapter.showImePicker(fromForeground = true) + override suspend fun showImePicker(): KMResult<*> = + inputMethodAdapter.showImePicker(fromForeground = true) override fun getPreference(key: Preferences.Key) = preferences.get(key) @@ -166,7 +175,8 @@ class ConfigSettingsUseCaseImpl @Inject constructor( suAdapter.requestPermission() } - override fun isNotificationsPermissionGranted(): Boolean = permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS) + override fun isNotificationsPermissionGranted(): Boolean = + permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS) override fun getSoundFiles(): List = soundsManager.soundFiles.value @@ -179,11 +189,18 @@ class ConfigSettingsUseCaseImpl @Inject constructor( override fun resetAllSettings() { preferences.deleteAll() } + + override fun openNotificationChannelSettings(channelId: String) { + notificationAdapter.openChannelSettings(channelId) + } } interface ConfigSettingsUseCase { + // TODO delete these fun getPreference(key: Preferences.Key): Flow fun setPreference(key: Preferences.Key, value: T?) + + val theme: Flow val automaticBackupLocation: Flow fun setAutomaticBackupLocation(uri: String) fun disableAutomaticBackup() @@ -217,6 +234,7 @@ interface ConfigSettingsUseCase { fun requestWriteSecureSettingsPermission() fun requestNotificationsPermission() fun isNotificationsPermissionGranted(): Boolean + fun openNotificationChannelSettings(channelId: String) fun requestShizukuPermission() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt index 05a86246bd..f43b29e8d1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt @@ -10,7 +10,6 @@ import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle import androidx.navigation.fragment.findNavController -import androidx.preference.DropDownPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.SwitchPreferenceCompat @@ -20,11 +19,9 @@ import io.github.sds100.keymapper.base.backup.BackupUtils import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.base.utils.ui.str -import io.github.sds100.keymapper.base.utils.ui.strArray import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.notifications.NotificationUtils import io.github.sds100.keymapper.system.shizuku.ShizukuUtils @@ -140,18 +137,18 @@ class MainSettingsFragment : BaseSettingsFragment() { } // dark theme - DropDownPreference(requireContext()).apply { - key = Keys.darkTheme.name - setDefaultValue(PreferenceDefaults.DARK_THEME) - isSingleLineTitle = false - - setTitle(R.string.title_pref_dark_theme) - setSummary(R.string.summary_pref_dark_theme) - entries = strArray(R.array.pref_dark_theme_entries) - entryValues = ThemeUtils.THEMES.map { it.toString() }.toTypedArray() - - addPreference(this) - } +// DropDownPreference(requireContext()).apply { +// key = Keys.darkTheme.name +// setDefaultValue(PreferenceDefaults.DARK_THEME) +// isSingleLineTitle = false +// +// setTitle(R.string.title_pref_dark_theme) +// setSummary(R.string.summary_pref_dark_theme) +// entries = strArray(R.array.pref_dark_theme_entries) +// entryValues = Theme.THEMES.map { it.toString() }.toTypedArray() +// +// addPreference(this) +// } // automatic backup location Preference(requireContext()).apply { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index df2ab75a5a..f8ced361d0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -1,40 +1,69 @@ package io.github.sds100.keymapper.base.settings +import android.os.Build import androidx.compose.foundation.layout.Column +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.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.PlayCircleOutline import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton 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.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow +import io.github.sds100.keymapper.base.utils.ui.compose.OptionPageButton +import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.WandStars @Composable fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { - SettingsScreen(modifier, onBackClick = viewModel::onBackClick) { Content() } + val state by viewModel.mainScreenState.collectAsStateWithLifecycle() + + SettingsScreen( + modifier, + onBackClick = viewModel::onBackClick, + viewModel::onResetAllSettingsClick + ) { + Content( + state = state, + onThemeSelected = viewModel::onThemeSelected, + onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick + ) + } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SettingsScreen( modifier: Modifier = Modifier, - onBackClick: () -> Unit, + onBackClick: () -> Unit = {}, + onResetClick: () -> Unit = {}, content: @Composable () -> Unit ) { Scaffold( @@ -42,6 +71,14 @@ private fun SettingsScreen( topBar = { TopAppBar( title = { Text(stringResource(R.string.action_settings)) }, + actions = { + OutlinedButton( + modifier = Modifier.padding(horizontal = 16.dp), + onClick = onResetClick + ) { + Text(stringResource(R.string.settings_reset_app_bar_button)) + } + } ) }, bottomBar = { @@ -75,8 +112,59 @@ private fun SettingsScreen( } @Composable -private fun Content(modifier: Modifier = Modifier) { - Column(modifier.verticalScroll(rememberScrollState())) { +private fun Content( + modifier: Modifier = Modifier, + state: SettingsState, + onThemeSelected: (Theme) -> Unit = { }, + onPauseResumeNotificationClick: () -> Unit = { }, +) { + Column( + modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = KeyMapperIcons.WandStars, + text = stringResource(R.string.settings_section_customize_experience_title) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.title_pref_dark_theme), + style = MaterialTheme.typography.bodyLarge + ) + + val buttonStates: List> = listOf( + Theme.AUTO to stringResource(R.string.theme_system), + Theme.LIGHT to stringResource(R.string.theme_light), + Theme.DARK to stringResource(R.string.theme_dark), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + KeyMapperSegmentedButtonRow( + modifier = Modifier.fillMaxWidth(), + buttonStates, + state.theme, + onStateSelected = onThemeSelected, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + OptionPageButton( + title = stringResource(R.string.title_pref_show_toggle_keymaps_notification), + text = stringResource(R.string.summary_pref_show_toggle_keymaps_notification), + icon = Icons.Rounded.PlayCircleOutline, + onClick = onPauseResumeNotificationClick + ) + } + + Spacer(modifier = Modifier.height(8.dp)) } } @@ -84,7 +172,11 @@ private fun Content(modifier: Modifier = Modifier) { @Preview @Composable private fun Preview() { - SettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { - Content() + KeyMapperTheme { + SettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { + Content( + state = SettingsState() + ) + } } } \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index d39057bb19..dd30127943 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -1,10 +1,12 @@ package io.github.sds100.keymapper.base.settings +import android.os.Build import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider @@ -19,10 +21,12 @@ import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.common.utils.otherwise +import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.utils.SharedPrefsDataStoreWrapper import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -74,6 +78,16 @@ class SettingsViewModel @Inject constructor( val defaultVibrateDuration: Flow = useCase.defaultVibrateDuration val defaultRepeatRate: Flow = useCase.defaultRepeatRate + val mainScreenState: StateFlow = combine( + useCase.theme, + useCase.getPreference(Keys.log), + ) { theme, loggingEnabled -> + SettingsState( + theme = theme + ) + }.stateIn(viewModelScope, SharingStarted.Lazily, SettingsState()) + + fun setAutomaticBackupLocation(uri: String) = useCase.setAutomaticBackupLocation(uri) fun disableAutomaticBackup() = useCase.disableAutomaticBackup() @@ -233,4 +247,29 @@ class SettingsViewModel @Inject constructor( popBackStack() } } + + fun onThemeSelected(theme: Theme) { + viewModelScope.launch { + useCase.setPreference(Keys.darkTheme, theme.value.toString()) + } + } + + fun onPauseResumeNotificationClick() { + onNotificationSettingsClick(NotificationController.CHANNEL_TOGGLE_KEYMAPS) + } + + private fun onNotificationSettingsClick(channel: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + !useCase.isNotificationsPermissionGranted() + ) { + useCase.requestNotificationsPermission() + return + } + + useCase.openNotificationChannelSettings(channel) + } } + +data class SettingsState( + val theme: Theme = Theme.AUTO +) \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt new file mode 100644 index 0000000000..da513b5c79 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt @@ -0,0 +1,7 @@ +package io.github.sds100.keymapper.base.settings + +enum class Theme(val value: Int) { + DARK(0), + LIGHT(1), + AUTO(2); +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ThemeUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ThemeUtils.kt deleted file mode 100644 index 63d6110cf6..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ThemeUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -object ThemeUtils { - const val DARK = 0 - const val LIGHT = 1 - const val AUTO = 2 - - val THEMES = arrayOf(LIGHT, DARK, AUTO) -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt index 1fa74b2a49..fb9ed752f1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt @@ -5,6 +5,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build +import android.provider.Settings import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.google.android.material.color.DynamicColors @@ -101,6 +102,17 @@ class AndroidNotificationAdapter @Inject constructor( manager.deleteNotificationChannel(channelId) } + override fun openChannelSettings(channelId: String) { + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, ctx.packageName) + putExtra(Settings.EXTRA_CHANNEL_ID, channelId) + + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + ctx.startActivity(this) + } + } + fun onReceiveNotificationActionIntent(intent: Intent) { val actionId = intent.action ?: return diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt index 8c6a64ddd3..ff2bc00d0a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt @@ -90,13 +90,13 @@ fun KeyMapperSegmentedButtonRow( index = buttonStates.indexOf(content), count = buttonStates.size, ), - colors = colors + colors = colors, ) { Text( + modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier, text = label, maxLines = 2, - overflow = TextOverflow.Ellipsis, - modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier + overflow = TextOverflow.Ellipsis ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/PageLinkButton.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionButton.kt similarity index 79% rename from base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/PageLinkButton.kt rename to base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionButton.kt index 04bc78581d..4b1f1eeeae 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/PageLinkButton.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionButton.kt @@ -4,14 +4,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ChevronRight -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme 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.tooling.preview.Preview @@ -20,7 +16,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme @Composable -fun PageLinkButton( +fun OptionButton( modifier: Modifier = Modifier, title: String, text: String, @@ -40,12 +36,6 @@ fun PageLinkButton( color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - - Icon( - modifier = Modifier.align(Alignment.CenterVertically), - imageVector = Icons.Rounded.ChevronRight, - contentDescription = null, - ) } } } @@ -54,7 +44,7 @@ fun PageLinkButton( @Composable private fun Preview() { KeyMapperTheme { - PageLinkButton( + OptionButton( modifier = Modifier.fillMaxWidth(), title = stringResource(R.string.title_pref_pro_mode), text = stringResource(R.string.summary_pref_pro_mode), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt new file mode 100644 index 0000000000..f3c6519306 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt @@ -0,0 +1,80 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon + +@Composable +fun OptionPageButton( + modifier: Modifier = Modifier, + title: String, + text: String, + icon: ImageVector? = null, + onClick: () -> Unit, +) { + Surface(modifier = modifier, onClick = onClick, shape = MaterialTheme.shapes.medium) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + if (icon != null) { + Icon( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 16.dp), + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + + Column(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Icon( + modifier = Modifier.align(Alignment.CenterVertically), + imageVector = Icons.Rounded.ChevronRight, + contentDescription = null, + ) + } + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + OptionPageButton( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.title_pref_pro_mode), + text = stringResource(R.string.summary_pref_pro_mode), + icon = KeyMapperIcons.ProModeIcon, + onClick = {}, + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt new file mode 100644 index 0000000000..e25bf328da --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt @@ -0,0 +1,99 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.WandStars: ImageVector + get() { + if (_WandStars != null) { + return _WandStars!! + } + _WandStars = ImageVector.Builder( + name = "WandStars24Dp000000FILL0Wght400GRAD0Opsz24", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveToRelative(646f, 522f) + lineToRelative(-86f, 138f) + quadToRelative(-11f, 17f, -30.5f, 14f) + reflectiveQuadTo(505f, 651f) + lineToRelative(-28f, -112f) + lineToRelative(-273f, 273f) + quadToRelative(-11f, 11f, -27.5f, 11.5f) + reflectiveQuadTo(148f, 812f) + quadToRelative(-11f, -11f, -11f, -28f) + reflectiveQuadToRelative(11f, -28f) + lineToRelative(273f, -274f) + lineToRelative(-112f, -28f) + quadToRelative(-20f, -5f, -23f, -24.5f) + reflectiveQuadToRelative(14f, -30.5f) + lineToRelative(138f, -85f) + lineToRelative(-12f, -163f) + quadToRelative(-2f, -20f, 16f, -29f) + reflectiveQuadToRelative(33f, 4f) + lineToRelative(125f, 105f) + lineToRelative(151f, -61f) + quadToRelative(19f, -8f, 33f, 6f) + reflectiveQuadToRelative(6f, 33f) + lineToRelative(-61f, 151f) + lineToRelative(105f, 124f) + quadToRelative(13f, 15f, 4f, 33f) + reflectiveQuadToRelative(-29f, 16f) + lineToRelative(-163f, -11f) + close() + moveTo(134f, 254f) + quadToRelative(-6f, -6f, -6f, -14f) + reflectiveQuadToRelative(6f, -14f) + lineToRelative(52f, -52f) + quadToRelative(6f, -6f, 14f, -6f) + reflectiveQuadToRelative(14f, 6f) + lineToRelative(52f, 52f) + quadToRelative(6f, 6f, 6f, 14f) + reflectiveQuadToRelative(-6f, 14f) + lineToRelative(-52f, 52f) + quadToRelative(-6f, 6f, -14f, 6f) + reflectiveQuadToRelative(-14f, -6f) + lineToRelative(-52f, -52f) + close() + moveTo(555f, 517f) + lineTo(603f, 438f) + lineTo(696f, 445f) + lineTo(636f, 374f) + lineTo(671f, 288f) + lineTo(585f, 323f) + lineTo(514f, 264f) + lineTo(521f, 356f) + lineTo(442f, 405f) + lineTo(532f, 427f) + lineTo(555f, 517f) + close() + moveTo(706f, 826f) + lineTo(654f, 774f) + quadToRelative(-6f, -6f, -6f, -14f) + reflectiveQuadToRelative(6f, -14f) + lineToRelative(52f, -52f) + quadToRelative(6f, -6f, 14f, -6f) + reflectiveQuadToRelative(14f, 6f) + lineToRelative(52f, 52f) + quadToRelative(6f, 6f, 6f, 14f) + reflectiveQuadToRelative(-6f, 14f) + lineToRelative(-52f, 52f) + quadToRelative(-6f, 6f, -14f, 6f) + reflectiveQuadToRelative(-14f, -6f) + close() + moveTo(569f, 390f) + close() + } + }.build() + + return _WandStars!! + } + +@Suppress("ObjectPropertyName") +private var _WandStars: ImageVector? = null diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index d91fa48d31..4c8b4e4b42 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -614,8 +614,8 @@ Keyboard picker notification Show a persistent notification to allow you to pick a keyboard. - Pause/resume key maps notification - Show a persistent notification which starts/pauses your key maps. + Show pause/resume notification + Toggle your key maps on and off. Automatically back up key maps to a specified location No location chosen. @@ -690,12 +690,18 @@ Default mapping options Change the default options for your key maps. - Reset all settings + Reset all DANGER! Reset all settings in the app to the default. Your key maps will NOT be reset. DANGER! 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 + Reset all + Customize your experience + Key maps + Data management + Power user options + Debugging @@ -719,18 +725,13 @@ Logging This may add latency to your key maps so only turn this on if you are trying to debug the app or have been asked to by the developer. - PRO mode + Use PRO mode Advanced detection of key events and more. - - - - - - Light - Dark - Follow system - + Light + Dark + System + Show volume dialog diff --git a/data/src/main/java/io/github/sds100/keymapper/data/DataHiltModule.kt b/data/src/main/java/io/github/sds100/keymapper/data/DataHiltModule.kt index 0dd446dbfe..4fd6319c42 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/DataHiltModule.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/DataHiltModule.kt @@ -11,13 +11,13 @@ import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl import io.github.sds100.keymapper.data.repositories.RoomAccessibilityNodeRepository import io.github.sds100.keymapper.data.repositories.RoomFloatingButtonRepository import io.github.sds100.keymapper.data.repositories.RoomFloatingLayoutRepository 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 javax.inject.Singleton @Module @@ -25,7 +25,7 @@ import javax.inject.Singleton abstract class DataHiltModule { @Singleton @Binds - abstract fun providePreferenceRepository(impl: SettingsPreferenceRepository): PreferenceRepository + abstract fun providePreferenceRepository(impl: PreferenceRepositoryImpl): PreferenceRepository @Singleton @Binds diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepositoryImpl.kt similarity index 97% rename from data/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt rename to data/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepositoryImpl.kt index 8d7f330d20..e07a4d6d77 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepositoryImpl.kt @@ -14,7 +14,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class SettingsPreferenceRepository @Inject constructor( +class PreferenceRepositoryImpl @Inject constructor( @ApplicationContext context: Context, private val coroutineScope: CoroutineScope, ) : PreferenceRepository { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt index 0b61b3a015..d59ce18d0b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt @@ -13,4 +13,5 @@ interface NotificationAdapter { fun dismissNotification(notificationId: Int) fun createChannel(channel: NotificationChannelModel) fun deleteChannel(channelId: String) + fun openChannelSettings(channelId: String) } From ceea3c977f97a8122e8357156a03e685e329d07a Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 11:15:23 +0100 Subject: [PATCH 141/215] #1394 create new default settings screen with long press delay --- .../sds100/keymapper/base/BaseMainNavHost.kt | 10 ++ .../settings/DefaultOptionsSettingsScreen.kt | 135 ++++++++++++++++++ .../keymapper/base/settings/SettingsScreen.kt | 54 ++++++- .../base/settings/SettingsViewModel.kt | 42 +++++- .../base/utils/navigation/NavDestination.kt | 6 + .../utils/ui/compose/icons/FolderManaged.kt | 131 +++++++++++++++++ base/src/main/res/values/strings.xml | 4 +- 7 files changed, 372 insertions(+), 10 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt index 574e89725b..61f3c5f591 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt @@ -24,6 +24,7 @@ import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel import io.github.sds100.keymapper.base.promode.ProModeScreen import io.github.sds100.keymapper.base.promode.ProModeSetupScreen +import io.github.sds100.keymapper.base.settings.DefaultOptionsSettingsScreen import io.github.sds100.keymapper.base.settings.SettingsScreen import io.github.sds100.keymapper.base.settings.SettingsViewModel import io.github.sds100.keymapper.base.utils.navigation.NavDestination @@ -85,6 +86,15 @@ fun BaseMainNavHost( ) } + composable { + val viewModel: SettingsViewModel = hiltViewModel() + + DefaultOptionsSettingsScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + ) + } + composable { ProModeScreen( modifier = Modifier diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt new file mode 100644 index 0000000000..5a0a84f013 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt @@ -0,0 +1,135 @@ +package io.github.sds100.keymapper.base.settings + +import androidx.compose.foundation.layout.Column +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.SliderMaximums +import io.github.sds100.keymapper.base.utils.ui.SliderMinimums +import io.github.sds100.keymapper.base.utils.ui.SliderStepSizes +import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText + +@Composable +fun DefaultOptionsSettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { + val state by viewModel.defaultSettingsScreenState.collectAsStateWithLifecycle() + + DefaultOptionsSettingsScreen( + modifier, + onBackClick = viewModel::onBackClick, + ) { + Content( + state = state, + onLongPressDelayChanged = viewModel::onLongPressDelayChanged + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DefaultOptionsSettingsScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + content: @Composable () -> Unit +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title_pref_default_options)) }, + ) + }, + bottomBar = { + BottomAppBar { + 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, state: DefaultSettingsState, + onLongPressDelayChanged: (Int) -> Unit = { }, +) { + Column( + modifier.verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.extra_label_long_press_delay_timeout), + defaultValue = state.defaultLongPressDelay.toFloat(), + value = state.longPressDelay.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { onLongPressDelayChanged(it.toInt()) }, + valueRange = SliderMinimums.TRIGGER_LONG_PRESS_DELAY.toFloat()..SliderMaximums.TRIGGER_LONG_PRESS_DELAY.toFloat(), + stepSize = SliderStepSizes.TRIGGER_LONG_PRESS_DELAY, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + DefaultOptionsSettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { + Content( + state = DefaultSettingsState() + ) + } + } +} \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index f8ced361d0..a2ded0151b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -14,7 +14,11 @@ 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.outlined.Gamepad +import androidx.compose.material.icons.rounded.Code +import androidx.compose.material.icons.rounded.Construction import androidx.compose.material.icons.rounded.PlayCircleOutline +import androidx.compose.material.icons.rounded.Tune import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -38,6 +42,7 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.OptionPageButton import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.icons.FolderManaged import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons import io.github.sds100.keymapper.base.utils.ui.compose.icons.WandStars @@ -53,7 +58,8 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) Content( state = state, onThemeSelected = viewModel::onThemeSelected, - onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick + onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick, + onDefaultOptionsClick = viewModel::onDefaultOptionsClick ) } } @@ -114,9 +120,10 @@ private fun SettingsScreen( @Composable private fun Content( modifier: Modifier = Modifier, - state: SettingsState, + state: MainSettingsState, onThemeSelected: (Theme) -> Unit = { }, onPauseResumeNotificationClick: () -> Unit = { }, + onDefaultOptionsClick: () -> Unit = { } ) { Column( modifier @@ -166,6 +173,47 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.Gamepad, + text = stringResource(R.string.settings_section_key_maps_title) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = stringResource(R.string.title_pref_default_options), + text = stringResource(R.string.summary_pref_default_options), + icon = Icons.Rounded.Tune, + onClick = onDefaultOptionsClick + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = KeyMapperIcons.FolderManaged, + text = stringResource(R.string.settings_section_data_management_title) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Rounded.Construction, + text = stringResource(R.string.settings_section_power_user_title) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Rounded.Code, + text = stringResource(R.string.settings_section_debugging_title) + ) + + Spacer(modifier = Modifier.height(8.dp)) + } } @@ -175,7 +223,7 @@ private fun Preview() { KeyMapperTheme { SettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { Content( - state = SettingsState() + state = MainSettingsState() ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index dd30127943..906553265d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -22,6 +22,7 @@ import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.common.utils.otherwise import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.utils.SharedPrefsDataStoreWrapper import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -78,15 +79,26 @@ class SettingsViewModel @Inject constructor( val defaultVibrateDuration: Flow = useCase.defaultVibrateDuration val defaultRepeatRate: Flow = useCase.defaultRepeatRate - val mainScreenState: StateFlow = combine( + val mainScreenState: StateFlow = combine( useCase.theme, useCase.getPreference(Keys.log), ) { theme, loggingEnabled -> - SettingsState( + MainSettingsState( theme = theme ) - }.stateIn(viewModelScope, SharingStarted.Lazily, SettingsState()) - + }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) + + val defaultSettingsScreenState: StateFlow = combine( + useCase.getPreference(Keys.defaultLongPressDelay), + useCase.getPreference(Keys.defaultDoublePressDelay), + ) { longPressDelay, doublePressDelay -> + DefaultSettingsState( + longPressDelay = longPressDelay ?: PreferenceDefaults.LONG_PRESS_DELAY, + defaultLongPressDelay = PreferenceDefaults.LONG_PRESS_DELAY, + doublePressDelay = doublePressDelay ?: PreferenceDefaults.DOUBLE_PRESS_DELAY, + defaultDoublePressDelay = PreferenceDefaults.DOUBLE_PRESS_DELAY, + ) + }.stateIn(viewModelScope, SharingStarted.Lazily, DefaultSettingsState()) fun setAutomaticBackupLocation(uri: String) = useCase.setAutomaticBackupLocation(uri) fun disableAutomaticBackup() = useCase.disableAutomaticBackup() @@ -258,6 +270,18 @@ class SettingsViewModel @Inject constructor( onNotificationSettingsClick(NotificationController.CHANNEL_TOGGLE_KEYMAPS) } + fun onDefaultOptionsClick() { + viewModelScope.launch { + navigate("default_options", NavDestination.DefaultOptionsSettings) + } + } + + fun onLongPressDelayChanged(newValue: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultLongPressDelay, newValue) + } + } + private fun onNotificationSettingsClick(channel: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !useCase.isNotificationsPermissionGranted() @@ -270,6 +294,14 @@ class SettingsViewModel @Inject constructor( } } -data class SettingsState( +data class MainSettingsState( val theme: Theme = Theme.AUTO +) + +data class DefaultSettingsState( + val longPressDelay: Int = PreferenceDefaults.LONG_PRESS_DELAY, + val defaultLongPressDelay: Int = PreferenceDefaults.LONG_PRESS_DELAY, + + val doublePressDelay: Int = PreferenceDefaults.DOUBLE_PRESS_DELAY, + val defaultDoublePressDelay: Int = PreferenceDefaults.DOUBLE_PRESS_DELAY, ) \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt index e77b96dc31..fe2e8cdbaf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt @@ -31,6 +31,7 @@ abstract class NavDestination(val isCompose: Boolean = false) { const val ID_CHOOSE_CONSTRAINT = "choose_constraint" const val ID_CHOOSE_BLUETOOTH_DEVICE = "choose_bluetooth_device" const val ID_SETTINGS = "settings" + const val ID_DEFAULT_OPTIONS_SETTINGS = "default_options_settings" const val ID_ABOUT = "about" const val ID_CONFIG_KEY_MAP = "config_key_map" const val ID_INTERACT_UI_ELEMENT_ACTION = "interact_ui_element_action" @@ -122,6 +123,11 @@ abstract class NavDestination(val isCompose: Boolean = false) { override val id: String = ID_SETTINGS } + @Serializable + data object DefaultOptionsSettings : NavDestination(isCompose = true) { + override val id: String = ID_DEFAULT_OPTIONS_SETTINGS + } + @Serializable data object About : NavDestination() { override val id: String = ID_ABOUT diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt new file mode 100644 index 0000000000..aaf9195c9b --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt @@ -0,0 +1,131 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.FolderManaged: ImageVector + get() { + if (_FolderManaged != null) { + return _FolderManaged!! + } + _FolderManaged = ImageVector.Builder( + name = "FolderManaged", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(720f, 760f) + quadToRelative(33f, 0f, 56.5f, -23.5f) + reflectiveQuadTo(800f, 680f) + quadToRelative(0f, -33f, -23.5f, -56.5f) + reflectiveQuadTo(720f, 600f) + quadToRelative(-33f, 0f, -56.5f, 23.5f) + reflectiveQuadTo(640f, 680f) + quadToRelative(0f, 33f, 23.5f, 56.5f) + reflectiveQuadTo(720f, 760f) + close() + moveTo(712f, 880f) + quadToRelative(-14f, 0f, -24.5f, -9f) + reflectiveQuadTo(674f, 848f) + lineToRelative(-6f, -28f) + quadToRelative(-12f, -5f, -22.5f, -10.5f) + reflectiveQuadTo(624f, 796f) + lineToRelative(-29f, 9f) + quadToRelative(-13f, 4f, -25.5f, -1f) + reflectiveQuadTo(550f, 788f) + lineToRelative(-8f, -14f) + quadToRelative(-7f, -12f, -5f, -26f) + reflectiveQuadToRelative(13f, -23f) + lineToRelative(22f, -19f) + quadToRelative(-2f, -12f, -2f, -26f) + reflectiveQuadToRelative(2f, -26f) + lineToRelative(-22f, -19f) + quadToRelative(-11f, -9f, -13f, -22.5f) + reflectiveQuadToRelative(5f, -25.5f) + lineToRelative(9f, -15f) + quadToRelative(7f, -11f, 19f, -16f) + reflectiveQuadToRelative(25f, -1f) + lineToRelative(29f, 9f) + quadToRelative(11f, -8f, 21.5f, -13.5f) + reflectiveQuadTo(668f, 540f) + lineToRelative(6f, -29f) + quadToRelative(3f, -14f, 13.5f, -22.5f) + reflectiveQuadTo(712f, 480f) + horizontalLineToRelative(16f) + quadToRelative(14f, 0f, 24.5f, 9f) + reflectiveQuadToRelative(13.5f, 23f) + lineToRelative(6f, 28f) + quadToRelative(12f, 5f, 22.5f, 10.5f) + reflectiveQuadTo(816f, 564f) + lineToRelative(29f, -9f) + quadToRelative(13f, -4f, 25.5f, 1f) + reflectiveQuadToRelative(19.5f, 16f) + lineToRelative(8f, 14f) + quadToRelative(7f, 12f, 5f, 26f) + reflectiveQuadToRelative(-13f, 23f) + lineToRelative(-22f, 19f) + quadToRelative(2f, 12f, 2f, 26f) + reflectiveQuadToRelative(-2f, 26f) + lineToRelative(22f, 19f) + quadToRelative(11f, 9f, 13f, 22.5f) + reflectiveQuadToRelative(-5f, 25.5f) + lineToRelative(-9f, 15f) + quadToRelative(-7f, 11f, -19f, 16f) + reflectiveQuadToRelative(-25f, 1f) + lineToRelative(-29f, -9f) + quadToRelative(-11f, 8f, -21.5f, 13.5f) + reflectiveQuadTo(772f, 820f) + lineToRelative(-6f, 29f) + quadToRelative(-3f, 14f, -13.5f, 22.5f) + reflectiveQuadTo(728f, 880f) + horizontalLineToRelative(-16f) + close() + moveTo(160f, 720f) + verticalLineToRelative(-480f) + verticalLineToRelative(172f) + verticalLineToRelative(-12f) + verticalLineToRelative(320f) + close() + moveTo(160f, 800f) + quadToRelative(-33f, 0f, -56.5f, -23.5f) + reflectiveQuadTo(80f, 720f) + verticalLineToRelative(-480f) + quadToRelative(0f, -33f, 23.5f, -56.5f) + reflectiveQuadTo(160f, 160f) + horizontalLineToRelative(207f) + quadToRelative(16f, 0f, 30.5f, 6f) + reflectiveQuadToRelative(25.5f, 17f) + lineToRelative(57f, 57f) + horizontalLineToRelative(320f) + quadToRelative(33f, 0f, 56.5f, 23.5f) + reflectiveQuadTo(880f, 320f) + verticalLineToRelative(80f) + quadToRelative(0f, 17f, -11.5f, 28.5f) + reflectiveQuadTo(840f, 440f) + quadToRelative(-17f, 0f, -28.5f, -11.5f) + reflectiveQuadTo(800f, 400f) + verticalLineToRelative(-80f) + lineTo(447f, 320f) + lineToRelative(-80f, -80f) + lineTo(160f, 240f) + verticalLineToRelative(480f) + horizontalLineToRelative(280f) + quadToRelative(17f, 0f, 28.5f, 11.5f) + reflectiveQuadTo(480f, 760f) + quadToRelative(0f, 17f, -11.5f, 28.5f) + reflectiveQuadTo(440f, 800f) + lineTo(160f, 800f) + close() + } + }.build() + + return _FolderManaged!! + } + +@Suppress("ObjectPropertyName") +private var _FolderManaged: ImageVector? = null diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 4c8b4e4b42..43562c2c1b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -687,8 +687,8 @@ 3. Key Mapper does not have permission to use Shizuku. Tap to grant this permission. 3. Key Mapper will automatically use Shizuku. Tap to read which Key Mapper features use Shizuku. - Default mapping options - Change the default options for your key maps. + Change default options + For triggers and actions Reset all DANGER! Reset all settings in the app to the default. Your key maps will NOT be reset. From 37237e92b6bbb38eeed3c240a5d6c82eca0862e2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 11:40:42 +0100 Subject: [PATCH 142/215] #1394 complete settings screen for default options --- .../settings/DefaultOptionsSettingsScreen.kt | 105 ++++++++++++++++-- .../base/settings/SettingsViewModel.kt | 67 ++++++++++- 2 files changed, 157 insertions(+), 15 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt index 5a0a84f013..edcd149615 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt @@ -46,14 +46,14 @@ fun DefaultOptionsSettingsScreen(modifier: Modifier = Modifier, viewModel: Setti ) { Content( state = state, - onLongPressDelayChanged = viewModel::onLongPressDelayChanged + callback = viewModel ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun DefaultOptionsSettingsScreen( +fun DefaultOptionsSettingsScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit = {}, content: @Composable () -> Unit @@ -97,31 +97,118 @@ private fun DefaultOptionsSettingsScreen( @Composable private fun Content( - modifier: Modifier = Modifier, state: DefaultSettingsState, - onLongPressDelayChanged: (Int) -> Unit = { }, + modifier: Modifier = Modifier, + state: DefaultSettingsState, + callback: DefaultOptionsSettingsCallback = object : DefaultOptionsSettingsCallback {}, ) { Column( - modifier.verticalScroll(rememberScrollState()) + modifier = modifier + .verticalScroll(rememberScrollState()) + .fillMaxWidth(), ) { Spacer(modifier = Modifier.height(8.dp)) + // Long press delay SliderOptionText( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - title = stringResource(R.string.extra_label_long_press_delay_timeout), + title = stringResource(R.string.title_pref_long_press_delay), defaultValue = state.defaultLongPressDelay.toFloat(), value = state.longPressDelay.toFloat(), valueText = { "${it.toInt()} ms" }, - onValueChange = { onLongPressDelayChanged(it.toInt()) }, + onValueChange = { callback.onLongPressDelayChanged(it.toInt()) }, valueRange = SliderMinimums.TRIGGER_LONG_PRESS_DELAY.toFloat()..SliderMaximums.TRIGGER_LONG_PRESS_DELAY.toFloat(), stepSize = SliderStepSizes.TRIGGER_LONG_PRESS_DELAY, ) + Spacer(Modifier.height(8.dp)) - Spacer(modifier = Modifier.height(8.dp)) + // Double press delay + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_double_press_delay), + defaultValue = state.defaultDoublePressDelay.toFloat(), + value = state.doublePressDelay.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onDoublePressDelayChanged(it.toInt()) }, + valueRange = SliderMinimums.TRIGGER_DOUBLE_PRESS_DELAY.toFloat()..SliderMaximums.TRIGGER_DOUBLE_PRESS_DELAY.toFloat(), + stepSize = SliderStepSizes.TRIGGER_DOUBLE_PRESS_DELAY, + ) + Spacer(Modifier.height(8.dp)) + + // Vibrate duration + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_vibration_duration), + defaultValue = state.defaultVibrateDuration.toFloat(), + value = state.vibrateDuration.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onVibrateDurationChanged(it.toInt()) }, + valueRange = SliderMinimums.VIBRATION_DURATION.toFloat()..SliderMaximums.VIBRATION_DURATION.toFloat(), + stepSize = SliderStepSizes.VIBRATION_DURATION, + ) + Spacer(Modifier.height(8.dp)) + + // Repeat delay + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_repeat_delay), + defaultValue = state.defaultRepeatDelay.toFloat(), + value = state.repeatDelay.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onRepeatDelayChanged(it.toInt()) }, + valueRange = SliderMinimums.ACTION_REPEAT_DELAY.toFloat()..SliderMaximums.ACTION_REPEAT_DELAY.toFloat(), + stepSize = SliderStepSizes.ACTION_REPEAT_DELAY, + ) + Spacer(Modifier.height(8.dp)) + + // Repeat rate + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_repeat_rate), + defaultValue = state.defaultRepeatRate.toFloat(), + value = state.repeatRate.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onRepeatRateChanged(it.toInt()) }, + valueRange = SliderMinimums.ACTION_REPEAT_RATE.toFloat()..SliderMaximums.ACTION_REPEAT_RATE.toFloat(), + stepSize = SliderStepSizes.ACTION_REPEAT_RATE, + ) + Spacer(Modifier.height(8.dp)) + + // Sequence trigger timeout + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_sequence_trigger_timeout), + defaultValue = state.defaultSequenceTriggerTimeout.toFloat(), + value = state.sequenceTriggerTimeout.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onSequenceTriggerTimeoutChanged(it.toInt()) }, + valueRange = SliderMinimums.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT.toFloat()..SliderMaximums.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT.toFloat(), + stepSize = SliderStepSizes.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT, + ) + Spacer(Modifier.height(8.dp)) } } +interface DefaultOptionsSettingsCallback { + fun onLongPressDelayChanged(delay: Int) = run { } + fun onDoublePressDelayChanged(delay: Int) = run { } + fun onVibrateDurationChanged(duration: Int) = run { } + fun onRepeatDelayChanged(delay: Int) = run { } + fun onRepeatRateChanged(rate: Int) = run { } + fun onSequenceTriggerTimeoutChanged(timeout: Int) = run { } +} + @Preview @Composable private fun Preview() { @@ -132,4 +219,4 @@ private fun Preview() { ) } } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 906553265d..2ded652186 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -43,7 +43,8 @@ class SettingsViewModel @Inject constructor( ) : ViewModel(), DialogProvider by dialogProvider, ResourceProvider by resourceProvider, - NavigationProvider by navigationProvider { + NavigationProvider by navigationProvider, + DefaultOptionsSettingsCallback { val automaticBackupLocation = useCase.automaticBackupLocation @@ -91,12 +92,24 @@ class SettingsViewModel @Inject constructor( val defaultSettingsScreenState: StateFlow = combine( useCase.getPreference(Keys.defaultLongPressDelay), useCase.getPreference(Keys.defaultDoublePressDelay), - ) { longPressDelay, doublePressDelay -> + useCase.getPreference(Keys.defaultVibrateDuration), + useCase.getPreference(Keys.defaultRepeatDelay), + useCase.getPreference(Keys.defaultRepeatRate), + useCase.getPreference(Keys.defaultSequenceTriggerTimeout), + ) { values -> DefaultSettingsState( - longPressDelay = longPressDelay ?: PreferenceDefaults.LONG_PRESS_DELAY, + longPressDelay = values[0] ?: PreferenceDefaults.LONG_PRESS_DELAY, defaultLongPressDelay = PreferenceDefaults.LONG_PRESS_DELAY, - doublePressDelay = doublePressDelay ?: PreferenceDefaults.DOUBLE_PRESS_DELAY, + doublePressDelay = values[1] ?: PreferenceDefaults.DOUBLE_PRESS_DELAY, defaultDoublePressDelay = PreferenceDefaults.DOUBLE_PRESS_DELAY, + vibrateDuration = values[2] ?: PreferenceDefaults.VIBRATION_DURATION, + defaultVibrateDuration = PreferenceDefaults.VIBRATION_DURATION, + repeatDelay = values[3] ?: PreferenceDefaults.REPEAT_DELAY, + defaultRepeatDelay = PreferenceDefaults.REPEAT_DELAY, + repeatRate = values[4] ?: PreferenceDefaults.REPEAT_RATE, + defaultRepeatRate = PreferenceDefaults.REPEAT_RATE, + sequenceTriggerTimeout = values[5] ?: PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, + defaultSequenceTriggerTimeout = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, ) }.stateIn(viewModelScope, SharingStarted.Lazily, DefaultSettingsState()) @@ -276,9 +289,39 @@ class SettingsViewModel @Inject constructor( } } - fun onLongPressDelayChanged(newValue: Int) { + override fun onLongPressDelayChanged(delay: Int) { viewModelScope.launch { - useCase.setPreference(Keys.defaultLongPressDelay, newValue) + useCase.setPreference(Keys.defaultLongPressDelay, delay) + } + } + + override fun onDoublePressDelayChanged(delay: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultDoublePressDelay, delay) + } + } + + override fun onVibrateDurationChanged(duration: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultVibrateDuration, duration) + } + } + + override fun onRepeatDelayChanged(delay: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultRepeatDelay, delay) + } + } + + override fun onRepeatRateChanged(rate: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultRepeatRate, rate) + } + } + + override fun onSequenceTriggerTimeoutChanged(timeout: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultSequenceTriggerTimeout, timeout) } } @@ -304,4 +347,16 @@ data class DefaultSettingsState( val doublePressDelay: Int = PreferenceDefaults.DOUBLE_PRESS_DELAY, val defaultDoublePressDelay: Int = PreferenceDefaults.DOUBLE_PRESS_DELAY, + + val vibrateDuration: Int = PreferenceDefaults.VIBRATION_DURATION, + val defaultVibrateDuration: Int = PreferenceDefaults.VIBRATION_DURATION, + + val repeatDelay: Int = PreferenceDefaults.REPEAT_DELAY, + val defaultRepeatDelay: Int = PreferenceDefaults.REPEAT_DELAY, + + val repeatRate: Int = PreferenceDefaults.REPEAT_RATE, + val defaultRepeatRate: Int = PreferenceDefaults.REPEAT_RATE, + + val sequenceTriggerTimeout: Int = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, + val defaultSequenceTriggerTimeout: Int = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, ) \ No newline at end of file From 61513cdeb431261e566d7661afae9e924f06aa01 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 13:30:05 +0100 Subject: [PATCH 143/215] #1394 add setting to turn on automatic back up --- .../base/settings/ConfigSettingsUseCase.kt | 5 +- .../base/settings/MainSettingsFragment.kt | 71 ------------ .../keymapper/base/settings/SettingsScreen.kt | 101 +++++++++++++++++- .../base/settings/SettingsViewModel.kt | 23 ++-- base/src/main/res/values/strings.xml | 8 +- 5 files changed, 112 insertions(+), 96 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index 1e6960dbad..8bd5304bf9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -108,7 +108,7 @@ class ConfigSettingsUseCaseImpl @Inject constructor( override fun setPreference(key: Preferences.Key, value: T?) = preferences.set(key, value) override val automaticBackupLocation = - preferences.get(Keys.automaticBackupLocation).map { it ?: "" } + preferences.get(Keys.automaticBackupLocation) override fun setAutomaticBackupLocation(uri: String) { preferences.set(Keys.automaticBackupLocation, uri) @@ -196,12 +196,11 @@ class ConfigSettingsUseCaseImpl @Inject constructor( } interface ConfigSettingsUseCase { - // TODO delete these fun getPreference(key: Preferences.Key): Flow fun setPreference(key: Preferences.Key, value: T?) val theme: Flow - val automaticBackupLocation: Flow + val automaticBackupLocation: Flow fun setAutomaticBackupLocation(uri: String) fun disableAutomaticBackup() val isRootGranted: Flow diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt index f43b29e8d1..3d2241a257 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt @@ -1,12 +1,9 @@ package io.github.sds100.keymapper.base.settings import android.annotation.SuppressLint -import android.content.ActivityNotFoundException -import android.content.Intent import android.os.Build import android.os.Bundle import android.view.View -import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle import androidx.navigation.fragment.findNavController @@ -15,21 +12,14 @@ import androidx.preference.PreferenceCategory import androidx.preference.SwitchPreferenceCompat import androidx.preference.isEmpty import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.backup.BackupUtils import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.str import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.notifications.NotificationUtils import io.github.sds100.keymapper.system.shizuku.ShizukuUtils import kotlinx.coroutines.flow.collectLatest -import splitties.alertdialog.appcompat.alertDialog -import splitties.alertdialog.appcompat.messageResource -import splitties.alertdialog.appcompat.negativeButton -import splitties.alertdialog.appcompat.positiveButton class MainSettingsFragment : BaseSettingsFragment() { @@ -42,18 +32,6 @@ class MainSettingsFragment : BaseSettingsFragment() { "pref_automatically_change_ime_link" } - private val chooseAutomaticBackupLocationLauncher = - registerForActivityResult(CreateDocument(FileUtils.MIME_TYPE_ZIP)) { - it ?: return@registerForActivityResult - - viewModel.setAutomaticBackupLocation(it.toString()) - - val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - requireContext().contentResolver.takePersistableUriPermission(it, takeFlags) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper addPreferencesFromResource(R.xml.preferences_empty) @@ -68,19 +46,6 @@ class MainSettingsFragment : BaseSettingsFragment() { } } - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.automaticBackupLocation.collectLatest { backupLocation -> - val preference = - findPreference(Keys.automaticBackupLocation.name) - ?: return@collectLatest - preference.summary = if (backupLocation.isBlank()) { - str(R.string.summary_pref_automatic_backup_location_disabled) - } else { - backupLocation - } - } - } - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { viewModel.isWriteSecureSettingsPermissionGranted.collectLatest { isGranted -> val writeSecureSettingsCategory = @@ -151,42 +116,6 @@ class MainSettingsFragment : BaseSettingsFragment() { // } // automatic backup location - Preference(requireContext()).apply { - key = Keys.automaticBackupLocation.name - setDefaultValue("") - - setTitle(R.string.title_pref_automatic_backup_location) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val backupLocation = viewModel.automaticBackupLocation.firstBlocking() - - if (backupLocation.isBlank()) { - try { - chooseAutomaticBackupLocationLauncher.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) - } catch (e: ActivityNotFoundException) { - viewModel.onCreateBackupFileActivityNotFound() - } - } else { - requireContext().alertDialog { - messageResource = R.string.dialog_message_change_location_or_disable - - positiveButton(R.string.pos_change_location) { - chooseAutomaticBackupLocationLauncher.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) - } - - negativeButton(R.string.neg_turn_off) { - viewModel.disableAutomaticBackup() - } - - show() - } - } - - true - } - addPreference(this) - } // hide home screen alerts SwitchPreferenceCompat(requireContext()).apply { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index a2ded0151b..8d7de440e1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -1,6 +1,10 @@ package io.github.sds100.keymapper.base.settings +import android.content.ActivityNotFoundException +import android.content.Intent import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateEndPadding @@ -19,6 +23,7 @@ import androidx.compose.material.icons.rounded.Code import androidx.compose.material.icons.rounded.Construction import androidx.compose.material.icons.rounded.PlayCircleOutline import androidx.compose.material.icons.rounded.Tune +import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -26,18 +31,27 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.backup.BackupUtils import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.OptionPageButton @@ -45,21 +59,85 @@ import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.icons.FolderManaged import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons import io.github.sds100.keymapper.base.utils.ui.compose.icons.WandStars +import io.github.sds100.keymapper.system.files.FileUtils +import kotlinx.coroutines.launch @Composable fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { val state by viewModel.mainScreenState.collectAsStateWithLifecycle() + val snackbarHostState = SnackbarHostState() + var showAutomaticBackupDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + val scope = rememberCoroutineScope() + + + val automaticBackupLocationChooser = + rememberLauncherForActivityResult(CreateDocument(FileUtils.MIME_TYPE_ZIP)) { uri -> + uri ?: return@rememberLauncherForActivityResult + viewModel.setAutomaticBackupLocation(uri.toString()) + + val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + context.contentResolver.takePersistableUriPermission(uri, takeFlags) + } + + if (showAutomaticBackupDialog) { + val activityNotFoundText = + stringResource(R.string.dialog_message_no_app_found_to_create_file) + + AlertDialog( + onDismissRequest = { showAutomaticBackupDialog = false }, + title = { Text(stringResource(R.string.dialog_title_change_location_or_disable)) }, + text = { Text(stringResource(R.string.dialog_message_change_location_or_disable)) }, + confirmButton = { + TextButton( + onClick = { + showAutomaticBackupDialog = false + + try { + automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) + } catch (e: ActivityNotFoundException) { + scope.launch { + snackbarHostState.showSnackbar(activityNotFoundText) + } + } + } + ) { + Text(stringResource(R.string.pos_change_location)) + } + }, + dismissButton = { + TextButton( + onClick = { + showAutomaticBackupDialog = false + viewModel.disableAutomaticBackup() + } + ) { + Text(stringResource(R.string.neg_turn_off)) + } + } + ) + } SettingsScreen( modifier, onBackClick = viewModel::onBackClick, - viewModel::onResetAllSettingsClick + viewModel::onResetAllSettingsClick, + snackbarHostState = snackbarHostState ) { Content( state = state, onThemeSelected = viewModel::onThemeSelected, onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick, - onDefaultOptionsClick = viewModel::onDefaultOptionsClick + onDefaultOptionsClick = viewModel::onDefaultOptionsClick, + onAutomaticBackupClick = { + if (state.autoBackupLocation.isNullOrBlank()) { + automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) + } else { + showAutomaticBackupDialog = true + } + } ) } } @@ -70,10 +148,12 @@ private fun SettingsScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit = {}, onResetClick: () -> Unit = {}, + snackbarHostState: SnackbarHostState = SnackbarHostState(), content: @Composable () -> Unit ) { Scaffold( modifier = modifier.displayCutoutPadding(), + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( title = { Text(stringResource(R.string.action_settings)) }, @@ -123,7 +203,8 @@ private fun Content( state: MainSettingsState, onThemeSelected: (Theme) -> Unit = { }, onPauseResumeNotificationClick: () -> Unit = { }, - onDefaultOptionsClick: () -> Unit = { } + onDefaultOptionsClick: () -> Unit = { }, + onAutomaticBackupClick: () -> Unit = { }, ) { Column( modifier @@ -198,6 +279,20 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) + OptionPageButton( + title = if (state.autoBackupLocation == null) { + stringResource(R.string.title_pref_automatic_backup_location_disabled) + } else { + stringResource(R.string.title_pref_automatic_backup_location_enabled) + }, + text = state.autoBackupLocation + ?: stringResource(R.string.summary_pref_automatic_backup_location_disabled), + icon = Icons.Rounded.Tune, + onClick = onAutomaticBackupClick + ) + + Spacer(modifier = Modifier.height(8.dp)) + OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Rounded.Construction, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 2ded652186..97b7a3e86a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -46,8 +46,6 @@ class SettingsViewModel @Inject constructor( NavigationProvider by navigationProvider, DefaultOptionsSettingsCallback { - val automaticBackupLocation = useCase.automaticBackupLocation - val isWriteSecureSettingsPermissionGranted: StateFlow = useCase.isWriteSecureSettingsGranted .stateIn(viewModelScope, SharingStarted.Eagerly, true) @@ -83,9 +81,11 @@ class SettingsViewModel @Inject constructor( val mainScreenState: StateFlow = combine( useCase.theme, useCase.getPreference(Keys.log), - ) { theme, loggingEnabled -> + useCase.automaticBackupLocation, + ) { theme, loggingEnabled, autoBackupLocation -> MainSettingsState( - theme = theme + theme = theme, + autoBackupLocation = autoBackupLocation ) }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) @@ -114,6 +114,7 @@ class SettingsViewModel @Inject constructor( }.stateIn(viewModelScope, SharingStarted.Lazily, DefaultSettingsState()) fun setAutomaticBackupLocation(uri: String) = useCase.setAutomaticBackupLocation(uri) + fun disableAutomaticBackup() = useCase.disableAutomaticBackup() fun onChooseCompatibleImeClick() { @@ -229,17 +230,6 @@ class SettingsViewModel @Inject constructor( } } - fun onCreateBackupFileActivityNotFound() { - val dialog = DialogModel.Alert( - message = getString(R.string.dialog_message_no_app_found_to_create_file), - positiveButtonText = getString(R.string.pos_ok), - ) - - viewModelScope.launch { - showDialog("create_document_activity_not_found", dialog) - } - } - fun onResetAllSettingsClick() { val dialog = DialogModel.Alert( title = getString(R.string.dialog_title_reset_settings), @@ -338,7 +328,8 @@ class SettingsViewModel @Inject constructor( } data class MainSettingsState( - val theme: Theme = Theme.AUTO + val theme: Theme = Theme.AUTO, + val autoBackupLocation: String? = null ) data class DefaultSettingsState( diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 43562c2c1b..074834ec6d 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -433,6 +433,7 @@ The keys need to be listed from top to bottom in the order that they will be held down. A \"sequence\" trigger has a timeout unlike parallel triggers. This means after you press the first key, you will have a set amount of time to input the rest of the keys in the trigger. All the keys that you have added to the trigger won\'t do their usual action until the timeout has been reached. You can change this timeout in the "Options" tab. Android doesn\'t allow apps to get a list of connected (not paired) Bluetooth devices. Apps can only detect when they are connected and disconnected. So if your Bluetooth device is already connected to your device when the accessibility service starts, you will have to reconnect it for the app to know it is connected. + Automatic backup Change location or turn off automatic back up? Screen on/off constraints will only work if you have turned on the \"detect trigger when screen is off\" key map option. This option will only show for some keys (e.g volume buttons) and if you are rooted. See a list of supported keys on the Help page. If you have any other screen lock chosen, such as PIN or Pattern then you don\'t have to worry. But if you have a Password screen lock you will *NOT* be able to unlock your phone if you use the Key Mapper Basic Input Method because it doesn\'t have a GUI. You can grant Key Mapper WRITE_SECURE_SETTINGS permission so it can show a notification to switch to and from the keyboard. There is a guide on how to do this if you tap the question mark at the bottom of the screen. @@ -617,8 +618,9 @@ Show pause/resume notification Toggle your key maps on and off. - Automatically back up key maps to a specified location - No location chosen. + Change automatic backup location + Turn on automatic backup + Periodically back up your key maps Choose devices @@ -693,7 +695,7 @@ Reset all DANGER! Reset all settings in the app to the default. Your key maps will NOT be reset. DANGER! - 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. + Are you sure you want to reset all settings in the app to the default? Your key maps will NOT be reset. The introductions and warning pop ups will show again. Yes, reset Reset all From 76a87ec9773081d19d45e2d2e2762e0e21054811 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 13:32:45 +0100 Subject: [PATCH 144/215] #1394 add link to pro mode in settings --- .../sds100/keymapper/base/settings/SettingsScreen.kt | 10 ++++++++++ .../base/utils/ui/compose/OptionPageButton.kt | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 8d7de440e1..27f873303e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -58,6 +58,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.OptionPageButton import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.icons.FolderManaged import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon import io.github.sds100.keymapper.base.utils.ui.compose.icons.WandStars import io.github.sds100.keymapper.system.files.FileUtils import kotlinx.coroutines.launch @@ -131,6 +132,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onThemeSelected = viewModel::onThemeSelected, onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick, onDefaultOptionsClick = viewModel::onDefaultOptionsClick, + onProModeClick = viewModel::onProModeClick, onAutomaticBackupClick = { if (state.autoBackupLocation.isNullOrBlank()) { automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) @@ -205,6 +207,7 @@ private fun Content( onPauseResumeNotificationClick: () -> Unit = { }, onDefaultOptionsClick: () -> Unit = { }, onAutomaticBackupClick: () -> Unit = { }, + onProModeClick: () -> Unit = { }, ) { Column( modifier @@ -301,6 +304,13 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) + OptionPageButton( + title = stringResource(R.string.title_pref_pro_mode), + text = stringResource(R.string.summary_pref_pro_mode), + icon = KeyMapperIcons.ProModeIcon, + onClick = onProModeClick + ) + Spacer(modifier = Modifier.height(8.dp)) OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Rounded.Code, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt index f3c6519306..c53dec5fa6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ChevronRight import androidx.compose.material3.Icon @@ -40,7 +41,7 @@ fun OptionPageButton( Icon( modifier = Modifier .align(Alignment.CenterVertically) - .padding(end = 16.dp), + .size(24.dp), imageVector = icon, contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, From e319cbe34ac5c9dcdb69d640a41b35a6922d9a5d Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 15:35:46 +0100 Subject: [PATCH 145/215] #1394 do not crash if ADB SSL handshake fails --- .../base/utils/ui/compose/OptionPageButton.kt | 4 ++ .../keymapper/sysbridge/adb/AdbClient.kt | 49 ++++++++++--------- .../keymapper/sysbridge/adb/AdbError.kt | 2 +- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt index c53dec5fa6..0e92b46627 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt @@ -2,9 +2,11 @@ package io.github.sds100.keymapper.base.utils.ui.compose 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.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ChevronRight import androidx.compose.material3.Icon @@ -46,6 +48,8 @@ fun OptionPageButton( contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, ) + + Spacer(modifier = Modifier.width(16.dp)) } Column(modifier = Modifier.weight(1f)) { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt index 89ee9c580b..9ffdb24fc2 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt @@ -23,6 +23,7 @@ import java.net.ConnectException import java.net.Socket import java.nio.ByteBuffer import java.nio.ByteOrder +import javax.net.ssl.SSLProtocolException import javax.net.ssl.SSLSocket private const val TAG = "AdbClient" @@ -66,34 +67,40 @@ internal class AdbClient(private val host: String, private val port: Int, privat plainInputStream = DataInputStream(socket!!.getInputStream()) plainOutputStream = DataOutputStream(socket!!.getOutputStream()) - write(A_CNXN, A_VERSION, A_MAXDATA, "host::") + try { + write(A_CNXN, A_VERSION, A_MAXDATA, "host::") - var message = read() - if (message.command == A_STLS) { - write(A_STLS, A_STLS_VERSION, 0) + var message = read() + if (message.command == A_STLS) { + write(A_STLS, A_STLS_VERSION, 0) - val sslContext = key.sslContext - tlsSocket = sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket - tlsSocket.startHandshake() - Timber.d("Handshake succeeded.") + val sslContext = key.sslContext + tlsSocket = + sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket + tlsSocket.startHandshake() + Timber.d("Handshake succeeded.") - tlsInputStream = DataInputStream(tlsSocket.inputStream) - tlsOutputStream = DataOutputStream(tlsSocket.outputStream) - useTls = true + tlsInputStream = DataInputStream(tlsSocket.inputStream) + tlsOutputStream = DataOutputStream(tlsSocket.outputStream) + useTls = true - message = read() - } else if (message.command == A_AUTH) { - write(A_AUTH, ADB_AUTH_SIGNATURE, 0, key.sign(message.data)) + message = read() + } else if (message.command == A_AUTH) { + 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) { + write(A_AUTH, ADB_AUTH_RSAPUBLICKEY, 0, key.adbPublicKey) + message = read() + } } - } - if (message.command != A_CNXN) { - error("not A_CNXN") + if (message.command != A_CNXN) { + error("not A_CNXN") + } + } catch (e: SSLProtocolException) { + // This can be thrown if the encryption keys mismatch + return AdbError.SslHandshakeError } return Success(Unit) @@ -152,7 +159,6 @@ internal class AdbClient(private val host: String, private val port: Int, privat private fun write(message: AdbMessage) { outputStream.write(message.toByteArray()) outputStream.flush() - Timber.d("write ${message.toStringShort()}") } private fun read(): AdbMessage { @@ -176,7 +182,6 @@ internal class AdbClient(private val host: String, private val port: Int, privat } val message = AdbMessage(command, arg0, arg1, dataLength, checksum, magic, data) message.validateOrThrow() - Timber.d("read ${message.toStringShort()}") return message } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt index 2e8fd0c6ac..d6da78ca4a 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt @@ -3,10 +3,10 @@ package io.github.sds100.keymapper.sysbridge.adb import io.github.sds100.keymapper.common.utils.KMError sealed class AdbError : KMError() { - data object Unpaired : AdbError() data object PairingError : AdbError() data object ServerNotFound : AdbError() data object KeyCreationError : AdbError() data object ConnectionError : AdbError() + data object SslHandshakeError : AdbError() data class Unknown(val exception: kotlin.Exception) : AdbError() } \ No newline at end of file From c3e1dbbe70040b1f1863a5996a4c567818a747f2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 16:12:22 +0100 Subject: [PATCH 146/215] #1394 create AutomaticChangeImeSettingsScreen --- .../sds100/keymapper/base/BaseMainNavHost.kt | 10 + .../AutomaticChangeImeSettingsScreen.kt | 220 ++++++++++++++++++ .../keymapper/base/settings/SettingsScreen.kt | 12 + .../base/settings/SettingsViewModel.kt | 66 ++++++ .../base/utils/navigation/NavDestination.kt | 6 + .../ui/compose/SwitchPreferenceCompose.kt | 63 +++++ base/src/main/res/values/strings.xml | 15 +- 7 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt index 61f3c5f591..79316f091c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt @@ -24,6 +24,7 @@ import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel import io.github.sds100.keymapper.base.promode.ProModeScreen import io.github.sds100.keymapper.base.promode.ProModeSetupScreen +import io.github.sds100.keymapper.base.settings.AutomaticChangeImeSettingsScreen import io.github.sds100.keymapper.base.settings.DefaultOptionsSettingsScreen import io.github.sds100.keymapper.base.settings.SettingsScreen import io.github.sds100.keymapper.base.settings.SettingsViewModel @@ -95,6 +96,15 @@ fun BaseMainNavHost( ) } + composable { + val viewModel: SettingsViewModel = hiltViewModel() + + AutomaticChangeImeSettingsScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + ) + } + composable { ProModeScreen( modifier = Modifier diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt new file mode 100644 index 0000000000..97e1b9d3fb --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt @@ -0,0 +1,220 @@ +package io.github.sds100.keymapper.base.settings + +import android.os.Build +import androidx.compose.foundation.layout.Column +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.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.outlined.Notifications +import androidx.compose.material.icons.rounded.Devices +import androidx.compose.material.icons.rounded.Notifications +import androidx.compose.material.icons.rounded.SwapHoriz +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +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.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.OptionPageButton +import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose + +@Composable +fun AutomaticChangeImeSettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { + val state by viewModel.automaticChangeImeSettingsState.collectAsStateWithLifecycle() + val snackbarHostState = SnackbarHostState() + + AutomaticChangeImeSettingsScreen( + modifier, + onBackClick = viewModel::onBackClick, + snackbarHostState = snackbarHostState + ) { + Content( + state = state, + onShowToastWhenAutoChangingImeToggled = viewModel::onShowToastWhenAutoChangingImeToggled, + onChangeImeOnInputFocusToggled = viewModel::onChangeImeOnInputFocusToggled, + onChangeImeOnDeviceConnectToggled = viewModel::onChangeImeOnDeviceConnectToggled, + onDevicesThatChangeImeClick = viewModel::onDevicesThatChangeImeClick, + onToggleKeyboardOnToggleKeymapsToggled = viewModel::onToggleKeyboardOnToggleKeymapsToggled, + onShowToggleKeyboardNotificationClick = viewModel::onShowToggleKeyboardNotificationClick + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AutomaticChangeImeSettingsScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + snackbarHostState: SnackbarHostState = SnackbarHostState(), + content: @Composable () -> Unit +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title_pref_automatically_change_ime)) } + ) + }, + bottomBar = { + BottomAppBar { + 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, + state: AutomaticChangeImeSettingsState, + onShowToastWhenAutoChangingImeToggled: (Boolean) -> Unit = { }, + onChangeImeOnInputFocusToggled: (Boolean) -> Unit = { }, + onChangeImeOnDeviceConnectToggled: (Boolean) -> Unit = { }, + onDevicesThatChangeImeClick: () -> Unit = { }, + onToggleKeyboardOnToggleKeymapsToggled: (Boolean) -> Unit = { }, + onShowToggleKeyboardNotificationClick: () -> Unit = { }, +) { + Column( + modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_auto_change_ime_on_input_focus), + text = stringResource(R.string.summary_pref_auto_change_ime_on_input_focus), + icon = Icons.Rounded.SwapHoriz, + isChecked = state.changeImeOnInputFocus, + onCheckedChange = onChangeImeOnInputFocusToggled + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_auto_change_ime_on_connection), + text = stringResource(R.string.summary_pref_auto_change_ime_on_connection), + icon = Icons.Rounded.SwapHoriz, + isChecked = state.changeImeOnDeviceConnect, + onCheckedChange = onChangeImeOnDeviceConnectToggled + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = stringResource(R.string.title_pref_automatically_change_ime_choose_devices), + text = stringResource(R.string.summary_pref_automatically_change_ime_choose_devices), + icon = Icons.Rounded.Devices, + onClick = onDevicesThatChangeImeClick + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_toggle_keyboard_on_toggle_keymaps), + text = stringResource(R.string.summary_pref_toggle_keyboard_on_toggle_keymaps), + icon = Icons.Rounded.SwapHoriz, + isChecked = state.toggleKeyboardOnToggleKeymaps, + onCheckedChange = onToggleKeyboardOnToggleKeymapsToggled + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.Notifications, + text = stringResource(R.string.settings_section_notifications) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_show_toast_when_auto_changing_ime), + text = stringResource(R.string.summary_pref_show_toast_when_auto_changing_ime), + icon = Icons.Rounded.Notifications, + isChecked = state.showToastWhenAutoChangingIme, + onCheckedChange = onShowToastWhenAutoChangingImeToggled + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + OptionPageButton( + title = stringResource(R.string.title_pref_show_toggle_keyboard_notification), + text = stringResource(R.string.summary_pref_show_toggle_keyboard_notification), + icon = Icons.Rounded.Notifications, + onClick = onShowToggleKeyboardNotificationClick + ) + } else { + // For older Android versions, this would be a switch but since we're targeting newer versions + // we'll just show the notification settings button + OptionPageButton( + title = stringResource(R.string.title_pref_show_toggle_keyboard_notification), + text = stringResource(R.string.summary_pref_show_toggle_keyboard_notification), + icon = Icons.Rounded.Notifications, + onClick = onShowToggleKeyboardNotificationClick + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + AutomaticChangeImeSettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { + Content( + state = AutomaticChangeImeSettingsState() + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 27f873303e..55cac5b40c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.outlined.Gamepad import androidx.compose.material.icons.rounded.Code import androidx.compose.material.icons.rounded.Construction +import androidx.compose.material.icons.rounded.Keyboard import androidx.compose.material.icons.rounded.PlayCircleOutline import androidx.compose.material.icons.rounded.Tune import androidx.compose.material3.AlertDialog @@ -133,6 +134,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick, onDefaultOptionsClick = viewModel::onDefaultOptionsClick, onProModeClick = viewModel::onProModeClick, + onAutomaticChangeImeClick = viewModel::onAutomaticChangeImeClick, onAutomaticBackupClick = { if (state.autoBackupLocation.isNullOrBlank()) { automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) @@ -208,6 +210,7 @@ private fun Content( onDefaultOptionsClick: () -> Unit = { }, onAutomaticBackupClick: () -> Unit = { }, onProModeClick: () -> Unit = { }, + onAutomaticChangeImeClick: () -> Unit = { }, ) { Column( modifier @@ -310,7 +313,16 @@ private fun Content( icon = KeyMapperIcons.ProModeIcon, onClick = onProModeClick ) + + OptionPageButton( + title = stringResource(R.string.title_pref_automatically_change_ime), + text = stringResource(R.string.summary_pref_automatically_change_ime), + icon = Icons.Rounded.Keyboard, + onClick = onAutomaticChangeImeClick + ) + Spacer(modifier = Modifier.height(8.dp)) + OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Rounded.Code, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 97b7a3e86a..c77e63fcb7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -113,6 +113,21 @@ class SettingsViewModel @Inject constructor( ) }.stateIn(viewModelScope, SharingStarted.Lazily, DefaultSettingsState()) + val automaticChangeImeSettingsState: StateFlow = combine( + useCase.getPreference(Keys.showToastWhenAutoChangingIme), + useCase.getPreference(Keys.changeImeOnInputFocus), + useCase.getPreference(Keys.changeImeOnDeviceConnect), + useCase.getPreference(Keys.toggleKeyboardOnToggleKeymaps), + ) { values -> + AutomaticChangeImeSettingsState( + showToastWhenAutoChangingIme = values[0] + ?: PreferenceDefaults.SHOW_TOAST_WHEN_AUTO_CHANGE_IME, + changeImeOnInputFocus = values[1] ?: PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS, + changeImeOnDeviceConnect = values[2] ?: false, + toggleKeyboardOnToggleKeymaps = values[3] ?: false, + ) + }.stateIn(viewModelScope, SharingStarted.Lazily, AutomaticChangeImeSettingsState()) + fun setAutomaticBackupLocation(uri: String) = useCase.setAutomaticBackupLocation(uri) fun disableAutomaticBackup() = useCase.disableAutomaticBackup() @@ -257,6 +272,12 @@ class SettingsViewModel @Inject constructor( } } + fun onAutomaticChangeImeClick() { + viewModelScope.launch { + navigate("automatic_change_ime", NavDestination.AutomaticChangeImeSettings) + } + } + fun onBackClick() { viewModelScope.launch { popBackStack() @@ -315,6 +336,44 @@ class SettingsViewModel @Inject constructor( } } + fun onAutomaticChangeImeSettingsClick() { + viewModelScope.launch { + navigate("automatic_change_ime", NavDestination.AutomaticChangeImeSettings) + } + } + + fun onShowToastWhenAutoChangingImeToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.showToastWhenAutoChangingIme, enabled) + } + } + + fun onChangeImeOnInputFocusToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.changeImeOnInputFocus, enabled) + } + } + + fun onChangeImeOnDeviceConnectToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.changeImeOnDeviceConnect, enabled) + } + } + + fun onDevicesThatChangeImeClick() { + chooseDevicesForPreference(Keys.devicesThatChangeIme) + } + + fun onToggleKeyboardOnToggleKeymapsToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.toggleKeyboardOnToggleKeymaps, enabled) + } + } + + fun onShowToggleKeyboardNotificationClick() { + onNotificationSettingsClick(NotificationController.CHANNEL_TOGGLE_KEYBOARD) + } + private fun onNotificationSettingsClick(channel: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !useCase.isNotificationsPermissionGranted() @@ -350,4 +409,11 @@ data class DefaultSettingsState( val sequenceTriggerTimeout: Int = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, val defaultSequenceTriggerTimeout: Int = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, +) + +data class AutomaticChangeImeSettingsState( + val showToastWhenAutoChangingIme: Boolean = PreferenceDefaults.SHOW_TOAST_WHEN_AUTO_CHANGE_IME, + val changeImeOnInputFocus: Boolean = PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS, + val changeImeOnDeviceConnect: Boolean = false, + val toggleKeyboardOnToggleKeymaps: Boolean = false, ) \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt index fe2e8cdbaf..520be94a26 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt @@ -32,6 +32,7 @@ abstract class NavDestination(val isCompose: Boolean = false) { const val ID_CHOOSE_BLUETOOTH_DEVICE = "choose_bluetooth_device" const val ID_SETTINGS = "settings" const val ID_DEFAULT_OPTIONS_SETTINGS = "default_options_settings" + const val ID_AUTOMATIC_CHANGE_IME_SETTINGS = "automatic_change_ime_settings" const val ID_ABOUT = "about" const val ID_CONFIG_KEY_MAP = "config_key_map" const val ID_INTERACT_UI_ELEMENT_ACTION = "interact_ui_element_action" @@ -128,6 +129,11 @@ abstract class NavDestination(val isCompose: Boolean = false) { override val id: String = ID_DEFAULT_OPTIONS_SETTINGS } + @Serializable + data object AutomaticChangeImeSettings : NavDestination(isCompose = true) { + override val id: String = ID_AUTOMATIC_CHANGE_IME_SETTINGS + } + @Serializable data object About : NavDestination() { override val id: String = ID_ABOUT diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt new file mode 100644 index 0000000000..c6db07d8da --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt @@ -0,0 +1,63 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun SwitchPreferenceCompose( + modifier: Modifier = Modifier, + title: String, + text: String?, + icon: ImageVector, + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.medium, + onClick = { + onCheckedChange(!isChecked) + } + ) { + Row( + modifier = Modifier.Companion + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + + Column(modifier = Modifier.Companion.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + if (text != null) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + Switch( + checked = isChecked, + onCheckedChange = onCheckedChange + ) + } + } +} \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 074834ec6d..f7d1e1d478 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -616,7 +616,7 @@ Show a persistent notification to allow you to pick a keyboard. Show pause/resume notification - Toggle your key maps on and off. + Toggle your key maps on and off Change automatic backup location Turn on automatic backup @@ -633,7 +633,8 @@ Automatically change the on-screen keyboard when you start inputting text The last used non Key Mapper keyboard will be automatically selected when you try to open the keyboard. Your Key Mapper keyboard will be automatically selected once you stop using the keyboard. - Show an on-screen message when automatically changing the keyboard + On-screen message + Show when automatically changing the keyboard Request root permission If your device is rooted then this will show the root permission pop up from Magisk or your root app. @@ -704,6 +705,7 @@ Data management Power user options Debugging + Notifications @@ -719,10 +721,13 @@ Shizuku support Shizuku is an app that allows Key Mapper to do things that only system apps can do. You don\'t need to use the Key Mapper keyboard for example. Tap to learn how to set this up. - Follow these steps to set up Shizuku. + Follow these steps to set up Shizuku - Automatically change the keyboard - These are really useful settings and you are recommended to check them out! + Automatically switch keyboard + Switch when needed then switch back + + Choose devices + Choose which devices trigger automatic keyboard switching Logging This may add latency to your key maps so only turn this on if you are trying to debug the app or have been asked to by the developer. From 5baf35614a98bfaf2403301e4e520ba454aac69a Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 16:13:40 +0100 Subject: [PATCH 147/215] #1394 update default settings strings --- base/src/main/res/values/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index f7d1e1d478..ad227b9149 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -589,23 +589,23 @@ - Default long press delay (ms) + Default long press delay How long a button should be pressed for it to be detected as a long press. Default is 500ms. Can be overridden in a key map\'s options. - Default double press duration (ms) + Default double press duration How fast does a button have to be double pressed for it to be detected as a double press. Default is 300ms. Can be overridden in a key map\'s options. How long to vibrate if vibrating is enabled for a key map. Default is 200ms. Can be overridden in a key map\'s options. - Default vibrate duration (ms) + Default vibrate duration How long the trigger needs to be held down for the action to start repeating. Default is 400ms. Can be overridden in a key map\'s options. - Default delay until repeat (ms) + Default delay until repeat The delay between every time an action is repeated. Default is 50ms. Can be overridden in a key map\'s options. - Default delay between repeats (ms) + Default delay between repeats The time allowed to complete a sequence trigger. Default is 1000ms. Can be overridden in a key map\'s options. - Default sequence trigger timeout (ms) + Default sequence trigger timeout Reset From 074dfd49c3632f6a16be4f3d1df7314b865fcfa9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 16:18:38 +0100 Subject: [PATCH 148/215] #1394 create new preference to force vibrate all key maps --- .../keymapper/base/settings/SettingsScreen.kt | 14 ++++++++++++++ .../keymapper/base/settings/SettingsViewModel.kt | 15 ++++++++++++--- base/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 55cac5b40c..2f3ee7fee7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material.icons.rounded.Construction import androidx.compose.material.icons.rounded.Keyboard import androidx.compose.material.icons.rounded.PlayCircleOutline import androidx.compose.material.icons.rounded.Tune +import androidx.compose.material.icons.rounded.Vibration import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api @@ -57,6 +58,7 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.OptionPageButton import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose import io.github.sds100.keymapper.base.utils.ui.compose.icons.FolderManaged import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon @@ -135,6 +137,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onDefaultOptionsClick = viewModel::onDefaultOptionsClick, onProModeClick = viewModel::onProModeClick, onAutomaticChangeImeClick = viewModel::onAutomaticChangeImeClick, + onForceVibrateToggled = viewModel::onForceVibrateToggled, onAutomaticBackupClick = { if (state.autoBackupLocation.isNullOrBlank()) { automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) @@ -211,6 +214,7 @@ private fun Content( onAutomaticBackupClick: () -> Unit = { }, onProModeClick: () -> Unit = { }, onAutomaticChangeImeClick: () -> Unit = { }, + onForceVibrateToggled: (Boolean) -> Unit = { }, ) { Column( modifier @@ -323,6 +327,16 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_force_vibrate), + text = stringResource(R.string.summary_pref_force_vibrate), + icon = Icons.Rounded.Vibration, + isChecked = state.forceVibrate, + onCheckedChange = onForceVibrateToggled + ) + + Spacer(modifier = Modifier.height(8.dp)) + OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Rounded.Code, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index c77e63fcb7..56391d16bf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -82,10 +82,12 @@ class SettingsViewModel @Inject constructor( useCase.theme, useCase.getPreference(Keys.log), useCase.automaticBackupLocation, - ) { theme, loggingEnabled, autoBackupLocation -> + useCase.getPreference(Keys.forceVibrate), + ) { theme, loggingEnabled, autoBackupLocation, forceVibrate -> MainSettingsState( theme = theme, - autoBackupLocation = autoBackupLocation + autoBackupLocation = autoBackupLocation, + forceVibrate = forceVibrate ?: false ) }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) @@ -374,6 +376,12 @@ class SettingsViewModel @Inject constructor( onNotificationSettingsClick(NotificationController.CHANNEL_TOGGLE_KEYBOARD) } + fun onForceVibrateToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.forceVibrate, enabled) + } + } + private fun onNotificationSettingsClick(channel: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !useCase.isNotificationsPermissionGranted() @@ -388,7 +396,8 @@ class SettingsViewModel @Inject constructor( data class MainSettingsState( val theme: Theme = Theme.AUTO, - val autoBackupLocation: String? = null + val autoBackupLocation: String? = null, + val forceVibrate: Boolean = false ) data class DefaultSettingsState( diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index ad227b9149..95101ed8c4 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -609,8 +609,8 @@ Reset - Force all key maps to vibrate. - Force vibrate + Make all key maps vibrate + Every time a key map is triggered Keyboard picker notification Show a persistent notification to allow you to pick a keyboard. From 56ed35dd785b0c247c9d2bae13293782137f588b Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 16:49:15 +0100 Subject: [PATCH 149/215] #1394 create new logging screen --- base/build.gradle.kts | 1 - .../sds100/keymapper/base/BaseMainNavHost.kt | 10 + .../keymapper/base/compose/ComposeColors.kt | 8 + .../base/compose/ComposeCustomColors.kt | 20 ++ .../base/logging/DisplayLogUseCase.kt | 50 +++-- .../base/logging/LogEntryEntityMapper.kt | 2 + .../base/logging/LogEntryListItem.kt | 11 - .../keymapper/base/logging/LogFragment.kt | 182 --------------- .../keymapper/base/logging/LogListItem.kt | 8 + .../keymapper/base/logging/LogScreen.kt | 211 ++++++++++++++++++ .../keymapper/base/logging/LogSeverity.kt | 1 + .../sds100/keymapper/base/logging/LogUtils.kt | 28 --- .../keymapper/base/logging/LogViewModel.kt | 207 +++-------------- .../keymapper/base/settings/SettingsScreen.kt | 25 +++ .../base/settings/SettingsViewModel.kt | 18 +- .../base/utils/navigation/NavDestination.kt | 6 + .../main/res/layout/list_item_log_entry.xml | 57 ----- base/src/main/res/values/strings.xml | 5 +- .../keymapper/data/entities/LogEntryEntity.kt | 1 + .../data/repositories/LogRepository.kt | 3 +- .../data/repositories/RoomLogRepository.kt | 13 +- 21 files changed, 367 insertions(+), 500 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryListItem.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/logging/LogFragment.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/logging/LogListItem.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt delete mode 100644 base/src/main/res/layout/list_item_log_entry.xml diff --git a/base/build.gradle.kts b/base/build.gradle.kts index d2813ccb33..b247f287b7 100644 --- a/base/build.gradle.kts +++ b/base/build.gradle.kts @@ -90,7 +90,6 @@ dependencies { kapt(libs.airbnb.epoxy.processor) implementation(libs.jakewharton.timber) implementation(libs.anggrayudi.storage) - implementation(libs.github.mflisar.dragselectrecyclerview) implementation(libs.google.flexbox) implementation(libs.squareup.okhttp) coreLibraryDesugaring(libs.desugar.jdk.libs) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt index 79316f091c..2b69425575 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt @@ -22,6 +22,7 @@ import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementScreen import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementViewModel import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel +import io.github.sds100.keymapper.base.logging.LogScreen import io.github.sds100.keymapper.base.promode.ProModeScreen import io.github.sds100.keymapper.base.promode.ProModeSetupScreen import io.github.sds100.keymapper.base.settings.AutomaticChangeImeSettingsScreen @@ -122,6 +123,15 @@ fun BaseMainNavHost( viewModel = hiltViewModel(), ) } + + composable { + LogScreen( + modifier = Modifier.fillMaxSize(), + viewModel = hiltViewModel(), + onBackClick = { navController.popBackStack() } + ) + } + composableDestinations() } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt index e76ded65f4..0a2c8c7518 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt @@ -49,6 +49,10 @@ object ComposeColors { val onMagiskTealLight = Color(0xFFFFFFFF) val shizukuBlueLight = Color(0xFF4556B7) val onShizukuBlueLight = Color(0xFFFFFFFF) + val orangeLight = Color(0xFF8B5000) + val onOrangeLight = Color(0xFFFFFFFF) + val orangeContainerLight = Color(0xFFFFA643) + val onOrangeContainerLight = Color(0xFF452500) val primaryDark = Color(0xFFAAC7FF) val onPrimaryDark = Color(0xFF0A305F) @@ -95,4 +99,8 @@ object ComposeColors { val onMagiskTealDark = Color(0xFFFFFFFF) val shizukuBlueDark = Color(0xFFB7C4F4) val onShizukuBlueDark = Color(0xFF0A305F) + val orangeDark = Color(0xFFFFCA97) + val onOrangeDark = Color(0xFF4A2800) + val orangeContainerDark = Color(0xFFF69300) + val onOrangeContainerDark = Color(0xFF331A00) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt index 81eb41e2c9..3e9dc7ddc2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt @@ -6,6 +6,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color +import io.github.sds100.keymapper.base.compose.ComposeColors.onOrangeContainerDark +import io.github.sds100.keymapper.base.compose.ComposeColors.onOrangeContainerLight +import io.github.sds100.keymapper.base.compose.ComposeColors.onOrangeDark +import io.github.sds100.keymapper.base.compose.ComposeColors.onOrangeLight +import io.github.sds100.keymapper.base.compose.ComposeColors.orangeContainerDark +import io.github.sds100.keymapper.base.compose.ComposeColors.orangeContainerLight +import io.github.sds100.keymapper.base.compose.ComposeColors.orangeDark +import io.github.sds100.keymapper.base.compose.ComposeColors.orangeLight /** * Stores the custom colors in a palette that changes @@ -25,6 +33,10 @@ data class ComposeCustomColors( val onMagiskTeal: Color = Color.Unspecified, val shizukuBlue: Color = Color.Unspecified, val onShizukuBlue: Color = Color.Unspecified, + val orange: Color = Color.Unspecified, + val onOrange: Color = Color.Unspecified, + val orangeContainer: Color = Color.Unspecified, + val onOrangeContainer: Color = Color.Unspecified, ) { companion object { val LightPalette = ComposeCustomColors( @@ -38,6 +50,10 @@ data class ComposeCustomColors( onMagiskTeal = ComposeColors.onMagiskTealLight, shizukuBlue = ComposeColors.shizukuBlueLight, onShizukuBlue = ComposeColors.onShizukuBlueLight, + orange = orangeLight, + onOrange = onOrangeLight, + orangeContainer = orangeContainerLight, + onOrangeContainer = onOrangeContainerLight, ) val DarkPalette = ComposeCustomColors( @@ -51,6 +67,10 @@ data class ComposeCustomColors( onMagiskTeal = ComposeColors.onMagiskTealDark, shizukuBlue = ComposeColors.shizukuBlueDark, onShizukuBlue = ComposeColors.onShizukuBlueDark, + orange = orangeDark, + onOrange = onOrangeDark, + orangeContainer = orangeContainerDark, + onOrangeContainer = onOrangeContainerDark, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt index 0ffde83f82..26b76607e2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt @@ -2,9 +2,7 @@ package io.github.sds100.keymapper.base.logging import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.ifIsData -import io.github.sds100.keymapper.common.utils.mapData +import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter import io.github.sds100.keymapper.system.files.FileAdapter @@ -13,6 +11,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import javax.inject.Inject class DisplayLogUseCaseImpl @Inject constructor( @@ -21,41 +22,44 @@ class DisplayLogUseCaseImpl @Inject constructor( private val clipboardAdapter: ClipboardAdapter, private val fileAdapter: FileAdapter, ) : DisplayLogUseCase { - override val log: Flow>> = repository.log - .map { state -> - state.mapData { entityList -> entityList.map { LogEntryEntityMapper.fromEntity(it) } } - } + private val dateFormat = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) + private val severityString: Map = mapOf( + LogEntryEntity.SEVERITY_ERROR to "ERROR", + LogEntryEntity.SEVERITY_WARNING to "WARN", + LogEntryEntity.SEVERITY_INFO to "INFO", + LogEntryEntity.SEVERITY_DEBUG to "DEBUG", + ) + + override val log: Flow> = repository.log + .map { entityList -> entityList.map { LogEntryEntityMapper.fromEntity(it) } } .flowOn(Dispatchers.Default) override fun clearLog() { repository.deleteAll() } - override suspend fun copyToClipboard(entryId: Set) { - repository.log.first().ifIsData { logEntries -> - val logText = LogUtils.createLogText(logEntries.filter { it.id in entryId }) + override suspend fun copyToClipboard() { + val logEntries = repository.log.first() + val logText = createLogText(logEntries) - clipboardAdapter.copy( - label = resourceProvider.getString(R.string.clip_key_mapper_log), - logText, - ) - } + clipboardAdapter.copy( + label = resourceProvider.getString(R.string.clip_key_mapper_log), + logText, + ) } - override suspend fun saveToFile(uri: String, entryId: Set) { - val file = fileAdapter.getFileFromUri(uri) - repository.log.first().ifIsData { logEntries -> - val logText = LogUtils.createLogText(logEntries.filter { it.id in entryId }) + private fun createLogText(logEntries: List): String { + return logEntries.joinToString(separator = "\n") { entry -> + val date = dateFormat.format(Date(entry.time)) - file.outputStream()!!.bufferedWriter().use { it.write(logText) } + return@joinToString "$date ${severityString[entry.severity]} ${entry.message}" } } } interface DisplayLogUseCase { - val log: Flow>> + val log: Flow> fun clearLog() - suspend fun copyToClipboard(entryId: Set) - suspend fun saveToFile(uri: String, entryId: Set) + suspend fun copyToClipboard() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryEntityMapper.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryEntityMapper.kt index 0e3d6f301d..ccb11093d3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryEntityMapper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryEntityMapper.kt @@ -8,6 +8,7 @@ object LogEntryEntityMapper { LogSeverity.ERROR -> LogEntryEntity.SEVERITY_ERROR LogSeverity.DEBUG -> LogEntryEntity.SEVERITY_DEBUG LogSeverity.INFO -> LogEntryEntity.SEVERITY_INFO + LogSeverity.WARNING -> LogEntryEntity.SEVERITY_WARNING } return LogEntryEntity( @@ -23,6 +24,7 @@ object LogEntryEntityMapper { LogEntryEntity.SEVERITY_ERROR -> LogSeverity.ERROR LogEntryEntity.SEVERITY_DEBUG -> LogSeverity.DEBUG LogEntryEntity.SEVERITY_INFO -> LogSeverity.INFO + LogEntryEntity.SEVERITY_WARNING -> LogSeverity.WARNING else -> LogSeverity.DEBUG } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryListItem.kt deleted file mode 100644 index 5dcd1551d8..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryListItem.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.sds100.keymapper.base.logging - -import io.github.sds100.keymapper.base.utils.ui.TintType - -data class LogEntryListItem( - val id: Int, - val time: String, - val textTint: TintType, - val message: String, - val isSelected: Boolean, -) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogFragment.kt deleted file mode 100644 index 5c84b9c89e..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogFragment.kt +++ /dev/null @@ -1,182 +0,0 @@ -package io.github.sds100.keymapper.base.logging - -import android.content.Intent -import android.os.Bundle -import android.view.View -import android.view.ViewTreeObserver -import androidx.activity.result.contract.ActivityResultContracts.CreateDocument -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.EpoxyRecyclerView -import com.airbnb.epoxy.TypedEpoxyController -import com.michaelflisar.dragselectrecyclerview.DragSelectTouchListener -import com.michaelflisar.dragselectrecyclerview.DragSelectionProcessor -import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.databinding.FragmentSimpleRecyclerviewBinding -import io.github.sds100.keymapper.base.logEntry -import io.github.sds100.keymapper.base.utils.ui.SimpleRecyclerViewFragment -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.system.files.FileUtils -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest - -@AndroidEntryPoint -class LogFragment : SimpleRecyclerViewFragment() { - - private val viewModel: LogViewModel by viewModels() - - override val listItems: Flow>> - get() = viewModel.listItems - - override val appBarMenu: Int = R.menu.menu_log - override var isAppBarVisible = true - - private val recyclerViewController by lazy { RecyclerViewController() } - - private val saveLogToFileLauncher = - registerForActivityResult(CreateDocument(FileUtils.MIME_TYPE_TEXT)) { - it ?: return@registerForActivityResult - - viewModel.onPickFileToSaveTo(it.toString()) - - val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - requireContext().contentResolver.takePersistableUriPermission(it, takeFlags) - } - - private lateinit var dragSelectTouchListener: DragSelectTouchListener - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - getBottomAppBar(binding)?.setOnMenuItemClickListener { menuItem -> - viewModel.onMenuItemClick(menuItem.itemId) - true - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.pickFileToSaveTo.collectLatest { - saveLogToFileLauncher.launch(LogUtils.createLogFileName()) - } - } - } - - override fun subscribeUi(binding: FragmentSimpleRecyclerviewBinding) { - super.subscribeUi(binding) - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.appBarState.collectLatest { appBarState -> - when (appBarState) { - LogAppBarState.MULTI_SELECTING -> { - binding.appBar.setNavigationIcon(R.drawable.ic_outline_clear_24) - } - - LogAppBarState.NORMAL -> { - binding.appBar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - } - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.goBack.collectLatest { - findNavController().navigateUp() - } - } - - val dragSelectionProcessor = DragSelectionProcessor(viewModel.dragSelectionHandler) - .withMode(DragSelectionProcessor.Mode.Simple) - - dragSelectTouchListener = DragSelectTouchListener() - .withSelectListener(dragSelectionProcessor) - - binding.epoxyRecyclerView.setController(recyclerViewController) - } - - override fun onBackPressed() { - viewModel.onBackPressed() - } - - override fun populateList(recyclerView: EpoxyRecyclerView, listItems: List) { - recyclerViewController.setData(listItems) - } - - private inner class RecyclerViewController : TypedEpoxyController>() { - private var scrollToBottom = false - private var scrolledToBottomInitially = false - private var recyclerView: RecyclerView? = null - - init { - addModelBuildListener { - currentData?.also { currentData -> - if (!scrolledToBottomInitially) { - recyclerView?.viewTreeObserver?.addOnGlobalLayoutListener(object : - ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - recyclerView?.scrollToPosition(currentData.size - 1) - recyclerView?.viewTreeObserver?.removeOnGlobalLayoutListener(this) - } - }) - - scrolledToBottomInitially = true - } else if (scrollToBottom) { - recyclerView?.smoothScrollToPosition(currentData.size) - } - } - } - } - - override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { - this.recyclerView = recyclerView - super.onAttachedToRecyclerView(recyclerView) - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - this.recyclerView = null - super.onDetachedFromRecyclerView(recyclerView) - } - - override fun buildModels(data: List?) { - if (data == null) { - return - } - - if (recyclerView?.scrollState != RecyclerView.SCROLL_STATE_SETTLING) { - // only automatically scroll to the bottom if the recyclerview is already scrolled to the button - val layoutManager = recyclerView?.layoutManager as LinearLayoutManager? - - if (layoutManager != null) { - val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() - - if (lastVisibleItemPosition == RecyclerView.NO_POSITION) { - scrollToBottom = false - } else { - scrollToBottom = lastVisibleItemPosition == layoutManager.itemCount - 1 - } - } - } - - data.forEachIndexed { index, model -> - logEntry { - id(model.id) - model(model) - - onClick { _ -> - viewModel.onListItemClick(model.id) - } - - onLongClick { _ -> - dragSelectTouchListener.startDragSelection(index) - true - } - } - } - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogListItem.kt new file mode 100644 index 0000000000..1709b82cdd --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogListItem.kt @@ -0,0 +1,8 @@ +package io.github.sds100.keymapper.base.logging + +data class LogListItem( + val id: Int, + val time: String, + val severity: LogSeverity, + val message: String, +) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt new file mode 100644 index 0000000000..7dd3708ebc --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt @@ -0,0 +1,211 @@ +package io.github.sds100.keymapper.base.logging + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +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.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +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.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette + +@Composable +fun LogScreen( + modifier: Modifier = Modifier, + viewModel: LogViewModel = hiltViewModel(), + onBackClick: () -> Unit, +) { + val log = viewModel.log.collectAsStateWithLifecycle().value + + LogScreen( + modifier = modifier, + onBackClick = onBackClick, + onCopyToClipboardClick = viewModel::onCopyToClipboardClick, + onClearLogClick = viewModel::onClearLogClick, + content = { + Content( + modifier = Modifier.fillMaxSize(), + logListItems = log, + ) + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LogScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + onCopyToClipboardClick: () -> Unit = {}, + onClearLogClick: () -> Unit = {}, + content: @Composable () -> Unit, +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title_pref_view_and_share_log)) }, + actions = { + OutlinedButton( + modifier = Modifier.padding(horizontal = 16.dp), + onClick = onClearLogClick + ) { + Text(stringResource(R.string.action_clear_log)) + } + } + ) + }, + bottomBar = { + BottomAppBar { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.action_go_back), + ) + } + Spacer(Modifier.weight(1f)) + IconButton(onClick = onCopyToClipboardClick) { + Icon( + imageVector = Icons.Outlined.ContentCopy, + contentDescription = stringResource(R.string.action_copy_log) + ) + } + } + }, + ) { 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() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun Content( + modifier: Modifier = Modifier, + logListItems: List, +) { + val listState = rememberLazyListState() + + // Scroll to the bottom when a new item is added + LaunchedEffect(logListItems) { + if (logListItems.isNotEmpty()) { + listState.animateScrollToItem(logListItems.size - 1) + } + } + + Column(modifier = modifier) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(8.dp) + ) { + items(logListItems, key = { it.id }) { item -> + val color = when (item.severity) { + LogSeverity.ERROR -> MaterialTheme.colorScheme.error + LogSeverity.WARNING -> LocalCustomColorsPalette.current.orange + LogSeverity.INFO -> LocalCustomColorsPalette.current.green + else -> LocalContentColor.current + } + + Row { + Text( + text = item.time, + color = color, + style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = item.message, + color = color, + style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + LogScreen( + content = { + Content( + logListItems = listOf( + LogListItem(1, "12:34:56.789", LogSeverity.INFO, "This is an info message"), + LogListItem( + 2, + "12:34:57.123", + LogSeverity.WARNING, + "This is a warning message" + ), + LogListItem( + 3, + "12:34:58.456", + LogSeverity.ERROR, + "This is an error message. It is a bit long to see how it overflows inside the available space." + ), + LogListItem(4, "12:34:59.000", LogSeverity.INFO, "Another info message"), + LogListItem( + 5, + "12:35:00.000", + LogSeverity.ERROR, + "Error recording trigger" + ), + LogListItem(6, "12:35:01.000", LogSeverity.WARNING, "I am a warning"), + LogListItem(7, "12:35:02.000", LogSeverity.INFO, "I am some info..."), + LogListItem(8, "12:35:03.000", LogSeverity.INFO, "This more info"), + ), + ) + }) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogSeverity.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogSeverity.kt index e90e7993da..91d594e028 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogSeverity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogSeverity.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.base.logging enum class LogSeverity { ERROR, + WARNING, INFO, DEBUG, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt deleted file mode 100644 index e927604002..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.sds100.keymapper.base.logging - -import android.annotation.SuppressLint -import io.github.sds100.keymapper.data.entities.LogEntryEntity -import io.github.sds100.keymapper.system.files.FileUtils -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -object LogUtils { - @SuppressLint("ConstantLocale") - val DATE_FORMAT = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) - - fun createLogFileName(): String { - val formattedDate = FileUtils.createFileDate() - return "key_mapper_log_$formattedDate.txt" - } - - fun createLogText(logEntries: List): String { - val dateFormat = DATE_FORMAT - - return logEntries.joinToString(separator = "\n") { entry -> - val date = dateFormat.format(Date(entry.time)) - - return@joinToString "$date ${entry.message}" - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt index 61959f6990..9f3e4ec7d4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt @@ -1,204 +1,49 @@ package io.github.sds100.keymapper.base.logging +import android.annotation.SuppressLint import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.michaelflisar.dragselectrecyclerview.DragSelectionProcessor import dagger.hilt.android.lifecycle.HiltViewModel -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.DialogModel -import io.github.sds100.keymapper.base.utils.ui.DialogProvider -import io.github.sds100.keymapper.base.utils.ui.MultiSelectProvider -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.base.utils.ui.SelectionState -import io.github.sds100.keymapper.base.utils.ui.TintType -import io.github.sds100.keymapper.base.utils.ui.showDialog -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.ifIsData -import io.github.sds100.keymapper.common.utils.mapData -import kotlinx.coroutines.Dispatchers -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.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import java.util.Date +import java.text.SimpleDateFormat +import java.util.Locale import javax.inject.Inject @HiltViewModel class LogViewModel @Inject constructor( - private val useCase: DisplayLogUseCase, - resourceProvider: ResourceProvider, - dialogProvider: DialogProvider, -) : ViewModel(), - DialogProvider by dialogProvider, - ResourceProvider by resourceProvider { - private val multiSelectProvider: MultiSelectProvider = MultiSelectProvider() - - private val _listItems = MutableStateFlow>>(State.Loading) - val listItems = _listItems.asStateFlow() - - private val dateFormat = LogUtils.DATE_FORMAT - - private val _pickFileToSaveTo = MutableSharedFlow() - val pickFileToSaveTo = _pickFileToSaveTo.asSharedFlow() - - val appBarState: StateFlow = multiSelectProvider.state - .map { selectionState -> - when (selectionState) { - is SelectionState.Selecting -> LogAppBarState.MULTI_SELECTING - else -> LogAppBarState.NORMAL - } - } - .stateIn(viewModelScope, SharingStarted.Lazily, LogAppBarState.NORMAL) - - private val showShortMessages = MutableStateFlow(true) - - private val _goBack = MutableSharedFlow() - val goBack = _goBack.asSharedFlow() - - val dragSelectionHandler = object : DragSelectionProcessor.ISelectionHandler { - override fun getSelection(): MutableSet = multiSelectProvider.getSelectedIds().map { it.toInt() }.toMutableSet() - - override fun isSelected(index: Int): Boolean { - listItems.value.ifIsData { - val id = it.getOrNull(index)?.id ?: return false - - return multiSelectProvider.isSelected(id.toString()) - } - - return false - } - - override fun updateSelection( - start: Int, - end: Int, - isSelected: Boolean, - calledFromOnStart: Boolean, - ) { - listItems.value.ifIsData { listItems -> - val selectedListItems = listItems.slice(start..end) - val selectedIds = selectedListItems.map { it.id.toString() }.toTypedArray() - - if (calledFromOnStart) { - multiSelectProvider.startSelecting() - } - - if (isSelected) { - multiSelectProvider.select(*selectedIds) - } else { - multiSelectProvider.deselect(*selectedIds) - } + private val displayLogUseCase: DisplayLogUseCase, +) : ViewModel() { + @SuppressLint("ConstantLocale") + private val dateFormat = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) + + val log: StateFlow> = displayLogUseCase.log + .map { list -> + list.map { + LogListItem( + id = it.id, + time = dateFormat.format(it.time), + message = it.message, + severity = it.severity + ) } } - } - - init { - combine( - useCase.log, - showShortMessages, - multiSelectProvider.state, - ) { log, showShortMessages, selectionState -> - _listItems.value = log.mapData { logEntries -> - logEntries.map { entry -> - val isSelected = if (selectionState is SelectionState.Selecting) { - selectionState.selectedIds.contains(entry.id.toString()) - } else { - false - } - - createListItem(entry, showShortMessages, isSelected) - } - } - }.flowOn(Dispatchers.Default).launchIn(viewModelScope) - } - - fun onMenuItemClick(itemId: Int) { - viewModelScope.launch { - when (itemId) { - R.id.action_clear -> useCase.clearLog() - R.id.action_copy -> { - useCase.copyToClipboard(getSelectedLogEntries()) - showDialog("copied", DialogModel.Toast(getString(R.string.toast_copied_log))) - } - - R.id.action_short_messages -> { - showShortMessages.value = !showShortMessages.value - } - - R.id.action_save -> - _pickFileToSaveTo.emit(Unit) - } - } - } - - fun onListItemClick(id: Int) { - multiSelectProvider.toggleSelection(id.toString()) - } - - fun onBackPressed() { - if (multiSelectProvider.isSelecting()) { - multiSelectProvider.stopSelecting() - } else { - viewModelScope.launch { - _goBack.emit(Unit) - } - } - } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) - fun onPickFileToSaveTo(uri: String) { + fun onCopyToClipboardClick() { viewModelScope.launch { - useCase.saveToFile(uri, getSelectedLogEntries()) + displayLogUseCase.copyToClipboard() } } - private suspend fun getSelectedLogEntries(): Set = if (multiSelectProvider.isSelecting()) { - multiSelectProvider.getSelectedIds().map { it.toInt() }.toSet() - } else { - val logState = useCase.log.first() - - if (logState is State.Data) { - logState.data.map { it.id }.toSet() - } else { - emptySet() - } + fun onClearLogClick() { + displayLogUseCase.clearLog() } - - private fun createListItem( - logEntry: LogEntry, - shortMessage: Boolean, - isSelected: Boolean, - ): LogEntryListItem { - val textTint = if (logEntry.severity == LogSeverity.ERROR) { - TintType.Error - } else { - TintType.OnSurface - } - - val message: String = if (shortMessage) { - logEntry.message.split(',').getOrElse(0) { logEntry.message } - } else { - logEntry.message - } - - return LogEntryListItem( - id = logEntry.id, - time = dateFormat.format(Date(logEntry.time)), - textTint = textTint, - message = message, - isSelected = isSelected, - ) - } -} - -enum class LogAppBarState { - MULTI_SELECTING, - NORMAL, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 2f3ee7fee7..52694d49b2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -18,6 +18,8 @@ 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.outlined.BugReport +import androidx.compose.material.icons.outlined.FindInPage import androidx.compose.material.icons.outlined.Gamepad import androidx.compose.material.icons.rounded.Code import androidx.compose.material.icons.rounded.Construction @@ -138,6 +140,8 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onProModeClick = viewModel::onProModeClick, onAutomaticChangeImeClick = viewModel::onAutomaticChangeImeClick, onForceVibrateToggled = viewModel::onForceVibrateToggled, + onLoggingToggled = viewModel::onLoggingToggled, + onViewLogClick = viewModel::onViewLogClick, onAutomaticBackupClick = { if (state.autoBackupLocation.isNullOrBlank()) { automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) @@ -215,6 +219,8 @@ private fun Content( onProModeClick: () -> Unit = { }, onAutomaticChangeImeClick: () -> Unit = { }, onForceVibrateToggled: (Boolean) -> Unit = { }, + onLoggingToggled: (Boolean) -> Unit = { }, + onViewLogClick: () -> Unit = { }, ) { Column( modifier @@ -345,6 +351,25 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_toggle_logging), + text = stringResource(R.string.summary_pref_toggle_logging), + icon = Icons.Outlined.BugReport, + isChecked = state.loggingEnabled, + onCheckedChange = onLoggingToggled + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = stringResource(R.string.title_pref_view_and_share_log), + text = stringResource(R.string.summary_pref_view_and_share_log), + icon = Icons.Outlined.FindInPage, + onClick = onViewLogClick + ) + + Spacer(modifier = Modifier.height(8.dp)) + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 56391d16bf..b9a0f03b4d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -87,7 +87,8 @@ class SettingsViewModel @Inject constructor( MainSettingsState( theme = theme, autoBackupLocation = autoBackupLocation, - forceVibrate = forceVibrate ?: false + forceVibrate = forceVibrate ?: false, + loggingEnabled = loggingEnabled ?: false ) }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) @@ -382,6 +383,18 @@ class SettingsViewModel @Inject constructor( } } + fun onLoggingToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.log, enabled) + } + } + + fun onViewLogClick() { + viewModelScope.launch { + navigate("log", NavDestination.Log) + } + } + private fun onNotificationSettingsClick(channel: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !useCase.isNotificationsPermissionGranted() @@ -397,7 +410,8 @@ class SettingsViewModel @Inject constructor( data class MainSettingsState( val theme: Theme = Theme.AUTO, val autoBackupLocation: String? = null, - val forceVibrate: Boolean = false + val forceVibrate: Boolean = false, + val loggingEnabled: Boolean = false ) data class DefaultSettingsState( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt index 520be94a26..6f3b6b5587 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt @@ -37,6 +37,7 @@ abstract class NavDestination(val isCompose: Boolean = false) { const val ID_CONFIG_KEY_MAP = "config_key_map" const val ID_INTERACT_UI_ELEMENT_ACTION = "interact_ui_element_action" const val ID_PRO_MODE = "pro_mode" + const val ID_LOG = "log" } @Serializable @@ -170,4 +171,9 @@ abstract class NavDestination(val isCompose: Boolean = false) { const val ID_PRO_MODE_SETUP = "pro_mode_setup_wizard" override val id: String = ID_PRO_MODE_SETUP } + + @Serializable + data object Log : NavDestination(isCompose = true) { + override val id: String = ID_LOG + } } diff --git a/base/src/main/res/layout/list_item_log_entry.xml b/base/src/main/res/layout/list_item_log_entry.xml deleted file mode 100644 index b1924cbf88..0000000000 --- a/base/src/main/res/layout/list_item_log_entry.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 95101ed8c4..a7788276d8 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -672,7 +672,10 @@ (Recommended) Read user guide for this setting. Enable extra logging - View and share log + Record more detailed logs + View log + Share this with the developer if having issues + Report issue Delete sound files diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/LogEntryEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/LogEntryEntity.kt index bd42e17c29..7e3d8901e0 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/LogEntryEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/LogEntryEntity.kt @@ -20,5 +20,6 @@ data class LogEntryEntity( const val SEVERITY_ERROR = 0 const val SEVERITY_DEBUG = 1 const val SEVERITY_INFO = 2 + const val SEVERITY_WARNING = 3 } } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/LogRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/LogRepository.kt index 1df1d3479a..bdfc30164e 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/LogRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/LogRepository.kt @@ -1,11 +1,10 @@ package io.github.sds100.keymapper.data.repositories -import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.data.entities.LogEntryEntity import kotlinx.coroutines.flow.Flow interface LogRepository { - val log: Flow>> + val log: Flow> fun insert(entry: LogEntryEntity) suspend fun insertSuspend(entry: LogEntryEntity) fun deleteAll() diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt index 083a8034b9..4ff6368208 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt @@ -1,18 +1,14 @@ package io.github.sds100.keymapper.data.repositories -import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.data.db.dao.LogEntryDao import io.github.sds100.keymapper.data.entities.LogEntryEntity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @@ -22,15 +18,8 @@ class RoomLogRepository @Inject constructor( private val coroutineScope: CoroutineScope, private val dao: LogEntryDao, ) : LogRepository { - override val log: Flow>> = dao.getAll() - .map { entityList -> State.Data(entityList) } + override val log: Flow> = dao.getAll() .flowOn(Dispatchers.Default) - .stateIn( - coroutineScope, - // save memory by only caching the log when necessary - SharingStarted.WhileSubscribed(replayExpirationMillis = 1000L), - State.Loading, - ) init { dao.getIds() From d34256870cb7cdef5ff52818cdab11c060adbf3b Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 16:58:52 +0100 Subject: [PATCH 150/215] #1394 create setting to hide home screen alerts --- .../keymapper/base/settings/SettingsScreen.kt | 13 +++++++++++++ .../keymapper/base/settings/SettingsViewModel.kt | 15 ++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 52694d49b2..7babb376f2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.material.icons.rounded.Keyboard import androidx.compose.material.icons.rounded.PlayCircleOutline import androidx.compose.material.icons.rounded.Tune import androidx.compose.material.icons.rounded.Vibration +import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api @@ -142,6 +143,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onForceVibrateToggled = viewModel::onForceVibrateToggled, onLoggingToggled = viewModel::onLoggingToggled, onViewLogClick = viewModel::onViewLogClick, + onHideHomeScreenAlertsToggled = viewModel::onHideHomeScreenAlertsToggled, onAutomaticBackupClick = { if (state.autoBackupLocation.isNullOrBlank()) { automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) @@ -221,6 +223,7 @@ private fun Content( onForceVibrateToggled: (Boolean) -> Unit = { }, onLoggingToggled: (Boolean) -> Unit = { }, onViewLogClick: () -> Unit = { }, + onHideHomeScreenAlertsToggled: (Boolean) -> Unit = { }, ) { Column( modifier @@ -266,6 +269,16 @@ private fun Content( icon = Icons.Rounded.PlayCircleOutline, onClick = onPauseResumeNotificationClick ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_hide_home_screen_alerts), + text = stringResource(R.string.summary_pref_hide_home_screen_alerts), + icon = Icons.Rounded.VisibilityOff, + isChecked = state.hideHomeScreenAlerts, + onCheckedChange = onHideHomeScreenAlertsToggled + ) } Spacer(modifier = Modifier.height(8.dp)) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index b9a0f03b4d..58eeed1744 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -83,12 +83,14 @@ class SettingsViewModel @Inject constructor( useCase.getPreference(Keys.log), useCase.automaticBackupLocation, useCase.getPreference(Keys.forceVibrate), - ) { theme, loggingEnabled, autoBackupLocation, forceVibrate -> + useCase.getPreference(Keys.hideHomeScreenAlerts), + ) { theme, loggingEnabled, autoBackupLocation, forceVibrate, hideHomeScreenAlerts -> MainSettingsState( theme = theme, autoBackupLocation = autoBackupLocation, forceVibrate = forceVibrate ?: false, - loggingEnabled = loggingEnabled ?: false + loggingEnabled = loggingEnabled ?: false, + hideHomeScreenAlerts = hideHomeScreenAlerts ?: false ) }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) @@ -395,6 +397,12 @@ class SettingsViewModel @Inject constructor( } } + fun onHideHomeScreenAlertsToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.hideHomeScreenAlerts, enabled) + } + } + private fun onNotificationSettingsClick(channel: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !useCase.isNotificationsPermissionGranted() @@ -411,7 +419,8 @@ data class MainSettingsState( val theme: Theme = Theme.AUTO, val autoBackupLocation: String? = null, val forceVibrate: Boolean = false, - val loggingEnabled: Boolean = false + val loggingEnabled: Boolean = false, + val hideHomeScreenAlerts: Boolean = false ) data class DefaultSettingsState( From cce12f078813582a1811cc863862cb20939dd439 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 18:48:07 +0100 Subject: [PATCH 151/215] #1394 create setting to for showing device descriptors --- .../keymapper/base/settings/SettingsScreen.kt | 33 +++++++++++++------ .../base/settings/SettingsViewModel.kt | 23 +++++++++---- .../base/trigger/ConfigTriggerUseCase.kt | 3 +- base/src/main/res/values/strings.xml | 4 +-- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 7babb376f2..ede4c2e65d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.material.icons.outlined.FindInPage import androidx.compose.material.icons.outlined.Gamepad import androidx.compose.material.icons.rounded.Code import androidx.compose.material.icons.rounded.Construction +import androidx.compose.material.icons.rounded.Devices import androidx.compose.material.icons.rounded.Keyboard import androidx.compose.material.icons.rounded.PlayCircleOutline import androidx.compose.material.icons.rounded.Tune @@ -144,6 +145,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onLoggingToggled = viewModel::onLoggingToggled, onViewLogClick = viewModel::onViewLogClick, onHideHomeScreenAlertsToggled = viewModel::onHideHomeScreenAlertsToggled, + onShowDeviceDescriptorsToggled = viewModel::onShowDeviceDescriptorsToggled, onAutomaticBackupClick = { if (state.autoBackupLocation.isNullOrBlank()) { automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) @@ -224,6 +226,7 @@ private fun Content( onLoggingToggled: (Boolean) -> Unit = { }, onViewLogClick: () -> Unit = { }, onHideHomeScreenAlertsToggled: (Boolean) -> Unit = { }, + onShowDeviceDescriptorsToggled: (Boolean) -> Unit = { }, ) { Column( modifier @@ -300,6 +303,26 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_force_vibrate), + text = stringResource(R.string.summary_pref_force_vibrate), + icon = Icons.Rounded.Vibration, + isChecked = state.forceVibrate, + onCheckedChange = onForceVibrateToggled + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_show_device_descriptors), + text = stringResource(R.string.summary_pref_show_device_descriptors), + icon = Icons.Rounded.Devices, + isChecked = state.showDeviceDescriptors, + onCheckedChange = onShowDeviceDescriptorsToggled + ) + + Spacer(modifier = Modifier.height(8.dp)) + OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = KeyMapperIcons.FolderManaged, @@ -346,16 +369,6 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) - SwitchPreferenceCompose( - title = stringResource(R.string.title_pref_force_vibrate), - text = stringResource(R.string.summary_pref_force_vibrate), - icon = Icons.Rounded.Vibration, - isChecked = state.forceVibrate, - onCheckedChange = onForceVibrateToggled - ) - - Spacer(modifier = Modifier.height(8.dp)) - OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Rounded.Code, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 58eeed1744..a245072a76 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -84,13 +84,15 @@ class SettingsViewModel @Inject constructor( useCase.automaticBackupLocation, useCase.getPreference(Keys.forceVibrate), useCase.getPreference(Keys.hideHomeScreenAlerts), - ) { theme, loggingEnabled, autoBackupLocation, forceVibrate, hideHomeScreenAlerts -> + useCase.getPreference(Keys.showDeviceDescriptors), + ) { values -> MainSettingsState( - theme = theme, - autoBackupLocation = autoBackupLocation, - forceVibrate = forceVibrate ?: false, - loggingEnabled = loggingEnabled ?: false, - hideHomeScreenAlerts = hideHomeScreenAlerts ?: false + theme = values[0] as Theme, + loggingEnabled = values[1] as Boolean? ?: false, + autoBackupLocation = values[2] as String?, + forceVibrate = values[3] as Boolean? ?: false, + hideHomeScreenAlerts = values[4] as Boolean? ?: false, + showDeviceDescriptors = values[5] as Boolean? ?: false ) }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) @@ -403,6 +405,12 @@ class SettingsViewModel @Inject constructor( } } + fun onShowDeviceDescriptorsToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.showDeviceDescriptors, enabled) + } + } + private fun onNotificationSettingsClick(channel: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !useCase.isNotificationsPermissionGranted() @@ -420,7 +428,8 @@ data class MainSettingsState( val autoBackupLocation: String? = null, val forceVibrate: Boolean = false, val loggingEnabled: Boolean = false, - val hideHomeScreenAlerts: Boolean = false + val hideHomeScreenAlerts: Boolean = false, + val showDeviceDescriptors: Boolean = false ) data class DefaultSettingsState( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt index 993f12eaa7..740f3aefb5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -265,8 +265,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( val showDeviceDescriptors = showDeviceDescriptors.firstBlocking() - inputDevices.forEach { device -> - + for (device in inputDevices) { if (device.isExternal) { val name = if (showDeviceDescriptors) { InputDeviceUtils.appendDeviceDescriptorToName( diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index a7788276d8..23381b651f 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -651,8 +651,8 @@ Hide home screen alerts Hide the alerts at the top of the home screen. - Show the first 5 characters of the device id for device specific triggers - This is useful to differentiate between devices that have the same name. + Show device IDs + Differentiate devices with the same name Fix keyboards that are set to US English This fixes keyboards that don\'t have the correct the keyboard layout when an accessibility service is enabled. Tap to read more and configure. From 37250b9ac3bc62837e4e2fd0c9fc0d44bed92388 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 19:49:22 +0100 Subject: [PATCH 152/215] minimum supported Android version is now 8.0 Oreo --- CHANGELOG.md | 2 ++ gradle/libs.versions.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d528f322..19043a2694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - The key event relay service is now also used on all Android versions below Android 14. The broadcast receiver method is no longer used. +- Minimum supported Android version is now 8.0. Less than 1% of users are on older versions than + this and dropping support simplifies the codebase and maintenance. ## [3.2.0](https://github.com/sds100/KeyMapper/releases/tag/v3.2.0) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 545d299ec1..be0a6ee1b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] compile-sdk = "36" hilt-navigation-compose = "1.2.0" -min-sdk = "21" +min-sdk = "26" build-tools = "36.0.0" target-sdk = "36" From b9aa5b5b6d1f7c0b3f0bdd98e0218aea43dc4952 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 20:03:26 +0100 Subject: [PATCH 153/215] #1394 delete old Settings fragments --- CHANGELOG.md | 4 + .../Android11BugWorkaroundSettingsFragment.kt | 192 ------- .../AutomaticallyChangeImeSettings.kt | 136 ----- .../base/settings/BaseSettingsFragment.kt | 60 --- .../DefaultOptionsSettingsFragment.kt | 206 -------- .../settings/ImePickerSettingsFragment.kt | 100 ---- .../base/settings/MainSettingsFragment.kt | 486 ------------------ .../keymapper/base/settings/SettingsUtils.kt | 28 - .../base/settings/ShizukuSettingsFragment.kt | 137 ----- .../notifications/NotificationController.kt | 4 + base/src/main/res/navigation/nav_base_app.xml | 2 - base/src/main/res/navigation/nav_settings.xml | 78 --- base/src/main/res/values/strings.xml | 5 +- 13 files changed, 11 insertions(+), 1427 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/Android11BugWorkaroundSettingsFragment.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticallyChangeImeSettings.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/BaseSettingsFragment.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsFragment.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/ImePickerSettingsFragment.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsUtils.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/ShizukuSettingsFragment.kt delete mode 100644 base/src/main/res/navigation/nav_settings.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 19043a2694..427f308832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ broadcast receiver method is no longer used. - Minimum supported Android version is now 8.0. Less than 1% of users are on older versions than this and dropping support simplifies the codebase and maintenance. +- Dropped support for showing a keyboard picker notification and automatically showing it when a + device connects. This is only supported on Android 8.1 and is extra work to maintain it. +- Dropped support for rerouting key events on Android 11. This was a workaround for a specific bug + in Android 11 which fewer than 10% of users are using and less are probably using that feature. ## [3.2.0](https://github.com/sds100/KeyMapper/releases/tag/v3.2.0) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/Android11BugWorkaroundSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/Android11BugWorkaroundSettingsFragment.kt deleted file mode 100644 index f82b8f2e84..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/Android11BugWorkaroundSettingsFragment.kt +++ /dev/null @@ -1,192 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.preference.Preference -import androidx.preference.SwitchPreference -import androidx.preference.isEmpty -import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.ChooseAppStoreModel -import io.github.sds100.keymapper.base.utils.ui.DialogModel -import io.github.sds100.keymapper.base.utils.ui.drawable -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.showDialog -import io.github.sds100.keymapper.base.utils.ui.str -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.system.leanback.LeanbackUtils -import io.github.sds100.keymapper.system.url.UrlUtils -import kotlinx.coroutines.flow.collectLatest - -@AndroidEntryPoint -class Android11BugWorkaroundSettingsFragment : BaseSettingsFragment() { - - companion object { - private const val KEY_ENABLE_COMPATIBLE_IME = "pref_key_enable_compatible_ime" - private const val KEY_CHOSE_COMPATIBLE_IME = "pref_key_chose_compatible_ime" - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - val isTvDevice = LeanbackUtils.isTvDevice(requireContext()) - - SwitchPreference(requireContext()).apply { - key = Keys.rerouteKeyEvents.name - setDefaultValue(false) - - setTitle(R.string.title_pref_reroute_keyevents) - setSummary(R.string.summary_pref_reroute_keyevents) - isSingleLineTitle = false - - addPreference(this) - } - - Preference(requireContext()).apply { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_guide) - setOnPreferenceClickListener { - UrlUtils.openUrl( - requireContext(), - str(R.string.url_android_11_bug_reset_id_work_around_setting_guide), - ) - - true - } - - addPreference(this) - } - - Preference(requireContext()).apply { - if (isTvDevice) { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_install_leanback_keyboard) - } else { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_install_gui_keyboard) - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - viewLifecycleScope.launchWhenResumed { - if (isTvDevice) { - val chooseAppStoreDialog = DialogModel.ChooseAppStore( - title = getString(R.string.dialog_title_choose_download_leanback_keyboard), - message = getString(R.string.dialog_message_choose_download_leanback_keyboard), - model = ChooseAppStoreModel( - githubLink = getString(R.string.url_github_keymapper_leanback_keyboard), - ), - negativeButtonText = str(R.string.neg_cancel), - ) - - viewModel.showDialog("download_leanback_ime", chooseAppStoreDialog) - } else { - val chooseAppStoreDialog = DialogModel.ChooseAppStore( - title = getString(R.string.dialog_title_choose_download_gui_keyboard), - message = getString(R.string.dialog_message_choose_download_gui_keyboard), - model = ChooseAppStoreModel( - playStoreLink = getString(R.string.url_play_store_keymapper_gui_keyboard), - fdroidLink = getString(R.string.url_fdroid_keymapper_gui_keyboard), - githubLink = getString(R.string.url_github_keymapper_gui_keyboard), - ), - negativeButtonText = str(R.string.neg_cancel), - ) - - viewModel.showDialog("download_gui_keyboard", chooseAppStoreDialog) - } - } - - true - } - - addPreference(this) - } - - Preference(requireContext()).apply { - key = KEY_ENABLE_COMPATIBLE_IME - - if (isTvDevice) { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_enable_ime_leanback) - } else { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_enable_ime_gui) - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - viewModel.onEnableCompatibleImeClick() - - true - } - - addPreference(this) - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isCompatibleImeEnabled.collectLatest { isCompatibleImeEnabled -> - icon = if (isCompatibleImeEnabled) { - drawable(R.drawable.ic_outline_check_circle_outline_24) - } else { - drawable(R.drawable.ic_baseline_error_outline_24) - } - } - } - } - - Preference(requireContext()).apply { - key = KEY_CHOSE_COMPATIBLE_IME - setTitle(R.string.title_pref_devices_to_reroute_keyevents_choose_ime) - isSingleLineTitle = false - setOnPreferenceClickListener { - viewModel.onChooseCompatibleImeClick() - - true - } - - addPreference(this) - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isCompatibleImeChosen.collectLatest { isCompatibleImeChosen -> - icon = if (isCompatibleImeChosen) { - drawable(R.drawable.ic_outline_check_circle_outline_24) - } else { - drawable(R.drawable.ic_baseline_error_outline_24) - } - } - } - } - - addPreference( - SettingsUtils.createChooseDevicesPreference( - requireContext(), - viewModel, - Keys.devicesToRerouteKeyEvents, - R.string.title_pref_devices_to_reroute_keyevents_choose_devices, - ), - ) - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.rerouteKeyEvents.collectLatest { enabled -> - for (i in 0 until preferenceCount) { - getPreference(i).apply { - if (this.key != Keys.rerouteKeyEvents.name) { - this.isVisible = enabled - } - } - } - } - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticallyChangeImeSettings.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticallyChangeImeSettings.kt deleted file mode 100644 index a52487e9f3..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticallyChangeImeSettings.kt +++ /dev/null @@ -1,136 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.annotation.RequiresApi -import androidx.preference.Preference -import androidx.preference.SwitchPreferenceCompat -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.system.notifications.NotificationController -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.PreferenceDefaults -import io.github.sds100.keymapper.system.notifications.NotificationUtils - -class AutomaticallyChangeImeSettings : BaseSettingsFragment() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // show on-screen messages when changing keyboards - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showToastWhenAutoChangingIme.name - - setDefaultValue(PreferenceDefaults.SHOW_TOAST_WHEN_AUTO_CHANGE_IME) - isSingleLineTitle = false - setTitle(R.string.title_pref_show_toast_when_auto_changing_ime) - - addPreference(this) - } - - // automatically change ime on input focus - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.changeImeOnInputFocus.name - - setDefaultValue(PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS) - isSingleLineTitle = false - setTitle(R.string.title_pref_auto_change_ime_on_input_focus) - setSummary(R.string.summary_pref_auto_change_ime_on_input_focus) - - addPreference(this) - } - - // automatically change the keyboard when a bluetooth device (dis)connects - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.changeImeOnDeviceConnect.name - setDefaultValue(false) - - isSingleLineTitle = false - setTitle(R.string.title_pref_auto_change_ime_on_connection) - setSummary(R.string.summary_pref_auto_change_ime_on_connection) - - addPreference(this) - } - - addPreference( - SettingsUtils.createChooseDevicesPreference( - requireContext(), - viewModel, - Keys.devicesThatChangeIme, - ), - ) - - // toggle keyboard when toggling key maps - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.toggleKeyboardOnToggleKeymaps.name - setDefaultValue(false) - - isSingleLineTitle = false - setTitle(R.string.title_pref_toggle_keyboard_on_toggle_keymaps) - setSummary(R.string.summary_pref_toggle_keyboard_on_toggle_keymaps) - - addPreference(this) - } - - // toggle keyboard notification - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // show a preference linking to the notification management screen - Preference(requireContext()).apply { - key = Keys.showToggleKeyboardNotification.name - - setTitle(R.string.title_pref_show_toggle_keyboard_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_toggle_keyboard_notification) - - setOnPreferenceClickListener { - onToggleKeyboardNotificationClick() - - true - } - - addPreference(this) - } - } else { - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showToggleKeyboardNotification.name - setDefaultValue(true) - - setTitle(R.string.title_pref_show_toggle_keyboard_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_toggle_keyboard_notification) - - addPreference(this) - } - } - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun onToggleKeyboardNotificationClick() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - !viewModel.isNotificationPermissionGranted() - ) { - viewModel.requestNotificationsPermission() - return - } - - NotificationUtils.openChannelSettings( - ctx = requireContext(), - channelId = NotificationController.CHANNEL_TOGGLE_KEYBOARD, - ) - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/BaseSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/BaseSettingsFragment.kt deleted file mode 100644 index 8bdfdbcbec..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/BaseSettingsFragment.kt +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Bundle -import android.view.View -import androidx.activity.addCallback -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.preference.PreferenceFragmentCompat -import com.google.android.material.bottomappbar.BottomAppBar -import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.str -import io.github.sds100.keymapper.system.url.UrlUtils - -@AndroidEntryPoint -abstract class BaseSettingsFragment : PreferenceFragmentCompat() { - - protected val viewModel: SettingsViewModel by viewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> - val insets = - insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() or WindowInsetsCompat.Type.ime()) - v.updatePadding(insets.left, insets.top, insets.right, insets.bottom) - WindowInsetsCompat.CONSUMED - } - - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { - onBackPressed() - } - - view.findViewById(R.id.appBar).apply { - replaceMenu(R.menu.menu_settings) - - setNavigationOnClickListener { - onBackPressed() - } - - setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_help -> { - UrlUtils.openUrl(requireContext(), str(R.string.url_settings_guide)) - true - } - - else -> false - } - } - } - } - - private fun onBackPressed() { - findNavController().navigateUp() - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsFragment.kt deleted file mode 100644 index 051ba20271..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsFragment.kt +++ /dev/null @@ -1,206 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.preference.Preference -import androidx.preference.SeekBarPreference -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.SliderMaximums -import io.github.sds100.keymapper.base.utils.ui.SliderMinimums -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.PreferenceDefaults -import kotlinx.coroutines.flow.collectLatest - -class DefaultOptionsSettingsFragment : BaseSettingsFragment() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - - // these must all start after the preference screen has been populated so that findPreference works. - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultLongPressDelay.collectLatest { value -> - val preference = findPreference(Keys.defaultLongPressDelay.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultDoublePressDelay.collectLatest { value -> - val preference = - findPreference(Keys.defaultDoublePressDelay.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultSequenceTriggerTimeout.collectLatest { value -> - val preference = - findPreference(Keys.defaultSequenceTriggerTimeout.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultRepeatRate.collectLatest { value -> - val preference = findPreference(Keys.defaultRepeatRate.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultRepeatDelay.collectLatest { value -> - val preference = findPreference(Keys.defaultRepeatDelay.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultVibrateDuration.collectLatest { value -> - val preference = findPreference(Keys.defaultVibrateDuration.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // long press delay - SeekBarPreference(requireContext()).apply { - key = Keys.defaultLongPressDelay.name - setDefaultValue(PreferenceDefaults.LONG_PRESS_DELAY) - - setTitle(R.string.title_pref_long_press_delay) - isSingleLineTitle = false - setSummary(R.string.summary_pref_long_press_delay) - min = SliderMinimums.TRIGGER_LONG_PRESS_DELAY - max = 5000 - showSeekBarValue = true - - addPreference(this) - } - - // double press delay - SeekBarPreference(requireContext()).apply { - key = Keys.defaultDoublePressDelay.name - setDefaultValue(PreferenceDefaults.DOUBLE_PRESS_DELAY) - - setTitle(R.string.title_pref_double_press_delay) - isSingleLineTitle = false - setSummary(R.string.summary_pref_double_press_delay) - min = SliderMinimums.TRIGGER_DOUBLE_PRESS_DELAY - max = 5000 - showSeekBarValue = true - - addPreference(this) - } - - // vibration duration - SeekBarPreference(requireContext()).apply { - key = Keys.defaultVibrateDuration.name - setDefaultValue(PreferenceDefaults.VIBRATION_DURATION) - - setTitle(R.string.title_pref_vibration_duration) - isSingleLineTitle = false - setSummary(R.string.summary_pref_vibration_duration) - min = SliderMinimums.VIBRATION_DURATION - max = 1000 - showSeekBarValue = true - - addPreference(this) - } - - // repeat delay - SeekBarPreference(requireContext()).apply { - key = Keys.defaultRepeatDelay.name - setDefaultValue(PreferenceDefaults.REPEAT_DELAY) - - setTitle(R.string.title_pref_repeat_delay) - isSingleLineTitle = false - setSummary(R.string.summary_pref_repeat_delay) - min = SliderMinimums.ACTION_REPEAT_DELAY - max = SliderMaximums.ACTION_REPEAT_DELAY - showSeekBarValue = true - - addPreference(this) - } - - // repeat rate - SeekBarPreference(requireContext()).apply { - key = Keys.defaultRepeatRate.name - setDefaultValue(PreferenceDefaults.REPEAT_RATE) - - setTitle(R.string.title_pref_repeat_rate) - isSingleLineTitle = false - setSummary(R.string.summary_pref_repeat_rate) - min = SliderMinimums.ACTION_REPEAT_RATE - max = SliderMaximums.ACTION_REPEAT_RATE - showSeekBarValue = true - - addPreference(this) - } - - // sequence trigger timeout - SeekBarPreference(requireContext()).apply { - key = Keys.defaultSequenceTriggerTimeout.name - setDefaultValue(PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT) - - setTitle(R.string.title_pref_sequence_trigger_timeout) - isSingleLineTitle = false - setSummary(R.string.summary_pref_sequence_trigger_timeout) - min = SliderMinimums.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT - max = 5000 - showSeekBarValue = true - - addPreference(this) - } - - Preference(requireContext()).apply { - setTitle(R.string.title_pref_reset_defaults) - - setOnPreferenceClickListener { - viewModel.resetDefaultMappingOptions() - true - } - - addPreference(this) - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ImePickerSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ImePickerSettingsFragment.kt deleted file mode 100644 index 84a58b17ba..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ImePickerSettingsFragment.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.annotation.RequiresApi -import androidx.preference.Preference -import androidx.preference.SwitchPreferenceCompat -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.system.notifications.NotificationController -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.system.notifications.NotificationUtils - -class ImePickerSettingsFragment : BaseSettingsFragment() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // show keyboard picker notification - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // show a preference linking to the notification management screen - Preference(requireContext()).apply { - key = Keys.showImePickerNotification.name - - setTitle(R.string.title_pref_show_ime_picker_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_ime_picker_notification) - - setOnPreferenceClickListener { - onImePickerNotificationClick() - - true - } - - addPreference(this) - } - } else { - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showImePickerNotification.name - setDefaultValue(false) - - setTitle(R.string.title_pref_show_ime_picker_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_ime_picker_notification) - - addPreference(this) - } - } - - // auto show keyboard picker - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showImePickerOnDeviceConnect.name - setDefaultValue(false) - - setTitle(R.string.title_pref_auto_show_ime_picker) - isSingleLineTitle = false - setSummary(R.string.summary_pref_auto_show_ime_picker) - - addPreference(this) - } - - addPreference( - SettingsUtils.createChooseDevicesPreference( - requireContext(), - viewModel, - Keys.devicesThatShowImePicker, - ), - ) - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun onImePickerNotificationClick() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - !viewModel.isNotificationPermissionGranted() - ) { - viewModel.requestNotificationsPermission() - return - } - - NotificationUtils.openChannelSettings( - requireContext(), - NotificationController.CHANNEL_IME_PICKER, - ) - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt deleted file mode 100644 index 3d2241a257..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt +++ /dev/null @@ -1,486 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.annotation.SuppressLint -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.annotation.RequiresApi -import androidx.lifecycle.Lifecycle -import androidx.navigation.fragment.findNavController -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import androidx.preference.SwitchPreferenceCompat -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.system.notifications.NotificationController -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.common.utils.firstBlocking -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.system.notifications.NotificationUtils -import io.github.sds100.keymapper.system.shizuku.ShizukuUtils -import kotlinx.coroutines.flow.collectLatest - -class MainSettingsFragment : BaseSettingsFragment() { - - companion object { - private const val KEY_GRANT_WRITE_SECURE_SETTINGS = "pref_key_grant_write_secure_settings" - private const val CATEGORY_KEY_GRANT_WRITE_SECURE_SETTINGS = - "category_key_grant_write_secure_settings" - private const val KEY_GRANT_SHIZUKU = "pref_key_grant_shizuku" - private const val KEY_AUTOMATICALLY_CHANGE_IME_LINK = - "pref_automatically_change_ime_link" - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isWriteSecureSettingsPermissionGranted.collectLatest { isGranted -> - val writeSecureSettingsCategory = - findPreference(CATEGORY_KEY_GRANT_WRITE_SECURE_SETTINGS) - - findPreference(KEY_GRANT_WRITE_SECURE_SETTINGS)?.apply { - isEnabled = !isGranted - - if (isGranted) { - setTitle(R.string.title_pref_grant_write_secure_settings_granted) - setIcon(R.drawable.ic_outline_check_circle_outline_24) - } else { - setTitle(R.string.title_pref_grant_write_secure_settings_not_granted) - setIcon(R.drawable.ic_baseline_error_outline_24) - } - } - - writeSecureSettingsCategory - ?.findPreference(KEY_AUTOMATICALLY_CHANGE_IME_LINK)?.apply { - isEnabled = isGranted - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isShizukuPermissionGranted.collectLatest { isGranted -> - findPreference(KEY_GRANT_SHIZUKU)?.apply { - if (isGranted) { - setTitle(R.string.title_pref_grant_shizuku_granted) - setIcon(R.drawable.ic_outline_check_circle_outline_24) - } else { - setTitle(R.string.title_pref_grant_shizuku_not_granted) - setIcon(R.drawable.ic_baseline_error_outline_24) - } - } - } - } - } - - 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 -// setDefaultValue(PreferenceDefaults.DARK_THEME) -// isSingleLineTitle = false -// -// setTitle(R.string.title_pref_dark_theme) -// setSummary(R.string.summary_pref_dark_theme) -// entries = strArray(R.array.pref_dark_theme_entries) -// entryValues = Theme.THEMES.map { it.toString() }.toTypedArray() -// -// addPreference(this) -// } - - // automatic backup location - - // hide home screen alerts - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.hideHomeScreenAlerts.name - setDefaultValue(false) - - setTitle(R.string.title_pref_hide_home_screen_alerts) - isSingleLineTitle = false - setSummary(R.string.summary_pref_hide_home_screen_alerts) - - addPreference(this) - } - - // force vibrate - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.forceVibrate.name - setDefaultValue(false) - - setTitle(R.string.title_pref_force_vibrate) - isSingleLineTitle = false - setSummary(R.string.summary_pref_force_vibrate) - - addPreference(this) - } - - // show device descriptors - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showDeviceDescriptors.name - setDefaultValue(false) - isSingleLineTitle = false - - setTitle(R.string.title_pref_show_device_descriptors) - setSummary(R.string.summary_pref_show_device_descriptors) - - addPreference(this) - } - - // toggle key maps notification - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // show a preference linking to the notification management screen - Preference(requireContext()).apply { - key = Keys.showToggleKeyMapsNotification.name - - setTitle(R.string.title_pref_show_toggle_keymaps_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_toggle_keymaps_notification) - - setOnPreferenceClickListener { - onToggleKeyMapsNotificationClick() - - true - } - - addPreference(this) - } - } else { - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showToggleKeyMapsNotification.name - setDefaultValue(true) - - setTitle(R.string.title_pref_show_toggle_keymaps_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_toggle_keymaps_notification) - - addPreference(this) - } - } - - // default options - Preference(requireContext()).apply { - setTitle(R.string.title_pref_default_options) - setSummary(R.string.summary_pref_default_options) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = MainSettingsFragmentDirections.toDefaultOptionsSettingsFragment() - findNavController().navigate(direction) - - true - } - - addPreference(this) - } - - // apps can't show the keyboard picker when in the background from Android 8.1+ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) { - Preference(requireContext()).apply { - setTitle(R.string.title_pref_category_ime_picker) - setSummary(R.string.summary_pref_category_ime_picker) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = MainSettingsFragmentDirections.toImePickerSettingsFragment() - findNavController().navigate(direction) - - true - } - - addPreference(this) - } - } - - // delete sound files - Preference(requireContext()).apply { - setTitle(R.string.title_pref_delete_sound_files) - setSummary(R.string.summary_pref_delete_sound_files) - isSingleLineTitle = false - - setOnPreferenceClickListener { - viewModel.onDeleteSoundFilesClick() - - true - } - - addPreference(this) - } - - // link to settings to automatically change the ime - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - addPreference(automaticallyChangeImeSettingsLink()) - } - - // android 11 device id reset work around - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - Preference(requireContext()).apply { - setTitle(R.string.title_pref_reroute_keyevents_link) - setSummary(R.string.summary_pref_reroute_keyevents_link) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = - MainSettingsFragmentDirections.toAndroid11BugWorkaroundSettingsFragment() - findNavController().navigate(direction) - - true - } - - addPreference(this) - } - } - - // Shizuku - // shizuku is only supported on Marhsmallow+ - if (ShizukuUtils.isSupportedForSdkVersion()) { - Preference(requireContext()).apply { - setTitle(R.string.title_pref_category_shizuku) - setSummary(R.string.summary_pref_category_shizuku) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = - MainSettingsFragmentDirections.toShizukuSettingsFragment() - findNavController().navigate(direction) - - true - } - - addPreference(this) - } - } - - Preference(requireContext()).apply { - setTitle(R.string.title_pref_reset_settings) - setSummary(R.string.summary_pref_reset_settings) - - setOnPreferenceClickListener { - viewModel.onResetAllSettingsClick() - true - } - - addPreference(this) - } - - // write secure settings - PreferenceCategory(requireContext()).apply { - key = CATEGORY_KEY_GRANT_WRITE_SECURE_SETTINGS - setTitle(R.string.title_pref_category_write_secure_settings) - - preferenceScreen.addPreference(this) - - Preference(requireContext()).apply { - isSelectable = false - setSummary(R.string.summary_pref_category_write_secure_settings) - - addPreference(this) - } - - Preference(requireContext()).apply { - key = KEY_GRANT_WRITE_SECURE_SETTINGS - - if (viewModel.isWriteSecureSettingsPermissionGranted.firstBlocking()) { - setTitle(R.string.title_pref_grant_write_secure_settings_granted) - setIcon(R.drawable.ic_outline_check_circle_outline_24) - } else { - setTitle(R.string.title_pref_grant_write_secure_settings_not_granted) - setIcon(R.drawable.ic_baseline_error_outline_24) - } - - setOnPreferenceClickListener { - viewModel.requestWriteSecureSettingsPermission() - true - } - - addPreference(this) - } - - // accessibility services can change the ime on Android 11+ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - addPreference(automaticallyChangeImeSettingsLink()) - } - } - - // root - createRootCategory() - - // log - createLogCategory() - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun onToggleKeyMapsNotificationClick() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - !viewModel.isNotificationPermissionGranted() - ) { - viewModel.requestNotificationsPermission() - return - } - - NotificationUtils.openChannelSettings( - requireContext(), - NotificationController.CHANNEL_TOGGLE_KEYMAPS, - ) - } - - private fun automaticallyChangeImeSettingsLink() = Preference(requireContext()).apply { - key = KEY_AUTOMATICALLY_CHANGE_IME_LINK - - setTitle(R.string.title_pref_automatically_change_ime) - setSummary(R.string.summary_pref_automatically_change_ime) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = MainSettingsFragmentDirections.toAutomaticallyChangeImeSettings() - findNavController().navigate(direction) - - true - } - } - - private fun createLogCategory() = PreferenceCategory(requireContext()).apply { - setTitle(R.string.title_pref_category_log) - preferenceScreen.addPreference(this) - - Preference(requireContext()).apply { - isSelectable = false - setSummary(R.string.summary_pref_category_log) - - addPreference(this) - } - - // enable logging - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.log.name - setDefaultValue(false) - - isSingleLineTitle = false - setTitle(R.string.title_pref_toggle_logging) - - addPreference(this) - } - - // open log fragment - Preference(requireContext()).apply { - isSingleLineTitle = false - setTitle(R.string.title_pref_view_and_share_log) - - setOnPreferenceClickListener { - findNavController().navigate(MainSettingsFragmentDirections.toLogFragment()) - - true - } - - addPreference(this) - } - - // report issue to developer -// Preference(requireContext()).apply { -// isSingleLineTitle = false -// setTitle(R.string.title_pref_report_issue) -// -// setOnPreferenceClickListener { -// -// true -// } -// -// addPreference(this) -// } - } - - @SuppressLint("NewApi") - private fun createRootCategory() = PreferenceCategory(requireContext()).apply { - setTitle(R.string.title_pref_category_root) - preferenceScreen.addPreference(this) - - Preference(requireContext()).apply { - isSelectable = false - setSummary(R.string.summary_pref_category_root) - - addPreference(this) - } - - // root permission switch - 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) - } - - // only show the options to show the keyboard picker when rooted in these versions - if (Build.VERSION.SDK_INT in Build.VERSION_CODES.O_MR1..Build.VERSION_CODES.P) { - // show a preference linking to the notification management screen - Preference(requireContext()).apply { - key = Keys.showImePickerNotification.name - - setTitle(R.string.title_pref_show_ime_picker_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_ime_picker_notification) - - setOnPreferenceClickListener { - NotificationUtils.openChannelSettings( - requireContext(), - NotificationController.CHANNEL_IME_PICKER, - ) - - true - } - - addPreference(this) - } - - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showImePickerOnDeviceConnect.name - setDefaultValue(false) - - setTitle(R.string.title_pref_auto_show_ime_picker) - isSingleLineTitle = false - setSummary(R.string.summary_pref_auto_show_ime_picker) - - addPreference(this) - } - - addPreference( - SettingsUtils.createChooseDevicesPreference( - requireContext(), - viewModel, - Keys.devicesThatShowImePicker, - ), - ) - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsUtils.kt deleted file mode 100644 index d1728bdb56..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.content.Context -import androidx.annotation.StringRes -import androidx.datastore.preferences.core.Preferences -import androidx.preference.Preference -import io.github.sds100.keymapper.base.R - -object SettingsUtils { - - fun createChooseDevicesPreference( - ctx: Context, - settingsViewModel: SettingsViewModel, - key: Preferences.Key>, - @StringRes title: Int = R.string.title_pref_choose_devices, - ): Preference = Preference(ctx).apply { - this.key = key.name - - setTitle(title) - isSingleLineTitle = false - - setOnPreferenceClickListener { - settingsViewModel.chooseDevicesForPreference(key) - - true - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ShizukuSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ShizukuSettingsFragment.kt deleted file mode 100644 index d55063e4f9..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ShizukuSettingsFragment.kt +++ /dev/null @@ -1,137 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.preference.Preference -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.drawable -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.str -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.system.url.UrlUtils -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.launchIn - -class ShizukuSettingsFragment : BaseSettingsFragment() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - setPreferencesFromResource(R.xml.preferences_empty, rootKey) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // summary - Preference(requireContext()).apply { - setSummary(R.string.summary_pref_category_shizuku_follow_steps) - addPreference(this) - } - - // install shizuku - Preference(requireContext()).apply { - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isShizukuInstalled.collectLatest { isInstalled -> - if (isInstalled) { - icon = drawable(R.drawable.ic_outline_check_circle_outline_24) - setTitle(R.string.title_pref_grant_shizuku_install_app_installed) - isEnabled = false - } else { - icon = drawable(R.drawable.ic_baseline_error_outline_24) - setTitle(R.string.title_pref_grant_shizuku_install_app_not_installed) - isEnabled = true - } - } - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - if (!viewModel.isShizukuInstalled.value) { - viewModel.downloadShizuku() - } - - true - } - - addPreference(this) - } - - // start shizuku - Preference(requireContext()).apply { - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - combine( - viewModel.isShizukuInstalled, - viewModel.isShizukuStarted, - ) { isInstalled, isStarted -> - isEnabled = isInstalled - - if (isStarted) { - icon = drawable(R.drawable.ic_outline_check_circle_outline_24) - setTitle(R.string.title_pref_grant_shizuku_started) - } else { - icon = drawable(R.drawable.ic_baseline_error_outline_24) - setTitle(R.string.title_pref_grant_shizuku_not_started) - } - }.launchIn(this) - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - if (!viewModel.isShizukuStarted.value) { - viewModel.openShizukuApp() - } - - true - } - - addPreference(this) - } - - // grant shizuku permission - Preference(requireContext()).apply { - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - combine( - viewModel.isShizukuStarted, - viewModel.isShizukuPermissionGranted, - ) { isStarted, isGranted -> - isEnabled = isStarted - - if (isGranted) { - icon = drawable(R.drawable.ic_outline_check_circle_outline_24) - setTitle(R.string.title_pref_grant_shizuku_granted) - } else { - icon = drawable(R.drawable.ic_baseline_error_outline_24) - setTitle(R.string.title_pref_grant_shizuku_not_granted) - } - }.launchIn(this) - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - if (viewModel.isShizukuPermissionGranted.value) { - UrlUtils.openUrl(requireContext(), str(R.string.url_shizuku_setting_benefits)) - } else { - viewModel.requestShizukuPermission() - } - - true - } - - addPreference(this) - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index aabd26a6f3..4f605fc21c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -61,7 +61,11 @@ class NotificationController @Inject constructor( private const val ID_FEATURE_FLOATING_BUTTONS = 901 const val CHANNEL_TOGGLE_KEYMAPS = "channel_toggle_remaps" + + // TODO delete all ime picker notifications and auto showing logic. + @Deprecated("Removed in 4.0.0") const val CHANNEL_IME_PICKER = "channel_ime_picker" + const val CHANNEL_KEYBOARD_HIDDEN = "channel_warning_keyboard_hidden" const val CHANNEL_TOGGLE_KEYBOARD = "channel_toggle_keymapper_keyboard" const val CHANNEL_NEW_FEATURES = "channel_new_features" diff --git a/base/src/main/res/navigation/nav_base_app.xml b/base/src/main/res/navigation/nav_base_app.xml index 469fe210d6..54b478e60c 100644 --- a/base/src/main/res/navigation/nav_base_app.xml +++ b/base/src/main/res/navigation/nav_base_app.xml @@ -5,8 +5,6 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_base_app"> - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 23381b651f..4b63fedf42 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -622,7 +622,8 @@ Turn on automatic backup Periodically back up your key maps - Choose devices + Choose devices + Choose which devices will show the input method picker Automatically show keyboard picker When a device that you have chosen connects or disconnects the keyboard picker will show automatically. Choose the devices below. @@ -713,7 +714,7 @@ - Automatically show the keyboard picker + Keyboard picker Tap to see the settings that allow you to automatically show the keyboard picker. Root settings From e87fea3648b1246be8df6e8886a11dde69c43196 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 20:25:16 +0100 Subject: [PATCH 154/215] #1394 enable extractNativeLibs in manifest so system bridge native library is extracted --- app/src/main/AndroidManifest.xml | 3 ++- sysbridge/src/main/AndroidManifest.xml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 698524e2c1..c4e87ba265 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ + of the app is 26.--> diff --git a/sysbridge/src/main/AndroidManifest.xml b/sysbridge/src/main/AndroidManifest.xml index dc5abee567..dad1614a20 100644 --- a/sysbridge/src/main/AndroidManifest.xml +++ b/sysbridge/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - + From 3d831d1f6a26c26527f30be45cc769a8eede7ce7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 20:29:09 +0100 Subject: [PATCH 155/215] #1394 update string --- base/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 4b63fedf42..b284af684f 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1659,7 +1659,7 @@ PRO mode started! PRO mode is now running and you can remap more buttons PRO mode is running - You can now remap more buttons and use more actions. + You can now remap buttons when the screen is off and use more actions. Go back From bd97231c7616056fd081a71eb049b25de5d00de9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 21:27:18 +0100 Subject: [PATCH 156/215] #1394 improve pro mode setup process --- .../base/promode/ProModeSetupScreen.kt | 24 +++++++++----- .../base/promode/ProModeSetupViewModel.kt | 32 ++++++++++++------ .../SystemBridgeSetupAssistantController.kt | 17 +++++----- .../base/promode/SystemBridgeSetupUseCase.kt | 4 +-- base/src/main/res/values/strings.xml | 11 +++++-- .../keymapper/common/utils/SettingsUtils.kt | 24 +++++++++++++- .../service/SystemBridgeSetupController.kt | 33 +++++++++++++++++-- .../sysbridge/starter/SystemBridgeStarter.kt | 1 + 8 files changed, 110 insertions(+), 36 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index af78c21e6d..729f336147 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -167,8 +167,7 @@ fun ProModeSetupScreen( AssistantCheckBoxRow( modifier = Modifier.fillMaxWidth(), - isEnabled = state.data.step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE - && state.data.step != SystemBridgeSetupStep.STARTED, + isEnabled = state.data.isSetupAssistantButtonEnabled, isChecked = state.data.isSetupAssistantChecked, onAssistantClick = onAssistantClick ) @@ -371,7 +370,8 @@ private fun ProModeSetupScreenAccessibilityServicePreview() { stepNumber = 1, stepCount = 6, step = SystemBridgeSetupStep.ACCESSIBILITY_SERVICE, - isSetupAssistantChecked = false + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = false ) ) ) @@ -388,7 +388,8 @@ private fun ProModeSetupScreenNotificationPermissionPreview() { stepNumber = 2, stepCount = 6, step = SystemBridgeSetupStep.NOTIFICATION_PERMISSION, - isSetupAssistantChecked = false + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = true ) ) ) @@ -405,7 +406,8 @@ private fun ProModeSetupScreenDeveloperOptionsPreview() { stepNumber = 2, stepCount = 6, step = SystemBridgeSetupStep.DEVELOPER_OPTIONS, - isSetupAssistantChecked = false + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = true ) ) ) @@ -422,7 +424,8 @@ private fun ProModeSetupScreenWifiNetworkPreview() { stepNumber = 3, stepCount = 6, step = SystemBridgeSetupStep.WIFI_NETWORK, - isSetupAssistantChecked = false + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = true ) ) ) @@ -439,7 +442,8 @@ private fun ProModeSetupScreenWirelessDebuggingPreview() { stepNumber = 4, stepCount = 6, step = SystemBridgeSetupStep.WIRELESS_DEBUGGING, - isSetupAssistantChecked = false + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = true ) ) ) @@ -456,7 +460,8 @@ private fun ProModeSetupScreenAdbPairingPreview() { stepNumber = 5, stepCount = 6, step = SystemBridgeSetupStep.ADB_PAIRING, - isSetupAssistantChecked = true + isSetupAssistantChecked = true, + isSetupAssistantButtonEnabled = true ) ) ) @@ -473,7 +478,8 @@ private fun ProModeSetupScreenStartServicePreview() { stepNumber = 6, stepCount = 6, step = SystemBridgeSetupStep.START_SERVICE, - isSetupAssistantChecked = true + isSetupAssistantChecked = true, + isSetupAssistantButtonEnabled = true ) ) ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt index 59cfc11557..43e024aebc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt @@ -35,7 +35,7 @@ class ProModeSetupViewModel @Inject constructor( when (currentStep) { SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> useCase.enableAccessibilityService() SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> useCase.requestNotificationPermission() - SystemBridgeSetupStep.DEVELOPER_OPTIONS -> useCase.openDeveloperOptions() + SystemBridgeSetupStep.DEVELOPER_OPTIONS -> useCase.enableDeveloperOptions() SystemBridgeSetupStep.WIFI_NETWORK -> useCase.connectWifiNetwork() SystemBridgeSetupStep.WIRELESS_DEBUGGING -> useCase.enableWirelessDebugging() SystemBridgeSetupStep.ADB_PAIRING -> useCase.pairWirelessAdb() @@ -57,20 +57,32 @@ class ProModeSetupViewModel @Inject constructor( private fun buildState( step: SystemBridgeSetupStep, - isSetupAssistantEnabled: Boolean - ): State.Data = State.Data( - ProModeSetupState( - stepNumber = step.stepIndex + 1, - stepCount = SystemBridgeSetupStep.entries.size, - step = step, - isSetupAssistantChecked = isSetupAssistantEnabled + isSetupAssistantUserEnabled: Boolean + ): State.Data { + // Uncheck the setup assistant if the accessibility service is disabled since it is + // required for the setup assistant to work + val isSetupAssistantChecked = if (step == SystemBridgeSetupStep.ACCESSIBILITY_SERVICE) { + false + } else { + isSetupAssistantUserEnabled + } + + return State.Data( + ProModeSetupState( + stepNumber = step.stepIndex + 1, + stepCount = SystemBridgeSetupStep.entries.size, + step = step, + isSetupAssistantChecked = isSetupAssistantChecked, + isSetupAssistantButtonEnabled = step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE && step != SystemBridgeSetupStep.STARTED + ) ) - ) + } } data class ProModeSetupState( val stepNumber: Int, val stepCount: Int, val step: SystemBridgeSetupStep, - val isSetupAssistantChecked: Boolean + val isSetupAssistantChecked: Boolean, + val isSetupAssistantButtonEnabled: Boolean ) \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index 5500563c7c..7bb37a39ad 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -74,7 +74,8 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private enum class InteractionStep { - WIRELESS_DEBUGGING_SWITCH, + // Do not automatically turn on the wireless debugging switch. When the user turns it on, + // Key Mapper will automatically pair. PAIR_DEVICE, } @@ -144,7 +145,6 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private fun doInteractiveStep(step: InteractionStep, rootNode: AccessibilityNodeInfo) { when (step) { - InteractionStep.WIRELESS_DEBUGGING_SWITCH -> TODO() InteractionStep.PAIR_DEVICE -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { doPairingInteractiveStep(rootNode) @@ -255,22 +255,21 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( when (step) { SystemBridgeSetupStep.DEVELOPER_OPTIONS -> { - + showNotification( + getString(R.string.pro_mode_setup_notification_tap_build_number_title), + getString(R.string.pro_mode_setup_notification_tap_build_number_text), + ) } - SystemBridgeSetupStep.WIRELESS_DEBUGGING -> {} - SystemBridgeSetupStep.ADB_PAIRING -> { showNotification( - "Pairing automatically", - "Searching for pairing code and port..." + getString(R.string.pro_mode_setup_notification_pairing_title), + getString(R.string.pro_mode_setup_notification_pairing_text), ) - interactionStep = InteractionStep.PAIR_DEVICE } - SystemBridgeSetupStep.START_SERVICE -> {} else -> {} // Do nothing } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index f4a8239e9e..27ba263962 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -117,7 +117,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( accessibilityServiceAdapter.start() } - override fun openDeveloperOptions() { + override fun enableDeveloperOptions() { systemBridgeSetupController.enableDeveloperOptions() } @@ -186,7 +186,7 @@ interface SystemBridgeSetupUseCase { fun stopSystemBridge() fun enableAccessibilityService() - fun openDeveloperOptions() + fun enableDeveloperOptions() fun connectWifiNetwork() fun enableWirelessDebugging() fun pairWirelessAdb() diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index b284af684f..e40724ccb0 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1651,8 +1651,9 @@ Start service Key Mapper needs to connect to the Android Debug Bridge to start the PRO mode service. - Notification permission - Key Mapper will use notifications to help guide you through the process + + Allow notifications + Key Mapper needs permission to notify you if there are any issues with the set up process. Give permission Setup assistant @@ -1662,4 +1663,10 @@ You can now remap buttons when the screen is off and use more actions. Go back + Enable developer options + Tap build number repeatedly + + Pairing automatically + Searching for pairing code and port… + diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt index 01e6c6af27..ea27098732 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt @@ -4,9 +4,16 @@ import android.Manifest import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper import android.provider.Settings import androidx.annotation.RequiresPermission import androidx.core.os.bundleOf +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow import timber.log.Timber object SettingsUtils { @@ -116,7 +123,7 @@ object SettingsUtils { } fun launchSettingsScreen(ctx: Context, action: String, fragmentArg: String?) { - val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply { + val intent = Intent(action).apply { if (fragmentArg != null) { val fragmentArgKey = ":settings:fragment_args_key" val showFragmentArgsKey = ":settings:show_fragment_args" @@ -136,4 +143,19 @@ object SettingsUtils { Timber.e("Failed to start Settings activity: $e") } } + + fun settingsCallbackFlow(ctx: Context, uri: Uri): Flow = callbackFlow { + val observer = + object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + + trySend(Unit) + } + } + + ctx.contentResolver.registerContentObserver(uri, false, observer) + + this.awaitClose { ctx.contentResolver.unregisterContentObserver(observer) } + } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 7f7129aab9..67202ab36c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint +import android.app.ActivityManager import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context @@ -9,8 +10,10 @@ import android.os.Build import android.provider.Settings import android.service.quicksettings.TileService import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.KeyMapperClassProvider import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.isSuccess @@ -35,7 +38,8 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, private val buildConfigProvider: BuildConfigProvider, - private val adbManager: AdbManager + private val adbManager: AdbManager, + private val keyMapperClassProvider: KeyMapperClassProvider ) : SystemBridgeSetupController { companion object { @@ -43,6 +47,8 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private const val ADB_WIRELESS_SETTING = "adb_wifi_enabled" } + private val activityManager: ActivityManager by lazy { ctx.getSystemService()!! } + private val starter: SystemBridgeStarter by lazy { SystemBridgeStarter(ctx, adbManager, buildConfigProvider) } @@ -56,6 +62,19 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override val startSetupAssistantRequest: MutableSharedFlow = MutableSharedFlow() + init { + coroutineScope.launch { + val uri = Settings.Global.getUriFor(ADB_WIRELESS_SETTING) + SettingsUtils.settingsCallbackFlow(ctx, uri).collect { + val value = getWirelessDebuggingEnabled() + + if (value) { + getKeyMapperAppTask()?.moveToFront() + } + } + } + } + override fun startWithRoot() { coroutineScope.launch { starter.startWithRoot() @@ -88,13 +107,15 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } override fun enableDeveloperOptions() { - // TODO show notification after the activity is to tap the Build Number repeatedly - SettingsUtils.launchSettingsScreen( ctx, Settings.ACTION_DEVICE_INFO_SETTINGS, "build_number" ) + + coroutineScope.launch { + startSetupAssistantRequest.emit(SystemBridgeSetupStep.DEVELOPER_OPTIONS) + } } override fun launchEnableWirelessDebuggingAssistant() { @@ -164,6 +185,12 @@ class SystemBridgeSetupControllerImpl @Inject constructor( return false } } + + private fun getKeyMapperAppTask(): ActivityManager.AppTask? { + val task = activityManager.appTasks + .firstOrNull { it.taskInfo.topActivity?.className == keyMapperClassProvider.getMainActivity().name } + return task + } } @SuppressLint("ObsoleteSdkInt") diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index 4dcbf2f03b..5f61f79a42 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -120,6 +120,7 @@ class SystemBridgeStarter( return KMError.Exception(IllegalStateException("User is locked")) } + // TODO enable usb debugging and disable authorization timeout // Get the file that contains the external files return startSystemBridge(executeCommand = adbManager::executeCommand).onFailure { error -> Timber.w("Failed to start system bridge with ADB: $error") From 89c9f01ef491a7b79cf667ee1027081bb8043e47 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 22:04:21 +0100 Subject: [PATCH 157/215] #1394 improve pro mode setup process more --- .../keymapper/base/promode/ProModeScreen.kt | 2 +- .../base/promode/ProModeSetupScreen.kt | 36 +++++++++++-- .../SystemBridgeSetupAssistantController.kt | 23 +++++++-- .../AccessibilityServiceAdapterImpl.kt | 26 +++------- base/src/main/res/values/strings.xml | 2 +- .../keymapper/common/utils/SettingsUtils.kt | 9 ++-- .../service/SystemBridgeSetupController.kt | 51 ++++++++++++++----- 7 files changed, 104 insertions(+), 45 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 03e1c0210b..55bbb9e038 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -281,7 +281,7 @@ private fun SetupSection( }, title = stringResource(R.string.pro_mode_set_up_with_key_mapper_title), content = { - if (state.setupProgress < 1) { + if (state.setupProgress < 1 && state.setupProgress > 0) { LinearProgressIndicator( modifier = Modifier .fillMaxWidth() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index 729f336147..4bc9324af4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -53,6 +54,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons import io.github.sds100.keymapper.base.utils.ui.compose.icons.SignalWifiNotConnected import io.github.sds100.keymapper.common.utils.State @@ -107,7 +109,7 @@ fun ProModeSetupScreen( // Create animated progress for entrance and updates val progressAnimatable = remember { Animatable(0f) } - val targetProgress = state.data.stepNumber.toFloat() / (state.data.stepCount + 1) + val targetProgress = state.data.stepNumber.toFloat() / (state.data.stepCount) // Animate progress when it changes LaunchedEffect(targetProgress) { @@ -172,13 +174,20 @@ fun ProModeSetupScreen( onAssistantClick = onAssistantClick ) + val iconTint = if (state.data.step == SystemBridgeSetupStep.STARTED) { + LocalCustomColorsPalette.current.green + } else { + MaterialTheme.colorScheme.onSurface + } + StepContent( modifier = Modifier .weight(1f) .padding(horizontal = 16.dp), stepContent, onWatchTutorialClick, - onStepButtonClick + onStepButtonClick, + iconTint = iconTint ) } } @@ -191,7 +200,8 @@ private fun StepContent( modifier: Modifier = Modifier, stepContent: StepContent, onWatchTutorialClick: () -> Unit, - onButtonClick: () -> Unit + onButtonClick: () -> Unit, + iconTint: Color = Color.Unspecified, ) { Column( modifier, @@ -207,6 +217,7 @@ private fun StepContent( modifier = Modifier.size(64.dp), imageVector = stepContent.icon, contentDescription = null, + tint = iconTint ) Spacer(modifier = Modifier.height(16.dp)) @@ -486,3 +497,22 @@ private fun ProModeSetupScreenStartServicePreview() { } } + +@Preview(name = "Started", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ProModeSetupScreenStartedPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 8, + stepCount = 8, + step = SystemBridgeSetupStep.STARTED, + isSetupAssistantChecked = true, + isSetupAssistantButtonEnabled = true + ) + ) + ) + } +} + diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index 7bb37a39ad..81ef0fd811 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -66,7 +66,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( /** * The max time to spend searching for an accessibility node. */ - const val NODE_SEARCH_TIMEOUT = 30000L + const val INTERACTION_TIMEOUT = 30000L private val PAIRING_CODE_REGEX = Regex("^\\d{6}$") private val PORT_REGEX = Regex(".*:([0-9]{1,5})") @@ -102,7 +102,14 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( createNotificationChannel() coroutineScope.launch { - setupController.startSetupAssistantRequest.collect(::startSetupStep) + setupController.setupAssistantStep.collect { step -> + if (step == null) { + stopInteracting() + dismissNotification() + } else { + startSetupStep(step) + } + } } } @@ -212,7 +219,8 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private fun clickPairWithCodeButton(rootNode: AccessibilityNodeInfo) { rootNode .findNodeRecursively { it.className == "androidx.recyclerview.widget.RecyclerView" } - ?.getChild(3) + ?.runCatching { getChild(3) } + ?.getOrNull() ?.performAction(AccessibilityNodeInfo.ACTION_CLICK) } @@ -252,6 +260,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private fun startSetupStep(step: SystemBridgeSetupStep) { Timber.i("Starting setup assistant step: $step") + startInteractionTimeoutJob() when (step) { SystemBridgeSetupStep.DEVELOPER_OPTIONS -> { @@ -278,6 +287,14 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( // TODO do this in the timeout job too } + private fun startInteractionTimeoutJob() { + interactionTimeoutJob?.cancel() + interactionTimeoutJob = coroutineScope.launch { + delay(INTERACTION_TIMEOUT) + interactionStep = null + } + } + private fun stopInteracting() { interactionStep = null interactionTimeoutJob?.cancel() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt index a601d51b87..941a9a9f3f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.system.accessibility import android.content.ActivityNotFoundException import android.content.Context -import android.content.Intent import android.database.ContentObserver import android.net.Uri import android.os.Build @@ -178,17 +177,7 @@ class AccessibilityServiceAdapterImpl @Inject constructor( private fun launchAccessibilitySettings(): Boolean { try { - val settingsIntent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - - settingsIntent.addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - or Intent.FLAG_ACTIVITY_CLEAR_TASK - or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - // Add this flag so user only has to press back once. - or Intent.FLAG_ACTIVITY_NO_HISTORY, - ) - - ctx.startActivity(settingsIntent) + SettingsUtils.launchSettingsScreen(ctx, Settings.ACTION_ACCESSIBILITY_SETTINGS) return true } catch (e: ActivityNotFoundException) { @@ -197,15 +186,12 @@ class AccessibilityServiceAdapterImpl @Inject constructor( } private suspend fun disableServiceSuspend() { - // disableSelf method only exists in 7.0.0+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - send(AccessibilityServiceEvent.DisableService).onSuccess { - Timber.i("Disabling service by calling disableSelf()") + send(AccessibilityServiceEvent.DisableService).onSuccess { + Timber.i("Disabling service by calling disableSelf()") - return - }.onFailure { - Timber.i("Failed to disable service by calling disableSelf()") - } + return + }.onFailure { + Timber.i("Failed to disable service by calling disableSelf()") } if (permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)) { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index e40724ccb0..101b05b7b1 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1661,7 +1661,7 @@ PRO mode is now running and you can remap more buttons PRO mode is running You can now remap buttons when the screen is off and use more actions. - Go back + Finish Enable developer options Tap build number repeatedly diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt index ea27098732..d80fdcdbb3 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt @@ -122,7 +122,7 @@ object SettingsUtils { } } - fun launchSettingsScreen(ctx: Context, action: String, fragmentArg: String?) { + fun launchSettingsScreen(ctx: Context, action: String, fragmentArg: String? = null) { val intent = Intent(action).apply { if (fragmentArg != null) { val fragmentArgKey = ":settings:fragment_args_key" @@ -132,9 +132,12 @@ object SettingsUtils { val bundle = bundleOf(fragmentArgKey to fragmentArg) putExtra(showFragmentArgsKey, bundle) - - flags = Intent.FLAG_ACTIVITY_NEW_TASK } + + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_NO_HISTORY or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS } try { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 67202ab36c..b2f4c87f8d 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -17,12 +17,13 @@ import io.github.sds100.keymapper.common.KeyMapperClassProvider import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.isSuccess +import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.sysbridge.adb.AdbManager import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -59,16 +60,28 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override val isWirelessDebuggingEnabled: MutableStateFlow = MutableStateFlow(getWirelessDebuggingEnabled()) - override val startSetupAssistantRequest: MutableSharedFlow = - MutableSharedFlow() + override val setupAssistantStep: MutableStateFlow = + MutableStateFlow(null) init { + // Automatically go back to the Key Mapper app when turning on wireless debugging coroutineScope.launch { val uri = Settings.Global.getUriFor(ADB_WIRELESS_SETTING) SettingsUtils.settingsCallbackFlow(ctx, uri).collect { - val value = getWirelessDebuggingEnabled() + val isEnabled = getWirelessDebuggingEnabled() - if (value) { + if (isEnabled && setupAssistantStep.value == SystemBridgeSetupStep.WIRELESS_DEBUGGING) { + getKeyMapperAppTask()?.moveToFront() + } + } + } + + coroutineScope.launch { + val uri = Settings.Global.getUriFor(DEVELOPER_OPTIONS_SETTING) + SettingsUtils.settingsCallbackFlow(ctx, uri).collect { + val isEnabled = getDeveloperOptionsEnabled() + + if (isEnabled && setupAssistantStep.value == SystemBridgeSetupStep.DEVELOPER_OPTIONS) { getKeyMapperAppTask()?.moveToFront() } } @@ -97,13 +110,21 @@ class SystemBridgeSetupControllerImpl @Inject constructor( launchWirelessDebuggingActivity() coroutineScope.launch { - startSetupAssistantRequest.emit(SystemBridgeSetupStep.ADB_PAIRING) + setupAssistantStep.emit(SystemBridgeSetupStep.ADB_PAIRING) } } @RequiresApi(Build.VERSION_CODES.R) override suspend fun pairWirelessAdb(port: Int, code: Int): KMResult { - return adbManager.pair(port, code) + return adbManager.pair(port, code).onSuccess { + setupAssistantStep.update { value -> + if (value == SystemBridgeSetupStep.ADB_PAIRING) { + null + } else { + value + } + } + } } override fun enableDeveloperOptions() { @@ -114,7 +135,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( ) coroutineScope.launch { - startSetupAssistantRequest.emit(SystemBridgeSetupStep.DEVELOPER_OPTIONS) + setupAssistantStep.emit(SystemBridgeSetupStep.DEVELOPER_OPTIONS) } } @@ -123,7 +144,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( launchWirelessDebuggingActivity() coroutineScope.launch { - startSetupAssistantRequest.emit(SystemBridgeSetupStep.WIRELESS_DEBUGGING) + setupAssistantStep.emit(SystemBridgeSetupStep.WIRELESS_DEBUGGING) } } @@ -151,7 +172,12 @@ class SystemBridgeSetupControllerImpl @Inject constructor( ) ) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_CLEAR_TASK) + addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_NO_HISTORY or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + ) } try { @@ -196,10 +222,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeSetupController { - /** - * The setup assistant should be launched for the given step. - */ - val startSetupAssistantRequest: Flow + val setupAssistantStep: StateFlow val isDeveloperOptionsEnabled: Flow fun enableDeveloperOptions() From e1ae2262b09a12aabbb093b0b84a772318261a51 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 22:18:30 +0100 Subject: [PATCH 158/215] remove users from about screen --- .../main/res/drawable/profile_pic_bydario.png | Bin 54204 -> 0 bytes .../main/res/drawable/profile_pic_kekero.png | Bin 62835 -> 0 bytes base/src/main/res/layout/fragment_about.xml | 26 ------------------ base/src/main/res/values/strings.xml | 7 ++--- 4 files changed, 3 insertions(+), 30 deletions(-) delete mode 100644 base/src/main/res/drawable/profile_pic_bydario.png delete mode 100644 base/src/main/res/drawable/profile_pic_kekero.png diff --git a/base/src/main/res/drawable/profile_pic_bydario.png b/base/src/main/res/drawable/profile_pic_bydario.png deleted file mode 100644 index cbd6e85dadc1766e23906d2f7e6d3dac47983db7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54204 zcmV)9K*hg_P)9AM|(Sag?l+OJA01p zZoSkCNo4E?fKq$g_VaXC%MuABA|oRsq5t#${=XVzsw6S9ZKxsC>?mxvAQi8Hg^?iv z9C!v9(?55Bh*MJR#conKWCC_&W#1?P;A-mrfaRw|Fk6kBU5b^;6;9Y_ z!?)mD`WEHyAR*CaJ^EvTL11VmKmrIn1<@bdKEoNUka&TB6?SQ}KF~+{w&#D|CG_8( z`F-*!{LLUHbCVtrjvr9T?~v$bJ*0aQ8qZK*Ql7*DJD$vpXY+xd6z}0;VR6R#2P zAo4;x5osz~VUs?rb$rA84iJCl_@s(CEb=#7_{G{4tK6W;vsoHI6>p(kz9hvCBy`0^ zvMdPmz=irIJ}e+)LqJu|@I5`-Mvd|<+0_!Ia+7X~8@x6C^YDGuWzo0UFX0QdS*193 z9v^X;TV`Zam`qW!&h}6hO{kF~Clux; zPR96*C2i!{U_HdyB!pF*kI6=Hh!oQ1V!v4%1QT!9NXNVc{j6|;Q|GM zoUA6hwXAyX|erA4)vf2S48(;^cgu5u@%y_99 z@s3ugav|QxD(#SXhS`zCn-JVB%xbO1c9r%(W`i^m&9<5383o?aL%d1G6P(O7w3GjG zfHw4@_5_%Q{%uZhPG3R|7As|ssQNmepoZUr#RAT5kH|#AtF%iEr%bHT$+mcLz=wbOPP`eEqDzj0)h@&2tu?Ywty+H5hR3Uyjd10 zO>o16zoqxkW2~B1IEx2<4(3^gQ)Cq>J?OCNYTX^zV23AUn1TsE%Zq?`wRVF@D+P8z zP5!_WaU&j(wjg9Vi-jm?gP)4l-^gmvOfy@$%1`Q;q4AC;q|Ju-iBF0qJ@8^ZprDg^ zmrvGP=q7fP=z=UJxBn4pXhJ(mav~o1M4qUB8X@Bpb%2G=nY)mMNjXr!oOZ+wLVREJ zJaH;a@(CB}mXxMe*>~klY@t<3nC;a5kan?){!f1;4dBu)z%!I_RF#VAT;)}5mnX-i zEhh`Ikx8vqbvD>Kp5taVF`IiZ+a*=t8rp(MSl;A2u_-6Gm?Q$4Nb`ACdNB20MU-DY z`8<5a&AN*fp8;qcB>q#$qwVM~i`T=6W)yd6KcnJ^N< z6D(+zRy?CSGOFZ%M_UZwU!~O~vT2OBYsO}Sd+Lr>X#RT5M9n-x+enwLH~@K-e>0(h zf5M9a_YFN+0;HSNknmz5-El=5s7Jq;Jo4adUG3i}#ojPt6+52LB1*xbif2)9b)6?} z8`;FJ*h399DeR{#LGF+Tcj+L1>=S@c6L(W^k++hQ`-ib)h9#oWOw2qQwx(i%LNU2a z+19X#r?hd}fj04up6!y}gQP>Qb|m>>?XXMs;CzS*yKPi8o-IFfcE?u}LZ}~&Vn35L zwZeieHay!lH3)PQPe&RJdxlNgA&4tJk-wQ0?ddu6uF~A8C*a?Lx75`b)?^lYl%LLr z;sMS1OrC||At8!g?T}8^UAn{CbC7B7;U%YDHD0H`Svk&1lSo1?@&c3D?Pqik7Fh8) zJuBW6H~9vEc65_g`;2a2$3s3TpBy)__Ed)iyK_Nd1-Z&ks5u`oo}_@xEkaZ)vaKVK;$aC# z=U#T{KX)9BzG5uKawXpWx!|9tdc1;0QIb%OFG%Y#fYXi6%c)4%sS}0H6MQ%dmRf*G(_+9SVYKs;^(p_F7 zrqq-j{ zLRQ6_AV|een@(}@bL4+XAJW~rrQRC1)JM{SIBJMIa^jlWO|y2#pMX^X^l+@wEyP$@ zpG<|YCpkpX*OB0tmns?@p#8#{`}vuAG>Z)yDLh$c^Ua-@YqvJ5S(~+`)A=n3jJ2bK z- zG0x10xqh6g^A>ux_sEY>ag=O-OaICcr8=tp>~)oPad!EhJEB-US0?-o$S;AQ*!5s{ zUBUo1c{WWT|7Nbl4FcLh1a&-of04};WOx%FBFNwDLfn;Ay^`sOYoR%5p@h|rUbx8@ zDeQ)Yzey&3dX${IIms{5NAi(lW#mr8z5piZoXIon;(^wr3?61l4L!&gX}A9B)o6^A zk^b5j9p5cY!?XMXD@MSBl=j{piO%}LtOE9@S?aR@hF>EnlO|PCU)t782T~GC3cL$5$!2WQ`bZu*2z6hAI4ah?;}CfXKaCC3>iDbq^`YmdK8+sP=<~WhqZBe{Vda2?I@%vgd^*qdL0` zcGNqBqaDFL*8La22#*Mhz#I`Z&~7;d?0DgoSqX zhcAFaOtUzKbYVs&F#dFI7)$I|wN~xUD#opvc zXqQ$%wv1ZXqRp0_H%a6!q)Y_8aYTq+@nMXsx{9sGFOT}U$I?M8y3W*vbvdOCt=J6p zuOIzHi}LqS+B2Nw9O;rB8tUh;iSVHUX#7MSF&#qWlPJ<7`{C3Mn`vUM6lpiBy4SkGGQM~&z3sGV``4RtPO+Kq> z(9H^5@Dus~=*46Ci@Xqv4cr4YFQ}L&yQx0zB1c8WXW}!KnTRIK%>6hHT-UFN^%Y?r zVQ%CB*XTgqrnVQYVpo0?l=)1gBsgajqL5)8QbXs=O%zm<#j)dEwqnDHy8>Lg*nv}$ z{X{zvH!O5lKEqFIW#AS^Dv3j2%q}AviydAiV8eejhj>!_Ej<#WP5wp-JzxRI;%MR_ z7vfK2m_xgX1HJs2Rz%vD$ls$MdR%C@LND6;raqcyA+rSqCj3o)s(NpFjHmsoLPB7l z<#WW9{4N&*G<1X2d^C0=sDW_|G}6QI zyYvtVCGsg%Ob1XK?F#G2 z7W_L$I}=u%DCN%E7Hh*NsoJ9(O>~?v6zEd+ z-a1}->S{1Ty|nUo;%~8foUu<~tQ`Hs-uP-BT1InzgA@AbDZ;q{s6e1%Pi6}b?8HT} ziEqj8uKOyCQZAOs(tkj&?VvhN>7NBO)SzGAkg;u8?Cu>Nbe#UK)*R8pL3ebJu`W+2 zaVU0kD>@2HJ;ePG_chL#TW?f&aEL{$fVJTTNj_mrbv`NH#7r*e-PG#Q(O1aTql1bVWKa=<71EJ`O3 zDL|*Bn&)H{*Ph#qar}~ubug_@L6T1ed=#-At9~sfzfezNhc}I=?alUvx*nSvM7Y3n zh_kyNFyNNhT17MGV8Oj8Ta4^y@lc)|i}aJB5;gm*Zbslq-hwSVN#|4AIkY0cRoWQ2 zui1ZIsYU-fK|#(LE%*`=-0eW?ph3r}3Yz)IxXaHH$`@;g)+sPV$RG`3YAfV0XDo4t zOQFHKA6ylhnWQ_qiaO#sJy3*G(Jn$1b@fNtd41~9b?-yLAM(2Ye=Vb&#YBYlF3;qX zk2*=ZOSeI(eTGoX&eqAX*bgb{CK^>K%T!A^k(f@oht#?_4tKHHKC|i z*y-xOtFid0jR}x@BA(<*ygJWV!9spJR^uVvU~{}B_vV*M ziQchQH5t7IIvTQIs;iMpbrqSTaw?14y+<_V;@0HV-mS;cCy__le*}&7X;x9^_}#Gt z|7@y!3VmG=`sC3%gaG{#f;)N*pUq^4sb~Ul1_6&$vorJKrhV2_v=<8O9iQOT%`I|4 zVFzsRkXE!Nn|%>y`6t5znMsM!#>QCjPW}<@y$eatGr-mew1pqyL{I(4+cAXx&qLUg zSD;5P8k8?Mhz~rYO)jS6;@LE5m9{P>nMJZyf8kg@bb&4xPax|aO66?r@@#f>Cg+MV zDJ6b}UAhl8IqKF!V*~sWxuA;PO~Dh)sF=jNVl{Voq+eIRKQupc3ZDA=3OUcjHb`gJ zLTjC}$L@S`YhEAG8fC{pn_Ut_ZtCoiWV8h*Yfj&z*MDC*u?-`KWB6*^qy(9Y^QbHN z1-%lLzLOj~dMK`MP>@7#bDl^P4_MIEekyuzM8xxjxs1?kXcrf=_be$t!^yl_clkG$ zS9CRx<_T@#PwwKQru*8$RFf{G${bwy(>9Z^Hl?*X zU&2q~o~pz5;_&dMuARTzm3&gSH*tbJ2zv_t2!3p=D9vscv|3kTwcqia`arAl8CKNn zsaG5F;sN<-S20Pq9Os!KjP+i-CV4l%!6u3+VwIEVIdn4*Bk8=L0~~r)x59>2)2xJ+ z3ThGavF)4ejK!bwzhM;Mwjh zviowHjuso(LqYNQcl^P)5pSf$3l3CqL%k1^umq|sMO8;bX=tP>;wHlWX6}x^8vj+h zI;A$~ztj4WT5}MiA)UbQxH~IeP=mYU;XFyJk$o1RPF8xSLb#fF?cc5KE!+lE5cXkf7nk{N}FiDC9pX zC+TnNI{0<>Q4N}yIZB#6nTsnv%ioF&+fXu@#H3*e?}*xIzy?8{L#lctcl)WZ1P}9L z>znN@C2`MaFh`!CWo*($_9FKb++2R9&ynA)Do4?qQkjF^ zjYclrVJG^!H~S4c%R?uzT>T%~-v)K5%3)K}`6bD)JVxb4qRD z8Hx>9+WTZ8#!S+?Wc=NJk{;4VUQn|>{7cDpXtur9i$jKvz-Y7nN}o*$T6n@IYlls? z^u1~;Rv#C`lCT==BF!QPnu7+vO%C)w?-=%UU-ZDZ{LDe0h5x8@cSE9|P0iS9b~9U} znOe1`w?;d(oJYn_vNM}5fK^lQtiH##wFbgdk(0DKN!ai#T8u6vkm73f5sXRE-R8ST zPf1WENzupM#M1Mu6lN!Gjt?IN0fWaB)$OiQuq?B#DoGXuxgpt2gtFNNtIdF0LXsiX z^j3Q7#5 zQl6#No)mX`CZ9t;k_sOtjtb^w;&o#mmMnd=vIh6`OX+4qHYoN*OmGiAQmfr7%Cs~2 z>+$(ihf)BmV{e4o_fY45DSs<^KRTjGyxQG6K1*+XXjVLh(scDMMa47ujGDXkzKfOG zah2xK%Sizxd@i2E1mPi%eO8zGY2@mMRy75UZI9BRhsdfH4%4b9OmRUqd@QI4xl@_^^p%;V>Hxf4MSC5nl8pRQ$>gn9d%YqBCHA4 z(b?#Eqz{DogylTrNqQeRq(BOQ0e5qzhHGyT&!(D`R=MKIu?G!y zc(PadPK31u3!Y#<=5UH=?mux@wJN-uB@p^(UvT9^@NJ{zr)#R=OQ<<7#0~dO_96@2 zp;*_xjCdk8IEz~Jqj68}&BdI=N4hKKMPp_#gv>`WOWAvWxS(mYhwXtq>XDU4>xshw znM7L8bNjsrB&%SEFBKqCe%~5-90Sgm{<0PwvKTFDC3;sNwIp z!Dfe96|Wf&+VA)z_ky5nm<_6_VcDWyf6)mUB@%t_oq9Dz{uW+{zq+5!`YF#IUcWPb z7R~V^-SD@-jvtLJy(gKh$^W`_=?l91N;DfY%P&EZ8vJbB_lWRslTRq%DY&|aMYoih z!(;uazV6BMz5e=D5 zf~sLgc%-{97OH$R&Gm|bHe*61!e)7)9V-ZpMAu*!D7d05Y$h6h@4*OCfZ zP@*O;U2{gewYt-cub2AARE3s6T zH6&Gmz z&H`n#c6^dntJ#Rlkw*|E)Z7iz-Zd<5w`4bH)U$(dXwc-nk8Vc%+XVi-sz^VQ1wTxU zxH<*Ry5W=9g57<$de_CyqXjq`%p-8C+?_C!D5KpSPZuO@dH6uATXR~1kegxjCoukf zVv?@O9Fo|q_vY7!uMeJzO7uGyRPetF|4*YgM!Q(}**7cE-&Ct{XqO(=72idW-_V3! z#fmR5p_?$dSSxyhhq_eum2f@{dBCKqUAm>-cZ?Q%3MM>DALb;_)-E1qO@3~^J@|fB zR%SUBCYz}Jd)ZrbA+%0*%9Z>kEoPTKN16F$p&1K(RI2*w8+LiAx2Uu8K@`#r7qsHV zKAA1z71BJ17sso#8537$6;k)2^sy}sSErFULyD*3-9Cq|`2{To$=NSZNR`0ok!-#H z7@&AF_a=w`tGKsmmxHyesdv*Pg>}#)A3#{d%fxf=+6I>N>Wsr0wSmN`_+{!R`5#Eo z{ZhB)Qhg3jsF^ic8xcA!>ZfUgJq^0HF`X76?A{F47&kuI6pnAiU`$ipvs>Q6=^&9wusS-Izr@^;k~#_N0)7ER zIvV~dUCk~5lS%p1ZEn$)2hkQ1>l8AGvp`Y^aw2V>{%MbAp2e0b!uHJPSQ;J`cKfh$6awN)O zp*+sS2XbQR;aJ3q9#$h4hMUZ2!{4kA!O#WbHF-1;b1((fUc^qGU{-lew|MSYTdv*o0lSZ# zTk~}vW}c(eVo!Y}bL2~i-qaUq-!}Hfa{ly!GRGH$d|IV_V9aNP_1Esr(#azLME+Ky zDSn#GA=+XIBjSO-2)b+$8@Cav@~kT!(!8It$`!e{Y7Qyo{~ zoaD2ld{8>|T8}gVRHr?|&ah)1bi* zW0bg|t9&_nj(7Aogk5Bzw0}PQd!;q5$t3;Q zFl+F z6sm;Qy$@)jU*f~~EY00DKbA{HgnXJ8oE|Z>S%xZhtC=PAG*g1PP(>x)(}qX;Do*>a2ryMV7k2q0k(uRFV6tfV z8OuROinWo8d6Q1|x8g^`L=LY&XS4$aI5alUjaubAk=^H>b_TtR+$+pUB7H_pZm7u@ z2(ok|grWsXQof?4*}+5Vw@KMlDbd#72X}Q~g$*xwalR05=y2X1qJxl!2=2p2`&rI> z#+;9v&BF0I@1?;W@NX#BP5`{Z(9!y<*Kq0qRK$9S`TZ0w)J{~$oU&=yquR5_*Y_EQ;m z5nerHmU&#JmrshHEG@Rd`iGe# zhaU9$tA^S=)UU6K@?c|ID#B+W`2tgDKSUjg)@ashSJBUaMZr&nhf(O(__?z+U5d+b zdm!DFkv7WYU4AaA($hqPM`KT>K!rKD?tJXOWU>C!)3;rJiRXT+j{3b(E|Soeo<)OM z{#M3qmqnZlk0cWZm++JHo}T(O3i5;ylk8)UCozc*ho35dM3JA1N^nmWsLJ0$lhj+z zkK~pV`Q`ApRWrd#zkmrtirt)*Ma<$7`f>O+iwt?e&tO+K^&W#4YoYadl|D%~lkNkk zLjlt+8_s4Q*GV!#S~D5NQq&aq7SQX#aZxdXDG_RNvAcg&VuJ~;AUr8Aikmx!9o(Y# z&UzoM1JWcAk47B0m`Blz4;jp{!W8M^7pX!Mm+CE*=C`v9HHLp{HAbub)cZdzPoXt^ z-`SHzd^Gn|-XJWYBA*KQxDhm%O5PrcWNAZW@E=K1>TcV^`1V3PoZ0~ z$S3J1vV93seC&LmxTb6H$HM=*4f6hD_iA5?tC%9*nkRYqm8DynL+C%1eue2ctTDpW zPvH;>CYa1UyB*%w=GU7_iF(PosDb4Zd>r@Q2)!D^8YN1eHBLBV{7@!7F!G5y%*5d$ zwq0N?WXY)Z6{M3+6X>UWf^$kPi?h+^*88POPH{^0p-PXrYG2$3 zzw{Q`@MQNw>Fg|uSL<`mOtcs2Zhwn-O%gwa<@hK_e%pswyeB0_IQIWlqDpsj(P+aB zzgHQz{w>jGJuGt`K?BNE# zWlXjqwZ98HYWf^(uvoR_79@eNdsFtrJ_aX^P%tMB=&Ri?rijlLfwrrg7PDzdo)z5z3eX(TY#7I({@K>c)`_7|zZ5aJNYlFWx(o7Sx zoc|wLZ?Ys;lBDZ>znY!F-6H^GR<(3ru1LNEHxw=?!ZqLj3GQuHS7iZ+2p`U1`g38& z16gDUBr^pG#Ncj6P4%DD0=+jL&0Esa%g$6ah3`9=?D$lb;?{WYb3o-SJ%;|d`%Cdj zEa@+W66|(;if#HO`L+67D9G~OHEPLi&!2Z9REy@yTsei0+Y?{phjh&j+yhkkw?cHu zawCu1);Ry7e!d-4rqrseLR8RTcGtm{a+jWd8#i_1gabx1BH2rF4_zZqNww~Zza{6; zBmF-mh&2cl=}=&AQZc*S9p43T&*Jhb^~|_;_4~zxj=EKGv8w1{-8+Z;M)qs*N76;> zorkG`WFgMoaeQJ)Oz6`)9}D z5b$XJ=b{?S1?(z}u1QN;9A7=8W*0fo6g-Zg0bQFcpW+zd+o8$vZP7e&8Ytl@C{~Ie z?MSWt6%XnqxVjVcRb01J-cd7A_&)F4XmpO*i|Ms6V)-33o3YW;Jvr% z3%-V4?4T$8)5InHZzAv|4#A8qDF$Z`a9-4j&yFvI$UYg#G1`N)1!wpmzsNHZJctkS z8$L@5D%OJDl1Jk;w8XDdMYl>PHDvLl2@z{;Gm43?dce(e7KpoU%scY)|ezAXxI zstlF6*E}Qc@lKh%tqdD%iZR<-vQWOH25*5M(xa&LBn2}c&xgDH3-$CO+S5pQ zB-|a}s$?d46@xA3tmty3T{$}zSgl+5#U6-v>6TiPw6`A3=wslD{(_-Bv9-xX{`cyl zZX-id!{r&=5LWj*)r`A+l3IsF!hs9=5H;RC#*cJ|iR$ zr=pL&hn~C3E0!X9%$D(?z1B57>A0ROBAG+eh+z(o>Z-NF9p0sK9kP-f0neP0&=Xgq zbF#i1s{I9O&*EC9-xVnbvK01%8T_!^4IXoll4pDt=Uh% zQ5Pz8oT|iUgoz;*v)dQ3iK4zZ!|F4H;=_3sRqnwxJq%b+9;U$0L+W^ZesR22SZm5V zbLf_Rs%3-jET2N(SDJO-y)^F)+waS#al=L}ZN*PI+4*jDmW0 z_T5R<3ffyB;!CRZ=y2HNRh&%8oXlcxwH5P`EM|QuLm((H#R0?yUT&9FXj@>C}Q|BjVPd(yXrduGnC(Dh#Mu*=-bTABur- z^bRGQcWH7N-L4UXBgwn6#Ie7m^^vGkVMaP+)Em0O+it8Ow1^Rm?#Z6sTi$nFM!nRC z@Pt}0m|byGmtm>NGp@vkpRDI$0kY5-$B3tH>a??|NGbj3rjc+;OZp{@;v+rFuMyw# zfLJ@Ag}6Oa`3eg8OJxi_65&6VgYG=emfe~`-RW?dw8OPgrD1S|mqMv5>8X2>$*y=00o7cR6oYBXL}BleVM*Us zEzOrvm)e7V5v=Lk#$%Xrqo~viWq|_2>IuBuohz>lMT9xciFv5Q{|RUBkRk6 zju?IeQ;97ZgUq7>@w{JdJ1=8*xVt^25J#wKI(y;Cqs|5T%cS~rTikc|A~z1bVfDKE zp9^cFbt6-_liv}?!Pt{OR>tBGn~C6V#@-!@t>291BwxE2<`?vJ{C7EoTrT?msR!gB z&(cOLc2VUcWD)7Lr+g@p9??@J;x*f*c8#)$OY+sZs4a>OHd+-0*_xY|qKDYUY$tkz zPl-_&f=MwH-*@RL(&)T+l}>|)pcW(w+Oax$Mt*U8H)piQp)~Krz~@NP1K&R5qxuW+ zsitbU12~(J9O9$t+LR^>19~(biLELR1>?u=e|+e2qKF!Lsmz5b1U^^xK6CC^`(}Z$&@xn(8B=A$_12K}PU}^1uGy|3}9mTKNSx z!F{n}@3hF{K(K}uu}GjreF+&IP3friTq)7p1!ie;7Y%E!8iPTu(r{$092k8;Ep$T- zD!L!eECf(GuZt{=74@x? zhgOLpVk#OcV_|D7J3jL5CGk=jf{!h~f3QIbE>%5UV$bIrFN`dmDuOZ8Pz{9~!PH%N z?r_QRS8C|DSwkF6TZf?Ls>`6UfLL}xhr5nLQ-TsIfvu2El_3}lb2)&9hTa-7{b=5k zmyqPIRi<} zWL0aGp?i2@O25@|4_7l-3x34|s@6kl@+LLdOqE~Fh-V<2irc~a%H|Xn%p+J0j~(KY zOio8Ax9-HW!tz(r$Y2p@lRn! zlGx_D>vdqaN3o(BD&o<4iRIr;y0^?5YhzD7HolG&v6vxr=`5IB#eMf54_dlC!OWrq z#{BDvJ-KfDKDeK=WBTFvS!73xMK(12QuIg{sp#MtLn^Z5*WyX>mR+C182K$cb~4AF zL>zHZR(I)i4x~6!T;uRoTdYcm=Io;NkMJo~`=T>g#3nm6nS@n`{_E2uS@5e5L}0=p zt+M56?RXdN2L+{k2JlHoVqG3E=8<`1w4h%9W`Le8MmO)iM7(v4R~HY0o)|=$8f>lS zz#Q0&k5;mSi#GJY{ul@`SCwE0)dunPg&CYOo{BD$mn)g1c{GYVhE^Y;qopt0 z3dZ2tIE3t$q;q8qw$!P3439+#d-5=2P=bQ0xVBi55OZh<9_cFHcD8mD*La@M)tY~)@Hwm@L`eYh-<}@v2rw_p6MGut-E~* zEMEf9AKM`toWhq-5%0kq9>gQL@4RpBhS<{MMn&{&<76z%!6;SfRC}W>;Yr#-;HJ3rn8VW`E$CYhk-|`RQ>#T31uRBvH<)yJS z#$b0|?<9H|TtrLXH@=MmU0c7A4+;NuXG;x*FX6Yxk^U+%6#X_jP;Xk7>rTr)18M2+qurhaM*p$)(hqAdR{}p$w_|so*28PAYr<0m z>uw~mFGUf^^~hu8IaT3~tsgStUYxRnQV>0S@mb^gzcwAR-S(&7%sf_xfLCHwet`<3 z`}mnXL%VWJ#Z4YXF+2K*Fn+N<9REzu`t~^PqsN;6 zqs7-*r^+a<>FZ9FOV=HApGfp8%q`V8;;d{v8rLmvH=SqA^Ff&}**V0H$&uDv`Bo40 zcG3ef^0n}#^ef9dF2rIU=ANv(H)|~H>BsJU*MD4m32&-L{Qu`T^?5Ve2blf~6f`Sw z3}t*aAIE3t-Bs7R?0;HiTgWfgDm6IypsgzGsCK$Ed`cH-gB>RfR-n5))Be3M^ikC^ zZaO-=v{sQlWD%jBJd?kNCb7d={C(FWnG1p5(vL=Ke#7f0qy#qW&YqkqzYltAK0Vt7 zKNGf-ex5j2jZgc~qj2B+;~CRy)0%%SM||8|lpGAqRCwRqk|5s}mKCMf^?1p$9oB{o z9*xmVMW^bma@~I=O7N`50q4RFb8rjZR(DOuo?HeE;hT||3UjzT-rb}lkMa4N0U>`&KX6K$s17-iH*_+)wc}!Lp_4t^X+A<%B9$iy z>wetav6&s+?T#8W*i$`ydJU`K6D;cMAtp9$*EAxy?JZwjZE71>lF); z8JEG2t^NH&rz!q=%9h;MC%P@7Fjh=_nRFi95}_#kKKdUIKQ_Agag5__0`9v$c1{K4 zK};c!iV?}L1xt_gX1}k0x~!ljKUR0xx&JDm)i~LoAVq(J58F>rWLsnD5Vxlwq(=4U z&XW9f7=9fN|Ld>MBcvz7kxxB!{L3Hs{B}Ke)dO0k4|#ye?n`)2cHaehrn^#8XcG@S zS~syeR+!XYEmQJ>77=x+n{;Yc|!3G@_ev5 zm2RQzkpWN0=hq4Q^I6kD2p(MOr|_e%VCY$$MZf+^|6~=viW~7R0-vlu@CzE!&7AOd zFfe*)@xA1nakanoSPK&{O6}8T2utSMoy|Ir3VBaoA70k@{gruUkuQA#u|3Md|7|?h z|8)TK*ly*|;ArUdOCt}er0bY{Z=8ynZEFZH4Zkg3b`4eM%2*u1ZKH&y!2!!gYdjY3 zjS?(7^PmDd-cwUS`1UyTRmZ@IrlHR|)aVh*l<-)%J$(wNNpsOOux7q5zAwxJ&33vq zvWGDxw}*OpOBkeEBa;ZfR-+YU8$H`-b?09^3#Hhs zf9y;ti%W4@%`SM7QXOaR)-lR+^Qwps^T~FrMt93RW}$SJet&Thkg)m_6{p7&RGm-mi~I? z-i~ZpkSkRTp&GXPy0TbOp(=*LwjHo@R6^{#-)}U>@2{LDKenT4X0kW-#uz#k z7E!w82ECrB@;~125kNy)C74D{qfUdd19~j%aTbDF$)rd{nhKXebK%~2TXd@aZD39Q zzJhfbeBVEf#B>qcSQvuG=e9R{lrF(XvY7#2!Vquise4X^QN@84yP(BhTRgJ%B6Q>q zRD&EoR)Cf#sMEd3FHchCn`xrJnVgAtv^`IK9)UoIocFV$_Qzm@?17PZCN^9TabsLf z7X`e-8M`NK7+ddPyGbHPm<2`M52#PwnJMAJwlA%QWJjCb-@>u@B?!0$z$vr_b74+pLY-Mz^R&XsXDJRbQ%qtgPv;gYHR$BQ)c4 zu%;hPH9U$z+U*fx0zJ@s<0U*OY9QAxaDGD1M|6AFyQ<;Yo*Z`>+{hE!VRCFvDNmA} zyUOyX44`=O!T%ovYFdk;Sv%UTcbvx#H*NT$u2YBye&8=+OZKPWzxy@|vEnKZblP7{ z?)x=;JO(PtPSi(uL_g5oz7)F2xQYsUsuusZp$FjTzu#=@_gR<0Ydhfhb@$&s*t_60 z7>Zt}?nY!pWjrqZmIybyrN7_lZ`i>qB*3W>FqhB^%}yAq&J$DNI{M?vy8GkC>%`9o zV`WS4J5%v(VQU(z4fmDvA6t)dVG2w9T(}3{ z#;#%W^;xcp6yjWYt-7ZrYSC1g3S-b=-V%&*3GL`=f|OOIyM0!CBujE_N~_V=6W3!d z=vKIXwuWaEOxmM$b{jzGA@yMXRlX}8xY%b2cXj*|jQA{F?lSvl*@K3*L!=!K9U1GJ zwH>RJ4!!aYKOn8`PiYp9)E!1?BwxcJS(#P~m&35z7n8=FJc~W`voVIhgnPq2J`?Lo z@NJ-=d-~QGi^k|2pN7Jg`Te1DccK|L0@cc%PT^4f^#7PD(*FDXxQV)2?K3+nleKvs z6s0qpz1ZkHvh4X-b(;KG*yO*z>oQWsF!=k%A6NDT;jG8(df&M3x{R!id*jRK`xdV! zm+&9&C^QX6pBLiZ{MdYbAaVI7;2i$P%46rgGX%fQ@QfoU!C&U%ow^jNG>p9$@*uUo zY*xrxiI2vfzI2y1{i^XGR+QajalGC2;69t(&p_fHmwir!8SuNA#b8(B-5IDU-qGs( zN(bC^DE2?m)MrM@_A6cie=-Kp*d|QprL7Gh@hRY5wEt{W;C8o38<49@OXMmj1Ca%HOZFEsJofHSUjRY)Ol6r&RLu zL2K+g3bu_K`j2ml@ZP*N-Bty@ZFFNr@9u{nU~T#S@Y_}M#55{RCb#B2*>@Im8nvfe z+No02Et9YHD6;*Q*iu4W?lu>9Bj^Rc)1~pCt_qC*+!^88j%n`+SkzxQLi}0*cc>3u zEaOE?Sg1N*IJU(0_F{)C`pD-Y;Ma$qAxwuB>a*W^R<*7p>(}LH?46%C$ge@ie@6fP zQhW~5WE^L-;%91Yypu2Fi}Y~$-aN?jA?na#*5;eFG=$U4g-fN%b3x|INbB}JgN}~4 z7p?xxtm%K;^=0&u8pBRf{<-jV>Nr9t78wgZal0QQ!?L)SuJh!NRbNI<)%W$l+6r$)fA5`zDdfD^u?*|(9+vO>lk|H|xs|co%^Q5`j_%zGW#m*23uoXokKMQZh^%fIFC%r-Ao{AD2l_(a!-&Y2027rpj|F`O zQS*{o)R&K;C;i+7ElnEjfA71B#T5$bDQW;2Z=ZMH6I(mZ(=&$alm9seuFnbW`H}hS z0)<14=V!C2Cv4IGXFRtDUhTp8s(9GvBPZSUh|AWsiLR=HQ&oi>HuLwzx79cG-ipPrxS19Tq$+|t|bzHa$EFcgi2+K+zb+rpkY)%bGC_r=RfEnwD- z``v9n*7YS}`i!2M7_I=J;|Z!neC`Tm0pVBCKTFaPc2+ zI8mj_nW{mcanL+)DuA!M-yht!<4JY;#|`NiAtf?d(l3ERtGPGd_x$HO!=O^UZ9Wa! zorG);ifi*flCOn1NSH**Lj%z#Fht)`>@#fW`g|9=J!=sn0^M}O#ituA)svZir)#>tW=CS7e zp=FG(7mbB47lu$Rd42Hxjb&lqJ={eXzGsK`!Z|hC#nGAYw=mMv zJaQzxbnu;L2;zZIJhwdb5RMS>=R5AOP#$HnuVJcE&wD)*U(JsD`koTn%s*Fr1chle zi}@z~w0C<}tk10P5=^O|d)%Sn-v=h?%cQw_Ya}|4s?ooktMt}blVw+t$D(iJ@s0iM z2t7L-_n&!PZRfTh&WQWs*HM=_4j#gaU>XSJKi}CmSF!F2k?Bg_A6g=Qe|4?IMkY?y z9R=s1Kc$N5udlqlBWWB6v8PTarhy{2>}3N1!-L za(}!ZsWA-hf|N1PpdnBYFcrT9NVjA+Z(C3BQWJkmYVaQ;&3fCpKUL8oN>tzFH4Z&O zUj_RmZPefztPtU$>4$tlJ*&9F`rHrQMK$e-RK=6$(bI79966tqqCN$gznZWq`~o%9 zNJ+QGmi+uooau~Yvry*%wq#8$W-)t3){-8=LbqL6eCZ6D`;uP_n>h!ww8XKiJYmq0 zK{d*A(T}7C=O>|YZ~A&7&D#8NXBy(`%$5%MvGDVXJ0=-yskx$aFRSiG;jOW(>`yMa z6ox^KnCByYyi|Sp;*Sq6D|Mk3)?=H9hQfx2fo=C~QLP}o%or~#_h-Vog7b+?hC!Dp zrok2eST%+(BR>{eG7mhSQ%Dp#skJo}0B;*ppwPK82Sv_g3{7z;RYn1P-w1k;j+Z1m z%?Yh^S<$b>BK-`6s!Dp0Ql8+HG|?1Ok^wN_DV6Ys*2u4pVs3|+-|&=~>uCRfaf?sT zp*$_c?N8@P#{}oEp}VQ_q;4V7J@ZoZA?Ojle~KjSnOa5-{bTRNFaF1dVae#7V~-_X z3%71^w{8CG!Cd@0Fog`m!B%-Z;=4VCJ~sb0uyL#*Y4)jTE;Rf5gL2kxP6I>wZ|@xd z10%8b-E?TI4zx5+CBNU<8^eImwsw$RMHpRXxAdMdPi{M>iCg;dAg$93;oCiz$FWB^ zo3QVTPLr2*yz#@pzJvJsqG{AzP_U<4(=fOw@4FJ6tLnsm{yYGiDEcy!=(j2V>#BKR zw|<+T=k3B2df$(iNb&&Iu~_wE^L;zkq;+Mzq%Wg}aKLVFdGNU*YIszQP>z}Pxl6pD zhHlCpsgN$S9p-E3?wEI@QwI2}8_!`n`Y zzA}>?me%v44i5iyQcG^T9?dP;cG~tVrOC3w$@|e9IPH6j%Yq zq1u$vCh$c zN^E)3tG)AktyJk|pTWo0p&o_p;pu|JL015 z=40bj_{^`z{8-uAu`2iIVe&XY!25o%BmJfBuD(H3u~T!Ciu?GP3CO!VB|RfJa8iof zME;vpU{ze>5N{oa^a+j@e0j2m&*qZ8QCD!aj+s{nv5ovCEVD?fbWf#8^rJ)6#{v3; zGUy&gJ%PAS)Z*8Fd_vxKb~Rid2BmwWnXe;{q~}iWuqRvcrBLNKmWa@U+Ea4zW^JZe zLonc_6+6C#-sVMPzw>%V6H5ymtB{W#?fbDs=hz`@!@sTmsc+b^r~tQ}my*7>BdGK7EL{c$6^oN)KYjXjB? zm#HgTgm^Sx7jKOcIt{!IoC>=-M@bQLH>B8GqxrdMjwhD*eAF{}Je}4>l%R(HQhOM9 zrEmY5ocnQMX-dC`U(6#6w!B2CwyYpG9Rh@Y3HB-SDeiDzqWKM^c;gjkZTFctqlSEu=X(#xRV&${oJklZ)2Jes%kV^a=C+H}Fc87I>vHzGQ3^w0Go@%A z{Bh;6VE6bCmfhEdY0zAC8j$>U_p-9@M<257Y>iTxM=Y|Fb@qMp^>K78d(&$@ENl0j zOvUI-y;R$T{Bu!$KC^j%N&V87j^{qa&^M^ywC2bQHSY32&+xz|#f&a?l7*O3Y0)*0 z(uS%%o27gfBnCX&-Q;suUK}6CGD~0N0X8_ptxH|)7x^UZ=%=}wSM;#Y@|m)TO(@jt zJ&$g&->3O`;Xao-^X%c~LYi*_HDs8of%U0kJI*LH^R}|tL*o?w*qMTl-H#nXZ<0P% z?-?&6FXOQgwf7TKL5}fsL?^7tWtPciGULmOm(7m{yVx6Lz^Ina@_BDwSMU4hq6*at z!B7e5av|&=KfNxf^-G?XluGN+ymUGP?r6SXoCFL-r;(69R!!CW?%SQ^DK&;qSV2?O zxpJzKS}Z@pyjWdYB_TBjnP(17E+ua&#^U|sQWNjL&YH^hiwbyz4z8#nX^3R7q9$t6VqL-OkrSN0% z$HyUlZ%GYjDuS(HG>$r-?B4h{rx_|X%TDh3@xk8M_Bfq2kG|gg-tqK6(oo$~hdXt6 zKF7B&I-N9*>QQ>v2Q@Sg%mW{HfKsWI^TggL1w~I1&{zN!X|B4AIt{APO|bk}ecNcw zOVhP!u59@X*a1nQ9WFdK?YyQ_?9qlaAP<-DhL%%~2 z2fTFOH6r!{- z*{%6wQ(u!IQ$o{31nZK{E~-`?P@!dhyhrzPdH44R^Gxj{Ug$JY2Zq8>A#@rJF+z9E zcZXW@uB zv>3!3`4Vv>?mkHJ!y!l@h)7uTlU?m_n{ryIXc)3uEdgUt> zU|V@S6a^J>R~fcK5ZooQg(QK9W*#`#PgRH~PSePf#S@`bINt#{r&I@+%Di^j^< zC`IGokGt;=)|CkMMoUho_{(dYN5>fl`?3kvQ^mIhIq@;I{j3H%Y#4T-W1KbYm#- z?L?K|R<=aa5S$05qSwO9&?U0klU@RKq(!QsyVc~E&_+G7wkEM+D=$`m{@%rJC`Gkz zK@$i$9?>BNSM!=Qc@5^&YKOdA4s3cfLyt{c`Yies05;MYTf{<+khoY0lYDwcCWm~I z(pvFJjD1?^CtW@lt@a$e6us|!Z~klXN&aiqo@#+On2ql0=9%Nb(Msxd#Kh}lS@)XX zuLt{<4TbL;kIlDoBqFUl_rkh@^Ud*kVIF#`%4*fn@0X)04uzg7RheW|DB z7i`}ie%x7CKy^HbxyuW;tCv+}z^QTax)PxXmxA!e9ZYMKs!Z0Mp+p^hyRqCmsOI|( zh3ddK^d%9#t@(ae2~MMaJJZr@>LQAG>}=_E_dlMwUq=;(wdLD_VfW!ZDZxwe*Fo>; zQF0-TrAy^fxTYY#RtkDg-5o_1l`(vh2YJHPN?cIj7wH~uct~;N3OP{UOgE)O?owFi z^d6dg=mPd`B}E%-spZ%xlqWRlAe_fwrM>nNicAlGJD`LrkMgXXiK|Gvz`u-N;l z_{e6Kh|#TS2qe~Y!9VXhkBZ2j?`*sN@>SEo-u&^wvVwiT3wfGoO{bZmV0&Hi<2}w7 zYVq~%ACv7M!t z=w`3GA5n(Eq7(ejp5YVu6E?TC+rKmv&XwQC&dmA}vOI$2f1CR5w;*$(Z;NFlM=_AI zdS;SHV!@lbH-;JZpb@qSS2LzktYQJ-b857fa1c!~X5PYbJmjA2u$b%~z#YL$6(YkU zOvD`_&(?wstK!2RpImjp-;THrpXh$eXZ(gd;BUwJH3D7&%M(wbu(}|^cF$PXkJYPr z9kr#8^oyO>kpj0=vv2FO$NKEaK0z{53pKbsaAI*?Y}it#!L6|*PJi5WIf-B#`w{eg z(J;CeZZ~W{?!Mm?K`E>&^I1{6Z~68kzWuIR-1(eRyl+R(H%z3w+$j)2l(<~HZ5?6v zvOEL8#5iaiu-TKxp1b1f38SehE=&c5m)+Nub>p8cf0xFpS^oGGZhU%m^!ybXN^c>A zj;)`E@vz6c8Kgn^dy$`-hL^y2Nv5bhe%WUn?yZkENc_^2M(2c zCh8`JdF*65n zhZxAr$}XKxM;jeIhV&X*==~S^^{KlvIYitEfSVS;WoCYw}kaVZp%k4z}o%b14SP%R)<7j{VQzf%H z#~EHkVp|a-YJ~+10{|G9g3Oo(j-&QXdBl4e<^9~CLX5YRc1`ue` zhk+^;U_%q+lB)jsfD!dTh2FrwuKv-m34(@jf=4)gI)d>w@W+TmJRKuIpokSXiq_Na zW)2pqkqnU7caEEZh?s%WH94BWO`5$Kx!4EFi!(rgXJDm0g%|MRz8W&?WGG_W02V69 z7N~hQ9!?jw+Vk(ec%*8%^I#@+HYyJcie!Zb=t>Z}wy9st{l&v55;l_$hGaPv>hCxwnN8GP8>&nJRrfVKR;}oB``)^F4#BagBAh!x`b&`t=+8@ zU<_Igm@y5Q#apU&&d7=1Z-8&w?+|MdWgZO8e+c|dXO83N1@ z0`CKZHMK+0N7OEVRm0ns`1_!SFjBzqB}e;H)?j+IU_<~o$R@SO!n5je=qMZ`G2j}JA)uvoa+Q75?!|qLm<@x$MHfbENt`{@ z*##4#aWp{Pe0Cae4KVt@%w7iVNqd2arEmyJ5o?CUbA=~>xK5l#Y*~*_c-6Ahrj{Y# zE;(;EyIcXN7T#~1W-U(;{&+_mIbCeLdX+CvyWSb1te{9aOKQ|j^cfk#GVz6Df)8Y*!%g_&v)tL(OEAc;galSYKe!CuoX>_G4!tg zS@@v-9(X`)5#eByB)QY&YsL<8WTXQ8g66;|D8gY5AI?UynL!dXG?O$kNde6Ak-7uP zN%%nyk`sVTEM&_>T*bq61(u5Gq%zJCEmAf$%9mi&tdW$X%LDSL-aR#z+Ko1w7l%^w!IJ+?IPBlDPxl|Dd)M1=12T}s-L+O!pol|+X2yVILOa2Q zKpygYKmsPQE`2)+Oy>g?;{4v1h8wr2>vy8{IIz8g60sMOjR_rq&QBA zw*h-WH0(u29o5D_GlWs{wiM#!0;7%tUh#PNx?H z0JCGSS_^9D1EIB_K#DkAx5&|YBol$7>hti=6>@vIV2(%u+4Xr4OVd69a(6PJ3l;rKE?QJ7u(_Ehqc(M5k zQOBWe?Hj|C&?K)RC=)R#w)uC2T*r=5x+Rkl%Zlot;eKI?sKONW^8qJleSxUvt|%1O z<4~@g6UT@-FiW8&5;!TeFtetqOl{5y-Q#l(ilTthbI!OZY5>i-Q@des1n8H+gvk|% zVsOyOiZ(Wx0W(0?B<_aL1R0I`cP@s_Fc??q%I4whSK|ha5TPXUbgTy~gES=Q0W0tU zN-_~kzXodGX(PtSDfKJiXzH>=&?v&AFtc?pplSJ`z3Ov=3-e(23u8d&Yy}1*sb>6Y zy@LI*G)5>uifsXGf6q7^W?HMieQMk7eq$U_g+24-$~oz|Av@<8Ev(@6o2+fMrJOO0 zki&@MKy}X<0MmfUv@ZP|EE$0IogwObZRJ%EBQ6(S25al;CFA9yqaql#qNDP4Vud15U5{Pcs$-^^{kWjzr9T}@ z#k1-q;zoW-6T9{fLIg%r6?Y*wk*T9N7exYEX|uYI^+Apzz$6VG$a0X2q2-5HAivo&%K(DYtp1~-KrC@yPv;3f?vqbUST#uOM0W2e26!dlVx*^i2A=p=WLW_Y=D z`g}E52$C+|Ut22@s8RrGTjZ57 z*>v&c$!XTHH+z1-m8*@jYSpnp;^oTNOew@+0Ht-K9FmsPYDTVQ0m%CW)k$LA5fgvB zv`G$VVGCw2Q#T1L>WgY{W?GOw~;!ZakqbmRaAOJ~3K~xBux-MBW>`7ff2TU^I zPK-fU`wcRhV3hUsVv9a+Cf=y>7qChZTo?{DuM!WiT`bhpiCkpjXt1W~00IOkS|1(> z82lT#f^TAW+^Mx?aRvMs7!0n?8ES>t>=&;Ao8#@kHQ=DC@H&JkG@y^c`lDZ^&jVvn z2B&0ye?t{gXxkxw-!PD4;_JO{<*OrS_6lg!xu89RBm;*T*kFOf0J*QLl1(M`rd0Vk>*w z$VEEfP@#knRS&Ak;!XZ9Qw=t|0>m!s;N$>M34)KV*}|kI7VRcUf(RBWwSZP35<4}z zM%iMhCU-da#FqwN6KPN(SH}h<>1w(OLpX&!Em|Esr1pz_KsUBTp-vSnVsU&{-Gytw zt(hOUzD)|6$W?-n$SG(k&0Cw_$A*dYlJdUB;eGUG>M|x?62~YAfqE8Rl77C6z&vvv zksV%9vOhn0y)&4$P1_29!=!yxK9~mWOXEQ>1j$e`X*zb8b?$>AjT|qqpyNQv5^!t? z&2(NMl@G?q+Z`cbU6l*xnbX9kK#al08_qlxK?sep=~@<~fJ}$LMsz={t34+-ERI zle>%u@g5i;6Ek=~g#RT-=2TJ(u#`yxjG|zMLR|!qhw+dG!NHwy@L6;&sPwGI+BPsWDj&#I`9Ll3s#=7m z*m|adA7f9Bydz@MH8ked-K&avvv=#lVyd}KtC@zu% zQA0!9wO2gyi2*eVeAiq-`tp8?Au2CcqU`Qw7-p3Nz8aed?8O1-AVx=kp2jEG6!+*0 zWg1W#)4oUcSU{yv$q0l~duZfFU8E!?*e^qs-$P>y`bf**Q*3}QfgP3|kQ%RHXh954 zZ)VqEl1goC4Ml?kSHo)E*5}uO>bNJgMBqS3B=IbriuCUt0cxbm1|YqT*bCgW6b!+i zJNAt2fV^A2d_3_C5KxNhsWo-!xqxiy0E`p%Ge-4tZ8mC52~2ELJjocf@7T9~(Wx#5 zRPQ^05NLrhs#eAU5XVuaaGWs=l0f7A#wPjNHmLG;vuWt4TL=t^07ZZ9I;!1Ah)jkM zkbp|b^;xhN+y}IoH9L-~z4FJT_d|PWw8tK~_yjHq(Qsw=`+v`AWVhetC3~Ino2=du+nD)lt>jpA>hnnGxBm-u+$R^g_fl+iS3R#Mf zz-C4&@emJa1UGrh+gZVOUnTigI?Lc&Qg!7s_q+7Hki7vjF%w(if9Do=)&V*Jwf5a= zH2lhHL)9ff!utVtJr7k!i*A!Z_+v+LUIr~0z{!PbbeaMHjri6Eu@Lxr$FgZ%`tfhZ z*B7{S>=;Il3B!P+aF|uAjf3_>`2gYXZ!|Ings64tWaTD+X_k=3-nNip<|%hWV~8`f z%Y@dI_bacnjttV@pS(=~c}biSNbOnAjo+@A0v_2baa07;bK{uwJmA2Z+4ytMtdAY; z^?5*myRtX`?RLP~FXP`Mc0j`0@!RFaA)Makza@8wX0TD7nRi%*9#$uBSe*IKBFA+G;{C-KP@ zkui?icKXvW$^Nc72(*kYVkfg{l4h||JHNcU`;q`?hZg{i(4B5zq2-f$(AK^WqBv&a zkcl3EDBNKGRAcgU?EI~+#IJtZf)M~k92H~01gwrqCL)mk&K9}{ni_A!{Ws?Yz>@V4 zNM=_)|8G5$l}=4|E8=z!#Ode>0`tJjsP_du4~T%!{A%1S4R*Wfx$@h+Q&ma8dAlG4 zJlm2^I##WV%ph?X7*nJ2HSoRT1x7Nd7LJ2*Rz5h4cDbMy<;)?;1+UtdF3K=NjK~$s z+Fxyq+6x1*?^>G?93f!YG$afGi;xR4fo4nA9>fZ-cuKjT85{95N$4d5B;!j0sq9z^ zw(}_9$u+W*b<@0q2nezM41RMuxtrW8NdqY#Wrchi9LON3QOwwO3Uy;i3jY z+6O=&0z1$qkXNdL0jOtScs*;>#kEP6PdV4uYO4qjmw>%u1Y$%1e^rf+4Ao{Q>AhB5 z5Y$Z1&!4&xjJjN+-Rj$}V2qq2qiM@}d;X?mwb>EGJlJ0s9FiF-x{H6Ue7UPBx10zO z~PkPw27tYhOiF%JHC=P;s{K6{m7m#em=gILqV zI4}lS)V^VuI0M_JoN?@ZQf^O@8EVyI?c@uv)gSK{MC#WAd+y9^pypy3jZxV+J7Xx4t$@IZg|#>Ov$8q` zzEu}+h->8vwe6nN6Gz+2z#q+7r`WldzWSOULJWKzI7a<*5x&CWPbOBQWEIyla~kZ& z)pJF4e!C$W8Q31p!Ej{GV@DrSqy>%MT#5}U@&2xQ7TiykT{L1 zPNS9`>w5A+ftWB29U546KwSY)&KrXn#>ilZ2A7tMnEvBY|0=0y-7_~CHb;}h4ThJ< z|9eM!YYfp3a3gNgAbuE}*l2=A^`yA7v$CrDDYVz>=NsRYfbFe$qM0H?z@r&%8>(2u z_tVFr;Y3kb%&BAB+?&spmw1*w5b zFxn+GVGnoV`=o#M$PnJfc6ecoHYLQshWH(oS(p=La+eom=a9OU_fb?1JQtB1N9>1= z4budG`;K{L8nIVc&~wrHfP`u0JayKFSS8mq zAy-@`I1mgmXgRDF%oAT<1aTM`0tbLlsW>78yj&sKZ32i$7&Ik)tjGljrzb$LmjM7@ z20W{#o++$L!0_9b zaENIB#16oan<&rK5ehL59Fqs*e8F~rfF+(B0kCOu2izG0FNtCc2T*l*Mim%9@;dT$KoQ13 zv?YoWz%}XFMjwNbW_%f%MKp?Vh1@g}W)L+S0{H9eZfhhX_W;4G8Wi6DUyIi{)8YCb zzIGk{$9(-&4Ep2!K94XuxSo}SbNA_g%!UG7GHPPu$>4CT=KvRem+bFv^i^2x7kQ7m z1T>GoW(>JxIC}Uo4R&7oE{Oqn7R(X1gssA&B6%AiFd1GF&2YFr59OjEG6s4#e8T2B z3L@?01^_XDQqEd-eLngy(jJEp+U1cxQPen~WYk^9!TG|uWmQ!Fd_%3g-uktpR_(i8 zZYWu#jvYo?;&q0RW7N7~U2*IP5k@TwrxD>w3zDNaQq&Ue@ac-cWcHNLZGI<;G=)YMXKs-v zg%nJHpEl?MC?03_v;Wy4`rh}9=>22FY5)4O627+RUq@2$|NK%Eq}RX-yjYWU@T1^x z2HvXE#tB#lS;ojY2*fpF2tA>R;AkiT(J&e^ z8fjgRg|{n)W()y5T#p^mbY!+Y+HKNNv>#X&n&CQY-LUTnk(Udv7l0g+_Cw2xyi4Bh z_8cb#Lolq?bgc}**C%3RiU{P(3-(p8IJ2GsZF+?yZ7nFh+wQR_{mW9KhI-pcu znldNIPg%~(gei5^ZV`%W%c|m>c#Zat86=j1&#b>k?!t~n_5^?w_?HpT$AR5B8Wsns zyXnPjg)UGeTlPoe3EvugEqpq%BEN9X(oaKBIL@p&oHZf*N2jqr3J&MI^>tFVOWB`g zS5M^gKMy)wgYN8^cc0Geehb_xKWhIX&9G3XTb!%oa;2>ijVWmA0%!d_U=9554X)+~ zh)cxlz`fvEdO-3tpIfb#+X202xOj~yjm1^89!Lo-a(oD=!j%lv+k$o1qac`6Q7X2r zxy^7KwH*kFuP+=1eS9J&-X@L%j@?_%7LD`lwbBBH1Q>^r;|##QzxvN#{p>DtV}NqTx+0DIc57ET&Vxl`iZIjX!C=i4QyDsnwhd*(aOD_b z)Zqx^vmzSX6??DPDx%?*Fa^F0s;)WkJ!5g)14p)n8(frdI3GfQt^o-O#7GS>Hr7!K z5AO{r|4}i2+|ksSOkV}pkjxfUYNu1}@5$@y8Oz8a zvZ?q7Vy~i*LPsk~!K0k$JdqMYu-6wYJA*Np+Ha<9viF_CC^=k=sV~2l4PI1?5abMr zG3xnbNQkNP*+Dq`+&W7l2~1BVe+48)k^ zu3XrX83|saC`9WhL6IJZ9t)=t$vB1{fnEU-f%bGQ)JOzS&|a~+)`J4eh+DvgdNlyC zXL=uh4C!an0RiueDqW!gEW{2#+{k7T+c1v+68i1A{rfm1*uR?ogx#nM0qB(!CFw%^ z8(dtUr~S|ul=Wph;X*V}2q#!KnC_9;u{uPo6|V`&P{bl6!#%@TH)^NhU!Sc zUcNQ$4P)?+cWrCGV8oz9zHQ0{p!SU^fwb>Gw5?d40OZcs7gT4A2*eOcM+*A&j#?1{ z=26QNx!}m!c7&*PM|F&Y{dH5Whz2tRG8uB^oD{$!RpA=2I>8RN2185$u;eC$(MSsZ zUjq;CNumMV0QWYx>0?2P4U8b9SJD~VA^7`j`f4fy8sYdtE>eIdfJBrM!oPs#`FUGzZ=x4PM3>Rxm3=9Dux!sciaS9!xb5{`nJr8WT z3lpY<{eYi|pqCjj*|!VUN;6K;*8|6)=K=z`07B`m!>kT>T2$jNG&7PEu`MvRZ4VqY z!;l!G9~tF9KJ@DyuP@EX4eNm+Fa$Nn>|FHggI`Z$t*DNi!O&P@S_`hJNAV=13uZl0 z{4?Vb@O=OX1~hlcAt4{yfGL7Pr3|dXTJhc5GP7%s2g7chCkA$;Nu7gz;T6V8h;zr)~M&fSm$@kB~>Iwk8Ix7C&J9g&LEjhcwf$$$uEuz46W zFt=+*QoF3`m~HMR*t8Uz}9P>+_PTsi@`iU}aF zxvE3bBO@BD_{YH^Xg}k0ern8n7rZ;Lclz68aR(ln#(z!#)N3vwr+D(jwZ9=IP^>OzG;*F^}=DqkowA8 zLi?s|B!v)ynsr5S{~zxlOjjKRQ^FA#rGLQerUw6ewP!qG2Rf=ckZN zyReBSjl?cGFc`0+N=2>yQCP(|AS3?&TGa0m%7h5L02?sD2iZa{wgyF*GB`T z{4}`Y8GG5zG62>-2cU2_Zw)z8i5v_o_}<)04ULRJ;w5Mxvh%aZ4BrAIOw=v*?|cXm zBDoX<<6DJJy`u3HLm;wP*sV3}I9|ua+k$ZdmY*MPy8xIK>io*>d zA_RNAA(;PqpcKv%7|G_SSpmQTYJnL;LaF|IaGp7hZDpkO$v}=%dlY%S=;u$=6=6V# zDw#1N=O%$?ir5bwnYSB;#9$OPO(6JkMX88}T=egExC0Ib-fx_uH!WnN*8F^;7WhSL zZn4nJ&ew#cLg@CvqbZVU2AU;lgN?*z0e~s!B_I%#$ZlQUI>=tJ9T*LOBIAFUvx|SU z8PI{1eD$sXTgk-%VIX&L{o|JRweD^0sx9w=^|LR0-R1y+MO@&dW>CODO=5;dqD2?i zrjRi-zlj5wn(`;$HK_SPRw0nNrF0mxbODB7pWRMfqfzvc-1|$?mX$MYU<#ZA0@ch2 zjB@8o#9`Qra-Yie&*zs^p;y&q;>!dVUuOU3qg__ml3mw~aR7)wOi>>W(q3MgxSsQf zmx)yj)Vg&k7=$5cJ5V#{nJ_id-?8#C+Wig}?k}Ac^LUC+#DEZZn>j`tZZA`FC`Alx zW5?@@J|CzBlGas6z;WW15^@Hhj};(jhyf$@bH`}dgr+gML`3oyA@uR)S@9@p-UV}u zWFj~C*b*&`k&JDSO_-Ac5EX&{9I)C9KJ*x7B-N*3bk5QvP(TJhzvAGCuNd&N%bfGj zS9-t+uj>!Q!Nsk)Hb^(}-LW}l)7@akH}5<1M9qeg+~E_MqJ{?k0>x2V>@N^cxCDU1 zzlHu{0=bGHTs@1WIA0>i&^uOuTJGG>s#vn>J1zMei#viI>5?UHo%z zb0JgGJYyO#7>aAzSsD$%?8kQ;S=E`{#^le3p351b0~BXUlFMDsXD9R2>YqByHZ2}E`y~AIV#kebWeH?AaImFqT&De&g&hVR1feADT;0HB} zCPXp|#|dKTES;|@4Sz0D1Op-^HSceD>rAE#G?R#H7a|2CaUZEp#NU= z%dCH(1mY}CQ4A9>(W|O^Bp`9ie6G`xjO9pyGXs7f(T$Yz6(UV z4>l_i7St3E36M*BOa=9Ehf04;%+t zJbga1Yt|JpavqwhmeT-2$Bty(urvfcS4<-WyTAC`8_amS(nuO2m1{d~5u%z5)dE@^ zVKi-pApnqErJ_3L0pG899{e_INgPbYA)&fHwmzZ=0Qqf31uoH-Ov%^WkIUv~Rvm9g zUjzY&CJ0%0X51py+HD^X;R##=D?#ruxdAv3TWjctSCDSPAc5ikk>98rn!PcbdNU9$ z#y6fvTL-QPgN%gW)G3q&IweqnGT5gSsXJMLO`gObh_Xxs z@c^o{WNimhq{j|E&DQLhS;W;L;OW@BrOEMGu~*z8AT{PtGz`F76I7c1-(7n{6Uh68 z*QC!~A5TaK0m0h&8~*Fs(nnz(3DaJ+6qq9yl!}~B3thmBe}AJz$lGB}GsVz?=Iwq% z2sBf1j!gtxG0#|6l`5mXz92>|3*7N~*Rj(j5-D=Na2T{@)G7nU$RmI=hDZ_?M1za> zfz3|5<*5Q#4jiQmL;m&TH0Y>0YEO!5UN-KH&VcH^0dks==rX}u>;&Z%lK5G8PXf49 zY?TH09(4d!5g8;DfN-XGRAQ3<12iGFq0a>1ChpMDcTa53v!APC0tVgb4uiCTD(1TO6_ZdG4OTlGL;6qoAIv;DmXzmE*2_ubz<5hJD%V_-67S8*K~>j3~Ufm6H9t$2T{(j|=OEcegfC2>ohFMqNP%GC;Bl5lE){$=PV34V$C5+7BFzQ~15$%jhYf1-j7IstEjuID|Lg z<9BMcisNlZ5wc?|od?k$R`T1xYtT;nudDvOD_0PlB2zeVIO`yA^UWeKhVGk1^7W?2 z3Ui3s3NPfGjCbUUXucmRrC$l#hA{^HTJ(J4c4fT4>k0h`qV~gKFyj=lXS`QS14vK1? zU`OfJt9@roUD*_)R)rPFYz~)f$!QQ6zZN`JRRG3#vA?c*9vl-S#;ZgvONU|I@mydL^TfYr;033Yvg;zVED{dQ-GKll$-qdgYBpN%9{Ggo`ds;XMKp{- z13_AbMj*()5rX~GaR3i73wza9yJ{MqwpQ0k(7?Hf1oi)J7z2&CCkE5Y&^i9eP#r_y z%fu=6Q0Mj}K68JAB^;Hn7t+C5&qHhBoYZIz>)wpDg;wGQ3vh5A86%SUuV-hvk(_3f zg3H9qr3IL2$=&kht8vmg(gueN$#@-g)XoLfNhcAJA^`>Pb)sC)g1zF$*x+T3)|btR zC{92}?MX!Kd#y#jF@8nkLSKk0{vft|Nq|IEh5zmU`rlPMJw>zd>X-%6RuiEaoLsjy zB$=+n3~pp*3+L*~kU|jfB$h3bs z0yTrR7hL1a(7|Ljj~oJ;>zom(8Sa2P5y*!(|6o1}T7GzTxubf&d;eZrofuWykqeB* zmw`<~VdPS=76j`qod%}J5Riz^1Fd})$Flc*=oqwB7-@lXqy-G7$DuXjGN4q{$`m;b zs1**lBL&`)a%FK?GanYN6Xy%(sSRXE0U}D(UYem{7vbc#2pKRvLFC$^ym~Sgc0k??RATpX-ylN9P+!EJ<;xub6V(ppXmqhtXRoCY*oFDxuZBJ~h zR|jby#L1{blm+=Kuv(w*Ou9%@e?OF9SD}DUc2WwV2`GdCK^UY1wp&wC^C>(l4)@QE zw*eb48QuL#fP4TZzqLI~%Ps`6D+B;6fXNd`HohnVF-ibCiK;)Ep2SE#ofUooWZVo} zg-d_xH3UFk9)HbR5d~+cicwC zsk3f^Q^Ym#I;d7XH<;meMX4AK;5ahysg*pHk4+fge%S6x9;IpaE8Fy*R!SE>I4k}r-5U_XvnTy{a*{N(XOK|k(@g<84$%wU>H$^r7Br7Jco>plGLxg-sw(c zg9SSP!Bd+G@+*B!=XB+OsUL_$)MmkZhN=c(Gs0D1(RG)0(`Mn~Zaq%CZ#%BiK`!h+ z+9Y0F5pby(_GZ8=?rekz2>v|uxe}xWm~bU`Ft_|LDv587*^JNy_^-$$Jc$B-LDM86 zKu(QdCPW1h#34~EXJ|(Np0Y>AUQwm$za<-iw4kkYH;d5rYl6gJ{4q6Ofu62&v7Xp- zi5LxA(R)UuwyKW}61-?DJvIX8qnsi`L;0i|fP^t(%_2BmeaRdH??Vqh zJsj)#guO&OD%(_D!J`5~3=F3IX#UpyXrtL+X#NRfU^^Vgfb9KVrIXUqrmK!#GW@Uq z=l`KXjp8V~I7meBA|%kDOcnDWq0x)mRe*AWJ*}x(5sUo)$a=T!NRq2b@3ZY55qSbo z#b*B;X=!OBy@f`))%Dy-cY6Dg<{z@zRRHo32Y1tjdnAfWAXgKtMkO*MJodr1Z8I!J z)QkvhvQZmP}w27vQrsH zbhj3KP)9_U(pMLSLi&Et+o;!}_k(uZ8PuK)bs0IdbyKLY8i~ye!q!~5Z%w<0tzm`W zeV*8|sl;iDx1&<{wz2l^PdPM%=Z&>cHcHW9BEt7oh(oj3ld3rnZ03+?+6o5I-BZVe zt!dP0)G_nesPLB)btmYxUGR3=8a21ce!LEyXLB>;1tLH5;(N zIvDV0)+6F3l45BkJU*IJ#Z6{7Slh%`YKuBRwJMsjh9JZkZrvXuh(&@}BOVZFMYFn; zgUuvo;?Ei%)uYG}KZ`y$U2B_L>1xzYaAh$|Ra0-;_6&GP95VqwS3pa%0wjj4Q`XBw zNUtMf(o#KF6?-+nC|NuT$HXb=Jm_`gwz0L5;_lAPjtn7vKT~S`0dqL+h)j9}?QeMw~TDE)}EC^#?Pst%)Gk2($-o2a2!9e+JhMYGjppZDv@-mV~3 zu~h%LuyhPh^u=QpTq^hCkB2|*A~YnXV6K|0awvkg?C%4CA~+^qk_x_rrjV&Gfp-4L zzDTV;nh}iEMFe$K6fCE(Z^38}W^{!4AzPZi0K z=V+j-kI`^dE}2n$7AY*seOj?h2szbYE-pc0Sh-m0TH z*aa6+TDQ2n-xPaLhY}uzA&@C<#@C)Ora8;KqAT?X6YVZWOS_xnho8&Ss@A<78uVFe zhLG-sxu|JhwS3=my?Q@#+qj5x(mnUmp{+VL(9hpC#r(`)<-U{SAMRAe6nyh|*Zz_AD%t z{Rk`-$yQKb%n`tw)R^z=^TCrDDl-}&I-#!}K8zlfxq6U_c@au>r8OI?)Bxh<*pba7 zOnge$T3?$Do%GYX*RHeOer;<$R_ku$u9i91zS-d&_H5zmm^fz+p~ajBoig{$OT|`_ zo8D$djWn7whXazg9c{{D2xpUNuJOJtAcn+w;^!TUl-pfsLno%hWr`s&gvO}Xr5~4N z`^r|O)SuUwUYa2*5sVqhkEKiBv>4Jl5(ir)n?vSv#qpDDY;5{JzAL zIRwXG99*gIv#$^CbDtjc3c*M^9atUT4sOQg=j>*&UfgNf7UKX=%s9mNqYlZJObXo! zQ)+45h7?NjZPVM)?+=!p0~5heg_cS&pPN5dF*s1j_tn>hxpZ1z3qL@E-TR(JXbjd$lJAAN5RFJ!8yUJ0 zb>g=Z*g+3|L(oJqi?x^uCy~{RAv!28F^%zZ^mXNRz^K@@x{9A0L->?P;*?cRRO^_@ z61d6-5dKkk%@}>u9@pnSL4oe*;jG3i)!m&gfDu`Lw4@LW_=pzyfFIHL1un3i6b}U0s9j}wm zxli>6X|X49E&g1&u53H4F@?q?%a_DlIA0W(kYY`y)CRMoGAw8NKS0NE&;t(FgS-RjK zrF#)bS%0ozj)_CECnsFnkeyrUg=a@Y0>Mx5~ ztflyMAX#@i?Td7eN-?*>vU%A!PiSb%Jifc{FsA_L`t!!R#qTf9(h>qjIY6kITXchp zgDqpy!H7Cb4?Km>wkj4&sL02vLyd>3q=XOXEo;13Fv??ifDzM#vQ%E2s9qG0sH0Qefg5OBwms>^k?BD3#Q;+IfRe$F?2Vk z(dYH34*a=t-TXFE&EFqxf*H?^9jHGa^0Tr_Tb$%Z`E_*l=atxO4VJg*1fXyDNAg42;_#*JNCthT6t_< zR}EPuyTB+WIEO2~NLObX?Xb9Wh=miTk$^*0PH=Hd)$gLYZ!`%04)@L9l0;L|($J2cXxbx9ig=Ug&dr{z5hi<8 zyo*!AFr>jA#3ghzt8@BFmc(P^2i2mo^^_4;#Gv7yEvJB@Ypd}wdJE>X)2KgYh=XtGq-#8`i z8=o7ut?dwVFU+OWBA!H3L`dVHLyjr&HgqT7Y8e)D&W-y_JXr{`Cq=Jr_x#&3ZR2T1%bLfy+ttsh}m9s9RP9tMbX){L|UIz|Y1)Z{vgSK5C z`s?wd_Y>`^-U@T=X%6`%an2f|zJwm;6wxRui(AjcW(}d7)ViWW_@H($I7tu1)tN)$ zkO({lNe;+(0^5`I5XV02_Ss}JH!07Sbj@c3@KE!uV6z95(Arn_A~hdYs7=<-0w!yb z$Lhh_;2~j8=2h_%mYt;0K&je0JqL{4Zi#DUF1>Adl#K|k zYwsQ`md#tyR{gp5;b2b9XoSX_-RwhXwoU_EtIkW_WZS)8|LTfZ10E8!dBv$DWDvF=dL(Zt6 zK!tev9%eMFFPcL9R8K3pq8$Tkm!Nz_2+ql`$(O_!Iwd~Ke&!lmEBZI>-VebgGbRT0 z4xl}DwVATwO8{D}ikkLwZPL#B#d`7qOWFH_Jy^EnvRW@Ahu~xLQao?0wa@8+kcvG> z7E|(d?fGcgc-Xf!{&HfBZtmJfxz!k1tv~ImU2QAo*`OG5(%;TnO!GxkKRO;HBL&|w zn?-PtW;iFN#9WnAFGcHqs(P5OwAdM9NW5jLwc084FbN+*6$YR&e(_rTxgd1PO2H6L z2vW)(Tblj7p$Z&;hJUFaBO@yE<1_dma=q2&PCY(h}%Wg$=< z-R2Epw`Ia(cy;zs7IC#N;RDDu@Mz6qut#f?tC*^iy@VIBi3G`(qXom!9!+-K)F#kv zRv34x#Nu={#q}aja3D76W+j#1KxS5aw&l`(_4Xv#!v1&=^!}ns^y0RTi_IO_DZ9Qs7f+_y|u6js}LcXC}m$)GqsKC;UGi;#T#? z=0oUHa;v|)1*vLi%fX2u`Iwl*2(@zqD^?-5{g!q=;~{5pMt6L!4Db*(C&yU5h%hIV zO!E#;GKqq=Hf9!^a9>gtxDX%et%p%n%>tYq#-mvpt;b%7>o4oCeS)@!iBTSqVSt19 z&@NEkwsb_}(5{Y;;!F4Zhyqn64!v)G3=@RD5Fsw(^}K8N2oy{{e5 zL<*W#M?YInVc^YFag;MUWXi;lsrJW}91UO4YHphcdHYpE!6=jyV^R|+cx=Akm9jDO zqT`5QZn?FK9^$r@(BgUE=;^Gb`_GMgg^es9q}kfsJ@lFgVdE2Q_}kb8=xXWbGzEvO zmyyQ?!5}?MmgxgQIruCZ!v`@4;VDp7lOH>%9-91-S)~OHFpzbxKB>Nmgch{n4Ip7N1#h=#*>@&4_w~cObud@^_n;7eTG{m=2J}>r zZz;bWFLv4H+3`>NjFAsYQ|fYRY8$g)lfqd(i6m7yx4O`Fr9uQYTX7`Kg(?hkE7W+s`u&Y5Glp$GS5Db~zx#9f zg=rIj=Z(ML`2Ey{DpMk)lyw;OGLZu{O{LususM-v7t3rM{PVymdkQYmY-w1nXokE^ z@y@hnrSWYdX|4Xb2%L9Ed(a`PnXI0YFRAMph5Qow$HM!Dagt7n@7Wm!IEFX1)neH- zq$(wQQ2K{Fkb4Gf5VpmH1Pfk;g;Wk0)HqT(p>#FXrzWLui_z=-D!vC z%YIcT@kzae?sRg?Ow4B5)o**a`lV29WnRD}2Kj(C7=n-obg#R9vk;8I)NIxpvBGU; z$1zKA%1uZUnlP*C08(>8)QXQ{%1pukTPW?0hAty1_;Y5h-PbOprefb#_!|$Fq!J8? zfGLppx%lTSyFKOmr~A4f&Y`t($~xuX1R_Z(d`xUj%`3U1!RjU2l=XV>`z)ltj!Z-E z(cFa*7^Hb$qNLduiZKYYz{k>2Qz6MYbvwex=1bPw(AaOf7LLhuysFa{j^RssYW+L+ z4$n406dScNnhn(^`1-&TJg66IzzZB9Tgl8p!N{|0xDv-AaCW^o-w z#>E-|c?vDbh{x82Yp3KvQlrwFM|-ORxL1B`9VTmaTS1nSgg6YnBB3SO!mQRgE6eRj z(L~bWx_U1D`(6Dy)UP@zL{9Phu|Ih)SyOZ*ETIq;|KmZmBCuWi08Zyg5#*#PE0T}F z$HoP{H-m;0_VB1yvp3wPo-&$Q^;mcK^eU<%zt~%V zQExE&RBLPc&Q?1IW0Q~s*LJX&?Y7!E_1~z)x>oKR%abA@gx&^1yba3XL4D0|HG|l! zTOo(DEEKy{&*#Q{Yf)#PY3^vn^QHnn7A^x5J?dltxlxkLw^2VAKl_G8@+_6NN#}w5 zMK9&wXD-=e^p8AOs=ZXNg;KmOI*nk5_&$*6`h5oPwTjfuHI~NZNB5n}-vYc%wSI1$ zK#}k+b&`9J?}WcEF(CX=iftZF|Z&W_2=QMN7cx`-v1L-+8e zI8+My3=1VOOSfn<=AIDCFV{2Fo~tIU)UUOE@uusiD*7i6A^8#^gOTyxFC5tHpB&i6SrFJq%ahwl24V)8`bgNp?W=Tq+97>@b+tQb% z!q?&NBeH!?K!IBOL}3ao)_YY!53ArLPVx~HnWJk0yN!n>YYwj>*0%PdM>S4!eX{7bN=x#3<`wXgu;g|FgXT{3ZQ;J=Htlaw~+=wV`kl^53E!jYKw^wK_I=R0Bag z;=qG-7lUbPvjZ9RNosd`o;5+61s= z_UCf%vp9ZEe*6_FdS!es{UNEfXUt|p5s;MYiM}k~s8J_-lNaew4W+2@k6lo8y+(Z~ zoogM9tVgJ6#5a`Td1(IS`yRl*cCo!jtahYg|2uX6Fuy)4^aJXIPoWJqbjL3{sc=C> zx>h~g&lMqU)B}3ZCX7nqyo+;Y5xYN2BL1j;3BP1ObN#x4tuCA^OH~T48>@NERG7qP z>3&89UI)%uKTAhzhs0Z=n$L|bTIZ!9>1FUg9$34&ZYY9p*%ke{GRO@VNMcO9X3tfd znJeE%Li%qHByYQZX*KtaY76D0b2d$Fr7e^WD3O9=>XRR+7gQ3P*sMtoaR`oyYJbc6 zu`$>F5&8|?8_d1R@6BoZ>eQ~s@&GPkedua>ZVZ~sg6sy$x)5~03ECBFtOcCDtsZ{f4tF#SsW6lZES z6E!P6q!kYkqR0mWT@g83RZe1pshFJYIY{17Rg(kSHI45tHAzPzb z&?qj6+0Noy=11{wiH{l|8@45$9IVz?h1-qArgXO(LD!ri)q(i;O&gg5$ZXQkFOX*y zAY9(1jIuqs6tO7=XtpcHi#!mk3H3AGj>6N|`3t$((W!j-;!>Xd*Szqt?KU;+1szDAFBE1LWPoe9U7rhOWPww#*;KxZgka8 zR1avb7Fz8|B=dkVJcX*ciWH49>VR51QNcRt?GU2 zt!Qfu@3jl5`4@Z6y2r~#oLa0yYABRPQD8)tsu@C?{hAotSi>w;LvYMm?VWNX!&-IB z9-|Y2$AGrGYlhHbhEB<13JN|46&VleS~2&16w_hOCTRq7VXaKva7tNdmj-SMiAQma zdXY9M!p+Ao3*^@zp-p}fMhgs4e@j%Dt9DVkuX?NQwC@&=TOyQ*;2`~mld?uvNODER zic=k$MBbbv$7omT5|JHYEDiQ5-9yRz+(8PvXSQt*IsT=g_y zBILlAV6-Rc51jF&rhuh2JlV3YVpCfOYRl7%W|0SRtNb+(@j-pZ=K$4Q{Z8GMQ`vi4?Ug zs;l@ACo3GnEdu>1CUdTUZ=r(9jy|{F-8OH_9(c6VbDO$+g2h_CN&*a9^$7_w?FM_mF9|t5(9L?(SXIA??Q0tm@KeF8nuq^o zQ={Pn(Vs<<{MV5w$d!aeYz+*o2(~VY$t2xv@QVl-(R<-m`X_FEc&noBg>&M^O5ksa zCX$<_TU%fkOI0=3(q};dHVgGF@fNz7dm$u;%MNF_FRtlxW3vbGs7^F^MGt9le9CKW zY){vwbAbhe2x}7q-W*H#)!eMRB^ZNkUp1(|M2aRF{oBAn{4A{aD8J#+jct|b({Hp& ziLyvt7J|3U9yeLkoT0P;(9xR`V5q?*OJfvD|+$dlA0@<*`Z>b_U>nmGjjoLfYZ#8!AWTjo}L z$yD`5tk_`{w7L0>QB4uGDmBOvRdcN@wX*`BJ&=EaH0R1(HAJ5}KU=sYw&dSZC+ZAn z?qG5wf;*c^tm?zDBt7gg_#VDl??tQ4UTJmgbU$G^JVt%p|8AHE=|BFT|A{JDadKHh zg?yl{N&Th~ix8(g17Y=+3U+_gFd6Y(cL+9=M{~WC7G&%_>n~=~Y0VNPcw)0={JadZo}~ zFx57y)@;6y&5l(I{#d!K4N6sQgeUblQE|v)D9f-Tx;%w$u&_FO`?fBsSE{ zq?*ZD+lO8)8h0p~?{b6Lw9#oi&!ZFjA*=3+P{pzke#vg$M-9+hph#==NpVAMI@!#1 z8v#9pMYbi*K_T8Z&9&w99uZn;6>62DzlW!`l}X&1lP{T%Aj6^7g*?@5jrveL*h=VF zn;WQ-T?kpCjTOJ}Pwn&Oz0&Hpt?AxY9K;8xc`!Y5<&LuXNd>V3hS*1*dx@4%^Lbe4 zs$XSaK+pD|xFSZg2t}$dA{;+evpOefF*oGrdIGkeHq~EIN_(IUDJ z>d)DaS*L@Foda8?Yc~53jjl+wu9aGwgs|whyT4!`DjT7Pof=l`re>z4IMGLqB@v=U ztcQKcy~+4dI%bQly+#Z=>Ar271)RdK5kD(K^qjDva9g6R2ZU9n(zFIDLJdx{G@IH2 zq<5+#%K9o8K%d2f*plw}U;^%ryX!eJ2{tLa{s>;oD$mlnq^Xuw9<95459p53E^1}j zmuYk(vANSY>j<(BsKBxEGxS+~5;Og3`b%-J9mHlU$Al9;N*nQqNvab9tzvZ}PHC>t zxNP_k@oBQ^v>(2;F_qP{Of^$pO#}nj-V_ez`YcQO;w#2x&w_SmLo-f;e%O8Qqt(B)SH7jCiSOagNu*sw&c3-EK(_36)9B>m{a24Shzp z*%4}5MNe?H!9Fd~)Lv%!4I7eG)Q+QW?z87-*3+|XY_+HAo7ftm);`M@_r&D3n_IbW zuF?=HB8jjHH5sICFhUp!HkJ3@;=a2;TNn*nq^4>u&_WRvi(bP;gk`7~6 z9nbw<`p4Eog>k5NPG2QNLuy2`fcs10ASVz^=#S#xb9*UI(ueig)jhcz+R6qxWDjDL zPoaBN;}B^dkhHs&_DZG=CL3+|winrm;#?o0siumPVl>zArGy+%e~54Pp)!ld9-kVC zpnl<&?3uU21Ov5Lm;tsRU}$l5M?vDrD0iQfAr|5Xo1DAXpn}~ z%pi0KhU8NqDE5?kan@dnj`?*Rz<--S{C!3+h5tIRng6=Qx9peD1D_LjG}}jXcQ*{5 z&BI_nHvhbn6u)2CLaRLBw?qov_hFpBk|69*1^cn?r>fmtXI;fe{X_cA28Hz*Wa{QR zx-V?CTB_3-o_*i`viqa*)zD=zP-RXPMCm(2{RzuEeN4Tdr-O(4`#*~?nA-11gO^+? zL$FnC;bf*@afkR0W@~5yOwwmF*OYFST~EYE)x2MwXVf$#an5ZxuOCa_foyNASl1F8 zye4mEYQBd+bY7(k=TY-UN}6(mx!J6b>T~d$^zVB;_q;JXdy!&pCMrLI-$pEk(DxI? zO;O#4USl~9+-sQ46`ZC4YT9gBU$Bdn$^aL?wk zuVEV3sz~i_HqkN$d=Aa-P0yZWzwVv)gu}mHTPc%qk-k~EsUb=;Z1Uy^(@X%ieaU)X z8PdKh+VO>LZ{8)(D4A7Yec)G=x}RVh$*D9Lq9Y3=-1?D=>#-eqYY8N`v$$A@F~!T7 z>NdbvF}KPgoN=}cNxI5I+uDFxB*?I!k6*@@yL<#tgPX-T}5G85a*ORP}D(!A$dBhdej)c$Ql zsP`xpd<{hdpF!4$kM2v0b&r$oDVx|!XhWtR@C6&K_5q?&_tqxCDAwqrUW0g;RtFr_ zv9zlgsDp{NvrFUr%ZdXv(5&=F-Mbt28*>W(p0yG`O7j~d)D0kdaVJ*vUh7}GJB_m` zofYT`TciB9gJK@ARvtxj>1v$)LhofjOJqq%8p7WO&Z*5_8Sp3~wA$N_?X}I#EOK-a zhq_PeJF&enOPj|>@FG_G(ns~K=x4FbU$X~d3$INi7PclNX+mxbWQWWc^h&Xx6}1iW z#$OGXPY2eIruL4^TrazdxXt;KD~a7qs8R(nvnv@7f` z${@(-ic;m=_R`=*{7@JB7u~9E#*>u9=Y~xVa`djSorTt--|P{!H#54eh66M^z6`VV zQF!`-EHrX-uXernP};Gw#tX3ctOWHf$)Zgn+6G^?Ry=euDgWD`e{Q-JM!9VS6^q&_ z2G#txSufddiP?%knA+K)=ft46xrwI%T@koZ!6J~SP8ugi7~itgjlMKM={B%_eaWN!$bG8 zK+;{VRw3Tat$s!9_F2a0h0!rd&9({;YUSDvph6|CE`wN`m@j$`HwLI^vpy@=+H24h zeZ1{oa>Ey=!@b=#yszY_|6z!$>zTGVB%`KdSfttYjMqIGu#{+5)YdCaR6Z^iS>IH)9<#&2$ZkqC!;w3?9iH^uwz;&ukB*byR| zK@LdLO#NmSSd@Dk+?u}HG0Xj#7II&?>8FYgY3P^6F!tCpLO`be_sQ7`*c79yXndRa zS?f{p&A#JC!Q0g5*@kPhp_P&uP1uXHw2>Ap#D|&7ZeicxV0=;w?=4!?!76yPC&k@d zM8(@~3H$2?hovg+X2loCa=|a+fSsI)s7|enbNr}!SnY?;5U@hQjmmNmIdl)GJK)z7 zpIvuoGP1nHo~I9@Yak3w%k=+IdHNbN%E`oO zjvg%5u8YnCA8@uZjKP>yCdRB|ZbVZjoX`Mv$Jidk=l;ekEUJy8FpCuiIUA#b8;my@ zH(NPdD>SvYz2k5zuGfBeVLx8a8vK{5gK=^_x*L!j+DSEA6Cok~Fa6q^Y7DBVn{K1H7#PvLO~C$pP%6bbz-RJ*Md&>PUb*x1{n zB*yT!XgN-ebwcoc;$;xHQT&=|^3b*^LpNwPxtpVypoYj!|Fn;d79o!{epVj?$x{VW z!#JzB#i}RnwLj9o#47J0;rTPawwL`8Bv!HF6Fqe{{P`|MQ}77C4W6KwY)%lafwJOr z7llW@i9mx1GuW$Go2-TPi3e$|)auVWuO~(8q;@E)m;-T@muBvxXwQ1FmR}~dy>hY} z_k*T)z1wy+<$|WaP^?8N)&?O@su$_+o22?ZF`}Q=t=5;MrG8;N#jNq1wDezhNSrev zey&|dxlOOzdzgYr9?-RFpB(7D^g&@3q52lgg=1FR3Vvdu|dXAn1`n6D7f zgSAx6d-XYdfULGLR@?C&Xep9P9jyvgBs`*7x?#c5HFsskn$c>X?6ZVxHKX;}qO5X< z4S#u#+e@?2gxL2Y{r`tmJiF-jNh+*zpk?p%+W(ByQT2al)1^+Gj;1zIp+-#h3ODip zmGx%Hab(Mq?NZzB9vwh3^V~X8C9i&J9B&M7{v)^_Rj0`$2t?zJ>HDzl0pu-63I#-h z0Y~_DYt&TLK#TmEjAnt!end^$b3h;XLi~bJ3{97l&Vvu3RG#F~+T?(b-Q4?4y4ohL z*1St)w41%8X~8JJG>oq4F|>vkdw?I!uia*xS3E`hI|X@>&*+!<*ff}_mAymUHjH0d zo{`a8>V0!52lt@vzfBr~pNoecnKTBJ4-8zw1%#%c#QxzQaENk4i7P&$Q)4k#nB;>z zs@r}jXx3)7=&ln_h;hHOG24G^lxiIG2*1EV21^!RSaecnLNVDehd9arNXbh(o=F%@ayxmJ<;}0Z03pe6lMzSJ0k=~dq6i_zhpDh z?$*A@WhinFuIjr8>Zv+f$8=@1S=_~7kLW|5tVKSHS(3C2OyN`b&`s*yN+la;{>4{Ih?34O$uIjb_*);F#4r1dUqn42qj|S1AH`B{vGZ?D7kN_rLWmJ0U&!yW zVJT6NWO=0{XJhN{&1{tcX_GIF3BNQKx+`-}BJjiqDBFW!_UmFgs z<`6lFaQvAlNp;Fb=Y`V)C)9ii#KT;WBF_cQe3qVWbXLg4HgS{wwdLRBlWem}H@LV% zn`Qa8kR~VVy%1n|$l)tL>1$s5{P_9}pBK7E*ROz-dhPF{eQ`YY3FtkXaB_w05Se0? zR%wGpy$_XmV7HFk938KlXR!to>IP*H4BEieZQ|mX}^Mi zW)JWmBa4e2mEW5W(mxiRBYz7v{5_C$2cutBlP^Z51r6K!>9XO>i}*d_9o^-#d_dP- zz2e)9aavM-dMAIum_bZJJn5Z%5TZ0xDmI?OUb2gcJ=|? zEy2;6!^dP}-(vq?YNoFr@qc~g8Mo@0=pD5?0y~&fb{w2?W)haJ%F!`Z=&~jzbFKQ1sJH0PjsG@s z5~K5i=6)Hvr4ipmvk&N>nZE~3@h@w(PZV92#1k6Ds=T0EGKPhJM-R{!ha+l!C3rR~ z``vxEliF+6#AE8;jQTopyoZk@i+PS$5x_wB5j#-=# zJ+S143ze?7@9112nlWnTp;#P0jh%m2b#2U!pJpNc`9*@(5%B%5J7TGO_i$6NeM5I! z_#ztGh@bdqWCpn@Pj3I)`Ga-^livk}84~wGyh=4%(YXqPz1Z4tSP%2JK+y2P`P(3w z0cO?K&ptvv(4m%-wV=hZV8jAvn4(N_Tz(&anhn1Mf)A^#H%mgdN#!nFn zKE!0wLbe!)O>rj{xpkOc?`CzrM0tPZh4w&i&kNnJ-(O;Lz1^k!drWHTGE615&(hBU7n24P1^9WLwb65?*?1oZP%Lazkba_ZKdKZxjE{= zUdmGf|JH)AoIGqOC3MAyvU95Z}`U(C=ggAuZ*@!%i^xBvD3{lB2@B2)_e_!6#i z|Lr2`Oj;=H0Lv?)e!Tw&qTWyM}1qYz+T*}2hO6TVyXM9e*IEDUD{AV&Xj#e{w zDPT0kd|x6Su+rW{;tN9Y564WgEokj*u%)RRRLjTZ$&+;hBpAIRi zpAsju5O?~R4>`yS@hg$y-tVqQ`3HS@?6o&5vK12OVHPWbW0dVS>G*f*W?4R0C$4xv zii(OvNnqxG`O6pZGQb6R(VbT=keY6kkH#!6U=-n7T6{1U^Ky{I4dhR`tDC(~@< zRnbt}d4HodIBzYE>5VwqL;4}r<4_`@!5Hpx0K3QGxs_gz0S)i~(ami(u*$xHK)>jW zO=rQ)AWwzweQ5x-i-?{{JpCm{bcvNq2D6Ew_XPUr1s`{LmE>|k)@-eEF??PXH!sd9Wi2$8cD}*pEZ8Xs->)@e|M9(B(KIJtbLy{AuSl^fp|zW zE*FfC?DXJsF`-bR1c6?;Xs!Pe!)p5T(SorCZYDZiN4jafQ4OrExF zjX0zQkI+Mz)+q^lQ7q~ywUxnG=WNB#ZTlX5HQFpCSqz+X+|f;>qbMt%t`GovxZQA( zU7x78pDg4rZ*<#9_v&^dT;ShH;A6GWVn}BC^k=TuOpkq~dk*LZn>^xUp^tb%cZvgQBv4`+@Im2R|0sb>Mc3qKbw!PsZN%Kb}<7M^Z~~{P4cAp6J%y{-?XI% z={OKE?5C=dB;NKPhRK~4(&_a5lfIVDSi#X0|0yzGrO{YAX0DM%>$@qfc;9>SG5JHh zR4coaYP63jkLg9W+=}*uR#+8VnG=~&F64Lpzc{jc&!J^^Ong>U+q2U8FXj;jIJCS- z16;z-EwZjnW;21A&^wU%8CtEv#ER6tRliAKn{Bf8Jp7jklwbx3f&Cqi>RNc>a(yBPZHEB zuktzLY@d=}ge!!bYaax&Nt>%FrnxAo%JgIN^-hHT+nH5^pWJ22CRkqas~?b%V@k4Joj@0U9%57?JKH~k1rap=r=oGC7}iXkZVo)WHWC=tX>>WP43aSX#v*ADpiMK8gozkbVZv-D|G@bS2!fQ)_10f3AKBPSGi4RcGAx zix;l3<|x*5ZH+rL4TFcqfq0m!yC>sgclc@1ojmFo<*uvI{(vRjk+nkpOXL|QyP-|4 z?Y$0uCI_kT!oY!=b5t}t6~B_z#w9Ky?RBlxH^7q;Bc3xH@AUR(rPb7jFs=KGdca5f zHOuC2oux5ziLxZkM7Z=Vx`mac$@Cl?>617&V^yP=9KeIoez-CK^Rz{nKQqjp+wKhu}@kvbP9yP&?xBabN z@c~bwtSiDgfRAc&$diW7S&P4zg=)pk&p65EqO14I9C*5ndKCx9LYlEgE3zEkW0D$^M*F`u_rGX;cbV|#21dre%sc4&i{=3 zJ&f^G5q^Gi4J}Xd5yD00X&=@-{l3%_uVbr{MRT&)FTp*F-x4?F$z0?n-OVcW?-fx1 z023xjL_t(F#T!hHw7jVohd}5dX6wFTxM%TEyZ32Xn{Jzi zV76cBlBa?AW8D+(6Jz}1Zq>R?hR5(x+TfmLitK~7zl$LLnR-iJhkUgsD~SQy5+vRl0OCS*HdH23H#=h2q8 zwSQ068i83wAntYLU6f3AGBbKet3dIRr1}YdpiP?Pli9GWgIyKts-C8c1HPf9vga2= z`6|*2fL-@Vz%x92#dyCCcC-Tx*Yu^89#xB!=8?WG%&U$ghemCjlh~3KE$KtVm3%Q& z#{{9c;)L1pFsY75KYRX8XW306VuxTZ0pv;jeO4;*Y;FsZHoAe#X0_ljavV99t5&ZR zJ*w+E|I3pWPurZr#Kk%I-u&FJ7V2;Ni#dw5@kkvcQNR_(lYP#Uo!&228o_jl)6S@j| z{ES&(pG??E9K8L_F^A^RN~E_%yd+a%d<|yhXu>h2Q(EL%`sn={3!mZ2&)wr+j?VLV ziNE|Wdaf^(wjSge+-m`iVp-WX7=7RLI+EtGaoaj!!S*EG&>+7+v-f@=%dN!d+Vp2C z_p15Xq`8Ah1f0SRU+~n$o@`R4=Pm#I&@k|F(3+g0T3Werh)!*&TeDgNzC>R*=r5pX z3!d9nn)uQ3d(+LjC3FE&Um|s@ zSsbGpzMLwm?1+Q+quu+LO%8h?E=jW&bCtiFly4&BF*Q=9Hz^rb#jQ#?qL4jHMOys^ zcf|}zo~+T_yXC12OXUS}QJm~Us^J^h`La=yLE#ZavIVKV9R5Pfoc(#{`lmaquIckk z?ceXCe3Xw7xEz#wAgsmJuSYgB1!H4NK32*o_PQb%LqUIbAC!zj2Q%Zroff<7&_;gc zCGQ=6Z^Rt4e6vo<3rZ9!g^v!U4p2GI`rNekZSow$wMn$z&x_&#u{*2g7Zc&4h!tgR zg~h%|Yf!q0YBieUrfsx@7U@@UXPk+TS#&36m7v$n9<00cDCR{PGa{51siGis9LvB! z@6yv?J?Yb~aIHi?X z<(qV}1uvTnDIHRcT;XTvZvRiKfUdjrAkPZnZ|D!a`7;2md+zJ$Z$Dn%d!iN$dz`3) z^&-E>%{kf%h24sBo;o!#^{K;SMC5N_erZ=x6ry z_blH13|Bpr>TYgp9TcrDyp&0L9o3?E?p=L~y)PBZ0X1l~Mif4*#||6SF^au$8KV0J zKwL9V;WPO|wxZdaX}GE8>#?cIj}G?-?Wg<-t324J)V=ST?Az)R?x|zrB-Yd~Q)@wv z-Xm5)6wjyXnU(3~ix*<(3Upo3Z~c&WI(B0FLhK6!X(T_{+u%c+P=nEXa_{0(fE1J61gM^;f94? zHb%EZ=?&!~R`C!U-d9XAj;yIev(Re&a=vsANH8mLl}8wzlQEjSZSV*VyD5~-x6Kl3 z?U_zZcojF|V?RuF!|MKZqmlhi77qEGa#k2-*Axxtp(wqYOpBOsBTuQ4P&c`mArRa$ z55x?YLD!AL$nc_LU*B4tyEn%Th4UOaN^glQe@WBN6#O(qS-bZTQLb9jM{ zlyn7-5>L^e8SfC|NM6t?uIA}p{FGVeJ`%0Uy4tXh&5JxK%lm4E#NW^Z{<-n@DEoeW z(7oVhd2)CUT9H^O)Uq`z+n?MP-Ar-`X7>>|xBsZ^_@~gFDQz1n%oU^F$f{u|_TN@*tg? zrl46VpQ4IiAl}i3dBI^CyhyXU!Tl$Sv;XNYQu5-~ly_d~RyL44x!Y)4w6YR)EVU;g zC9%pgamR!GTg0C`d^PMuzaFc0nS8&B?mZUobW@OuKnH2+cy(=DDtZZvwZK8!#juyc z4b3pXhkbp}LPK~Gt@#L`O+y}$pODX9W&!D%7}PcCoStlX9MRwnBlecrfmsNy#6@d@1iVtngaj@smS|oaFRUb&_o>rdG$j{j- z2!9K|u>IE9h%x=|O;1^R*~>_M_h-mWKA1q|t<~(4xre7@jf(1iF)1c7*Ju_X4^Z|B zfo|5#q)q5*B+Yn|=AGr&(O!L4vCj-82=e=`z^S8mFq>$0sPYjb+LR}2RVoz8h(>3M zBh>Qg1y=?3YTacKS5z)Y>!(FDWxet5ERs@K~Vi{k+fuEx_J8qVw}9KiqYF*H*)Ff@a~k7 zFoulJp^v1TlSVTtk4U?{LJ-C$d=pnO?TOrw${EIVsEi5p5uXt5v*q!#hgA(6kll(4 zfHvnrx`cl1K`ZvS;cm}dHq=`2fLD7$vkE)VCJ)IVS={kdO_rGlS6vk)lPO}-H6HSstsv@IXJkap|X6Du1UhAJU0Set(EN+ zCVSnRP~-PtGOGmM#3AF9UX@0p2id7q)Dy13e1vD>QHx)`_X!v)lWGa(W)+0D?4&qIn@sV&YV5Ri5J8l=|V@EpHJ>k@gKXo$C zIb9uS_G_+UD5S;hpHgQ2C|;0!u@WDw2G}E7U@#91@d#8sAn`Al>?5AV8Bfke+zln; z-t8nlNAF+XH<86qE!!q~b-Uv>4Dw4=a<++13Qx*7bZRU?!zC0un80HKLz{Bl8Z{)Z zDan)QS8qj%M{HX!wU*U!%&;~SP{k0=E!C^ibD{R?%gu{iwPwCFHT+Oj_gP)5$bd^$ zW*ZLaMcj!OM{~~N59wRdP)*=7wTAWi)b)3#Y@%xKx}dhqrS(yE**(nk0qFC1)p)zQ zx*8Z9?T2*Q0kT<4a46|E+QhhfO_;dBLVm-q#HPM|So_`EUz-1NsXPJOeRyZbd&OF@ z?kmcTZsL$a-iU`K`4OzvNFJ%bmd z-O*!!2YRhngH4>QEXqTAlka_K+pS-M2r=Za?G%GP+sVE`MS|?f5pXJ|jAN1tF&@0U z?II0n8z_V$M`PlbG~+XR-+CD}gk$GN%!+q$cAnCPlWESM(SF}@6<3vKjzr!hq`@|R zh+1TQG_z>9g4UN&pdNNei6?uKmZd-Jr&HZJULOy-qIC?`?4@4p2kJ8q3Y)*IDyeW4o&Vh=(1K1S~< zj9|i%?LGXC7D}@02?J|*NWH>)F{kCRez;;bDTaMPR- zw{WZdfw);Gob(VLXi9@u4a3N$&5P8Wb5z4f(8YbmyIWG=Aq;FQp6nyT==&|`MEdM_ zaf%>c#0X=HU;A;+&b?785B3Nn@kSo$>;|JiClh2%xR2Jst{E<!| z{okL>^)ohnU7+_&h3rAA7xIcbm3d1Y?o^REeY6FK63v*6YCCjSw?ymPOq0pIu$j<` zjbCuHKHw=%e(HIiSNLpz(C1(4A|K^{hHg7uC6pUFSAMgGE;Dl}T!IV+g~uZWCd~`3 z*XkM=NGZwD8XYIQ41z|MRZFtYYJ*Fe=9MfN)(4E_V7Vh(9K|EmLJwr}w=(%KuBtG6 zFc$BjV{V%~6Of1Wqve9uZVI6H*5g4{S51^-f_pXX-=Ktw#k*ha_fYIw-Xjdwn|!is zW@`+M@y_dsY zcx_LBN@ed`!fx%3QE^xQ%E9rcbVt{1`+ph(D$>i@x@^3gdvL*b#Rmvq@VDfOhH$w) zm+xk){`wQG?0#!}`O^j|yMR^;F6qEPEblOpOi+mcCXR9YIge+1t>9KAeLC)BEE z8q)~{+H5i3qZ|=dahQCVTzdwxd0S+pM*w+ofF7!aTYi-X#jz3SZlCGe&P7!!_B(P+ zuSz*4=OU+aD^hEfp5k_Izo8}NfI zA{|Q6H!XZQUi*dkt({S0aJUxv4MA!T!*L*?RD)UV8AvCAvO5S{)s-d0_+zQgO4@huF zS9PSrXu?0jLonHcZ8Tc%;aMCqf3JOD@Ke6Pm~xpmi;K%%FHhF2c&nOySN^R#mHEj8 z*>Ul|l>AK+TFDnP*$>5|cXir=%PNUYtPK7hZdvoO2t5W5gL9Tfd#gbbl)QDICAA*6 z9YJKXjeRnz(~gW^|o$xtuWOjs$0 zZFB(aMR66jtIjRsLhUUgII8;5XI;!E8QLKZ@+4p6lzwJh_bTkGw4uKFKz;|K4-tM+ zxjfoOGhvbx8yegd%!!xi9>a;oEVZWoAy!d{Qjp(D^bGL%pmw0EIL&(==gT9p?Qb&s zKfL0G9`fioWNzXPbEx6BKn(ifHabw4Wgzf4c@^5cfmc=T^ai72^0H`4DI6lO}U}Tax$V{4bV+Mhe!rInY-IBm4&s> z=9s>>rN;Oj8?aSzuLK{K9nyJ=e+#h_XANNMPm ze+$NAuB+BX!<26JBh>7{Je(u(kgo8^P9C2(l5P<=ga>>~22jT6EkuwOvn&ml%^_dx zHNbvUo$<5}`9zC)u37pg4m5fbE_VE2##E41gw3Ygm)R?9A$9p0&le2xk_?W2z|E?a zM*lrL;=$gPMe&YiJl2Xz)656D?oQXwRPwbDPPi-Xu04jGB^L6a{9#??S2JSoOq0=! zfxCAof`>|i>Cuvz%xKoAuaM^dgulCTEX``pMQrk9hSUaOPkobbjE_6l7&A|Ww002ovPDHLkV1h3xtPubJ diff --git a/base/src/main/res/drawable/profile_pic_kekero.png b/base/src/main/res/drawable/profile_pic_kekero.png deleted file mode 100644 index 1f2c7aee264769100f9c39c74bc38cdc90c26976..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62835 zcmV*2KzF~1P) zy-Fz+k3(8vJ|>IA(9ca~ut)>|^}#`PZ~&CP(X`EX zzx|zkTkiCFz|0{)zV_5AA^^Z3A}lN+_|wy~@4Wr?y`x8mgAoFx&vh+_`CRhr`8`BL zhz*M8-bhCT01!l~s~#zeSBHp*0D|4_d9F6s{K~Um`ogE4yE;;YNJ?FO4b2Du z0T4hz>Nv^$=>a@5uU6~DV!5+3GR7d{YB_)VZ@=-NFhcJHMZ5b-DO?Wsp#5%1g8aurRZTXs!1z zUw(LY+B9w4RRmT3iUrr>T z0s@e2l^_TLfCvf##Gd4%#p14}-~HuJ7rpvtJ0}s}w0A$eHG6z~|JH~9>C0bv9{+y?9o!&OYCkzQnKk3mH+{XFppX}f5h%#e2+z%`P|Ou zw%?D}Zj^fdxtrfT`S#J-*`=MG2;YRPXs#l#n7P9>5h99gc|CZ_Q8&lVuj9E9kRP8L zn8?DdYYS@u==^TFMifA(>QXC;t`bCmJxDje;#c^IuU|9)%mScQ_PjsmmVt%XU1(-f zfQZ1DFrLR73U;GUgxA&-&m6w^=9{W0@uz23BJBO~hj+gEum8r))!`|gXI~V{W z02M`f{pQUde*f*Nu$3*4C>bdL$qq94!Pszd|8ew7w(nca?xL;Azpxv#T_59?K>`F6 z1Y{1YW^J^^M6$Xj6hb5f5iwe)<3?yTGcW^06^+wVyz~>O5`hqc^Q_b_%3^irDv&K{ z9(d7K3nG#LcfAWDB)}ra&GG@5OS{ikgB~h^pAuIh62j!+qksQz{_VZ@-@h~(6=nH? zQdJp^IV!Esk!=A1hTKEUo4_HS3?gFWK&cBua@lDB%f?FqfC$H7E&vD;gt=XhC%UeY zDE9jP?(U_l*KU1u=lLtw%ChLf7Zw3Yge+NhLajA*^cv_XMr-@hx3B4m-rd?T;;#F& z_fD9VQc5XALKFZXMMmqUZ5NAW*aD=qbKnL^Z@zzF=T9CE#I-F^`Lo4dKY61}2s($K zo+Z2y0L5WRh(v%8{XB=`)%=6djy3IHxvc97i7uS6Zb` zxt>iI$H&2YW)41RrEWg=Tw}~fM~|Iz(ebf}u!O|))72r8v6fIpYjTdKA__pf{_f{# z8*XTOYW$3=(uZQFY9W5Xh#QF^gzj*lnHRV&Ff5V$3*Cm`X8!#)+t(WLQW zzA(1FU>E<<8*M=f@?!$P&PAv8iU&l}NDAP%S^cP4zVW%we(u+QZRh$`wD#{Ce%%&n zS<~M7&iDS)m%n0&>bkC~3UfCQDW!G%psq^W>qVlTyK-w!$q-4TN?phn`#l8CQCmhF zNCL+e*jN(qS!6XHuP2lA0*lq=jo06p2Y)h|xDWv-3E2W7z$_dD5R_6bgxH~xKOi9y zD#C80b`F5j03}5#HfzV#5jNPw_`wcY`J*vh@WJ^Im>EQ@F=x}s>G60xU2tFlffzck zPM{fs_{^^U-fj``zE#SMc6NU%)ch2i&c+E7L*MopStJsWXt@ZH0ge`nA2qAjf9>;E z-*}aVeN~pYX~PY^p2WT|x69?bU-|00fAdWU;l|agRZ%LXB2yqDZ8Tz(src~^KZv`L zBpq!<6Pyl2q!MdqPR>R0l>`xHB!vWt$Y;LYiCoE)Apjt^?d; zZ+`zf-}WIKjCKeyIYHTYMq#nW#BL`dMnQotLL@~J0tg`H*fCi)kQ9Jy$7P%5+wl4> z9EThW0|bgT9Z=bS2Dj3?4aC5GzsR}g=8ij2j5QT_Jt%CdUY;x_**AL%#jdq5RsIj03oDX!K z+=6o-e|Y<=|Ic6ajJbL3>Yz6?T45s8ptVwpw#AxcR}y`Q(iz&SY!}NM@tx=Hk!u0O zal#@HJTejyWX>WT^X~$KWDP~uetCM*KRk?L1|jb6>>OUbcIWnoy|OH|%vETyjep=fSV!y0bv&F;vr{n3O)IDT2RsjS8 zhJ^!JLNP+>0V+Us-a*ByaI4jPKIa?H9sc~fXt(Lj^=*l22#As-UThoz5+Z0VTHP5> z<=T~-zx^9}*zateaPH8jQqt(Kod4ik-~PA%$G-}T<-zr9b=8Z$f&~bHh?LeW(Hdnj zBkeKnRs;YEcW*okh!5ZULF`#T$}|R(^e2FrVhsS{z&S42nC8y@btf1ftR|BXLjT|Z zNdbZ)dg=KWhW)|gkMHhPbzzImcQav&nhQasp%8-aEDj>*9NuoD3UY%#DNaic4Ywo7 zXyD*H%pf18SCyy7(|fn>JwASMZ+E0vp(iE8zzB|(V`%4~EuaM|0X3i!0A>(C2{KzO zZrwh*e&c0p?Jvq)!Is>xeY*K4hV;=RY9&@Xt=IcIFaOdf%fX;KhnQ%P5D*1OZ5#5e z0)YfSIXe2+|LXs|^`j5|$t$l~TV%~e1c8)_oc(-Eo)rNA04Vx{;GA2pAAj&;WsGoY zYv$x5@|kSH!|dylv&?&rumAwT9L}W!A+pG7I#tG$!x0h*f;Hyy;bF5{J%04)+KwV5 z$&qDev;s2&DA_pKO=IR}>S$Ki%1=#sbZM*euRgW!l3t;e+U*aHk4|oXaQoTohl762 zLc&NMATR|_3?zck3a$W9QtX0(aXN25e!T3BuJ7&b6a8WothjYPHHU^0dAlG>r0hTd zio}?GJ-IFF(IC>Km(2;sr5N z02D+Naq$8JW8-)-Yu@?M$(3hced*;_y2Q>eYRfuSZrwP3Av+gAK&9BI*3fBPTzccx za%ZHCPM?ooje)!XAt5ucWT7w8CkVmcegA{+{>?Y)vfSU<8SaeAs@E(Q%z;R0tyQ#h zuxUUM3Bh~sMFbfEpjj@u@t+}+a$ZRojHjIf%nN#V$NkU6mdqSQ-T1vIq=*1JH$OQ+ zMBD3;QUGAIzI^G@-DWLq8zVFVh``b{+FEVQ8HbN%i)*jk?CtGpV<6FO%ua@5 zC@dmElC z7H01hM{Te}em+`^8am6>bt7(2zv->ccIDs>h102K*)@K_w{HCCQEzv*x4Vl<0U~L2 z?fUiL{CYB8&t|@DL*NiRptRo4mdjCDU2uH4+oQs}f%=o6>Bwz9+4IuM*2fUBOBlwi zK=d0%>yfp`C$q19_1zzRc)za8!~MbDs2ujR^KLd@t=8>g>5Mi4`n|i&I2`;B|KsPY zs<&7y*6TG9DW$a55oA#&y@0cy33l5_=oH|^v|Jnf`$rG2>>pHCzjJbQ_089=-@KVb zrhL`86`2Dlg~AebfNWU6d-u-U-~Yy6{N?1q!=kVUJG*63xVDA1We#OoE2Xk|i&R{g zB?KQr$ZSObVwNt%3l@=$?214jC>V(x0Wu2aktZVC9kj7psqi7n=t4w*0108(eM3T6 zoSiD8i~axz5zuI3v@u2_;_A^Oj&fOGAN+i^@*!xQqbk`7@1Dk`^2Z>?(vs85TWUsk zBFO+JGAH6fU}gqUNP*dNK%&A}L=<5H_2GvT!HHJKgI-C*%hkerrw+#xJ~O z<-Yz`e{Jf*1z*;6UDu=0=<3z0&ph)?gqPMj3;ikLek$62BCv4F2Vw3B0X$yJ?~cdi z-rfUlU;35LJonnGwqI{(NP5s&@DPIJL#NRhoFAW^{=a|z=MR5$YtXA-dG^Mz*CRp} z;Sj2-(#9ZBK4PL{%10Z0RpY;%OBWy)~+c~Rjkg{eU3t$|P zidsR(H3cJ~3*$$RdQDsJ?q;Vch*S+nz18aM@nh$lb4~yn=U7-c%S9N?zigLboVT6n z5`@78ADBg8?OH*IXJU=kS|bvGASt6&p$z~Kk~LbBB0|g@j#SJYr!As^h~hbi5ID+& zS=>o2etfhJp>^$oe8i4gTY{LZ?7l{Ypqg> zi2fd^>DZK+fp@X4%-iPD-tLvxUV+iz@W!*I*SlyKH=IZy78U?AQ*KF_P9`6`^X~m0 z-)h_D#b>Vft4c+oM;H|rWkE#ehH0c$9JXl?Yz(BZkP^wnS5idCtWx`CDGhVWrF6C6 z`MrriOaw*oI+7)&R7_nXQZyqmx(y-B&rVTMIT#`l2Oqo--c|j9cmCw@V*n_uT`X5F zu%-=uIRw1L3EGYfNIiR^Ig{Mxlv8zf?p(%P!3Nl|I7(b{UGwbClq%n%Y%j#3CA zN*kSbz+BnTsbw22HtIy@1=ko#i-<-nl;Xh7d*@o$I_F$)ZhUlf6nO1gGVe*Kl#UKrWyQIF$otM#C(ONg(G7idP8X?BPRw zHtz530<-uK^O1G0XRcjy9IQ5{ZhADGULK7yrx(xcm_=mWw$6K{2$3pVSfh=xR_ozV zE2Ro!mC{6{sFP8NQhtYW$_G1w5g$UvPsUJwOb8vf=!RtyCSy;dfJEt0m|g2aY;q9* z&`N2g%A$Z|(LtEw)w;$tA3yl1TW(>JF2N7ZwsjvjKr9fgHkr`^T^&o?o zS+S76|Jtk1KmYtoFTFGv z42q(Flq*asL)tTo+@Fq@R$abw1q%yhc|Q5DiQKoXRs6>+ZPWbdJKz1@*S@}5u5Mnv zQkS_-D<|u0Q4lH-@vY0WC)F$jAGUFDAutyEOy!XCwt#|(H?Cjx#`-9QQd%s+-nR&WcoQ}sJ^3v67d;R{f z*N>#v7-NjFMWMCQN^5JCQn3j_F1kra=FJ&(V>HpUjRw78vk7{s7Z5g$kcQQH*NG>NM(PDT4Q^ETDDFPf5jhVzvnQ5Kvf=%BkE~Ps1TFA{hfp zk;Dd(trmsclNnRn<5YO_CNl>PsXkOV1oqyqn`SUijbbfNXN26{5xlFMmH4=n{xpBU6ZR>nszgVwVO|xv) z-iI`X1&r2<$>h&g>mPscqwCK;tC0mj2vP;9=WuT}EiN5A_xw$}GnBG;@(j+uA|SDH zMFNN82M_-G|NeLLv$M1L?8f0`os*g(;+<2Z%3KN|ARH-G2+Z6YUK(6Fxc%+#O6QC) zkLL}NK>#>N2tG;eUB(wcB(yC@F(wjS5zN8nCVD^b0nmc2jcc`gi}t*hLMSVQ8SA^px; zOEzlRz#`&;M}Tho%gMCt@qA57Fjo~h}2~{ zsC)4h1oR;Yz@V=CWz{(M;N*DRHvRp*U;Xu8(;bJ2=dgDE&SLiJXFoGM*oS^EPL0|o z(cA1eopcVJ+r{d=Z~pD^g9kfhHCryLoYai?YgemYudj3EtB82-Q_UL7>HPo~{H|y59z)XZ%=|Y>r z8m+XoRvT@swZ_zgVNsUGS|ZhHv0b~!#$aL>CQAbOmf^y!KiN4)!BYW&0u|kbB)=@o zF(Kg_7uY9kY&Q~YGPRiWIROOkf{!&^!rrqFJ|bq>MBCE@=Gk)5IA@f0-uvKHN6kw}QB zC<{_58HUz{3?l&m=a%Df+xV2gqfLRe7#yO zTj#v@9J~M!kujz$ORaTuWZIaz*Q!dCeg)L zS`Dh2h{p5TY`HX5`GtS>rQiSJ7hit)WozxZaBs<;_IBR*#3!mt`)KXDGDL9O%ns$EkJBW>jmV9TmTd16d^wC-L$bV zd&Ynf1fA^=kr3FqcD-)aO(P++-g)m4KoMzcmC{NnrIpsYEX%U4%U<7>rL9V3G$2Yh z4MRWaW4F#}0-Nji=M5ZCA}20ZUs5z@Y9aqQJb<@2}ccM6^`N~Uu3e#i zuaimZZiz&lk%hTgt>69TTl4Yc`u;&->=rYZ=oFM8TUhUdb1eskO;T*r1s#?y z0IqvA>V)DukFv#ux8SwS&Sf*38Jzb`+s@{*Rns6qVT@K<5t*VOq6jHlSX-2JzgG_i zy`7y{m37V_W-AlveeeOdidQ!$Oh`aTDis(5ND^ZO(^!kd6chSTKu}T{F=qLtyP^mt z=-5?amZ=e#SptWqb>r!zY1-4t`0?@4WIkW4mO;2*)xEMRjj4;WFs8J{V!b4yv)Sy~ z!^@3p3uC5>MX#({@0aWKs#zas+@wrMWy>;(4U0RWigU}tC4>o;wylvafE^?LvE zbm1+?bn{$WKe;7T2_TU&x>>DT*RGbU$$ajd zi^EGPRTgDYSW-$+8uhj;dc$FFG%`hDi-MHSG=Cekc<$f=h$s}Z@tBPhcClLg)DtBS zKj8|~2v3BvyPV`AH*e%pFqRlRuGYqc-Wx|a@h=0RrYUO|J;ph{VRvMu<3FV6_z}seml1tKLKsq-hTgsul@VK zXjbc@?wvE@0)YboX|2n$^4{lqh4hcNk(5akY`f9(y|HCCo&ZxOG!T=uA+&9~ZkpL* z-bOqD8bykfHo7Rwx~h#e##&nxWv_3mvZ#ByurY1YIr)s~PJZMIct9dT6>D{}y3vIN z=N;t+9s;mcbDO@iMXdz7nPzEM9OHiu9K82U+fHY*@oaj0b~alq?@!iqM?yty^bqT| zIS$SjMd@1?h1w9pYQ1KG!kT5%07&b7U6gx+!Q=5MA+4HqRaV9r?|tLlsO|xP)>)VS-G}*^yuRstlQQoMT!h!U#8UUZ3xkafT*-m%XWSL`1oWpIlOlDzy8xd{mtM0 zt>JL^Wc8^&SuJOpKK1HLbhwXNLw5QApojo~0U}e`^shj)!)Irw-~10>e*EF>zAY7^ z025ND`^IpQ$Y^6Y@Mbh_NYI6L#_k>NI=K5U@>FO=U)8p4bk7jwk4n!u3kq(=Y47I zsMm|Jj}HL?Tdf-J)~zdx@(cg$^MCq3|4;k-`_@`GpMFlj;=(@n=4<-!0If+)iU6^U z4-n(5TRg|LV><*N9zS?=diP%2G^1XPNC*^q9{l1)vbv zXhlSY)`hi;)e1y*`-8$-tFt0nBaVALPybnHvKuA7tS(cywy?^?J zKl!7}moNW4W%By!r`{aC`Z5jbY#TaVAtgm6rPjB)oY_0K7*D?Omw&Z7JL?riZOf?T zaMsOpZ(&{6`Dy29&b9t#sOR2$nla06GO17@gwQl?+qTYo0WsPXWnr};rHnO-R3z_J zU6;Ln)gS1xz{spO;*xCjdOplnqA*`WcC}l1l=M5Ml{Pj=w!heMrhJLnOiN_oY5uEs zZr!-$Vl`i_9-hsP#Ml89827B_Xwby zqj~NF5<+m!IoCSp*G;39s_WWXYYQ7)o{nWq#*~ARE(=o>q&4DZNrjCy%|_(hVYXh% z(Aefxup#N#QOXP{xG-t}HsfP+36VGjAC_x(ws1dsI6gTW&sOcS<>{Io151y}T}`Ui z+S-zoRocdiEQ-~8C;%u3gs2dVq2BIQRBAqcuv(0p<#_kf3urADSB6)@_})9Kwyg>( zZHWlB?lFs2WQ?t>jRBTX3K3&HDiJl#34j(^uGg*iglMeY+ueQRbD#ay-~YWU&s)Q7E6sv@@`P}q3 z!i7|Ky51ZQ!q#bJ2N;W^QgKT*Kyq=ETpTgk)qw~|%!w}BFkiJNlf{Fx=7WdRlgT77 zXj73I01+2VNS2gBP=uYz9}gcF&mhc&8vqIr3B)K;q<5~`aGy>HG=?hHm(xE>b$ z=gNcS{PB-_CB-5 z|MAP8c(W*qe-Kg$SWwuFpNsqmk(s+G(i=A=0wQhG9^d)sKYjC?^VxjAu3g(^8z&G~ z^U0|SREkK!R<`cKw!P!-=-Z<_j_#OJ^uY%o>khvp|juq3AT6Zc+BFx^C8?hT7Nul?mjv>o6eUl zy8xi9?p?B~%oT-<0tl>tDUw_y1`0v|3IVl%6(p3DbcR^%3Pc!~gVJP1NYbnq%sf52 z#~@OV%kPqlHh+uVGMNTq0ujA@(3wQWr6L!Jv%mSwNs-`U?Qs-CTDWege} zo%MD}n`8?Fotc!p+FFO2&gvjQnjP7ZJtT#Y3OS+Ew9XMdIrrpTz#`(EUp4OGq`CWe za_7<6>7t!C()yrusmu;l#>7QIL}J*DB7^|RyD1UzLIMg<$8jQnE>_JV761x`lcI_W z#Sn_16Qus0v31+5i)yr9oCLS-_4kWQZ>+q#Kbh^UkT^}4? zuKImj*SajDI1V`h*V&KcDbf^H*$4+4rnp7bGiQpJLIfa%+Y7~X-h1m17q*S+6u4>q za_v5RG{5zDar@!%WVvE6NJbZRQCd=3kbngsfN<2#Hu>uuFR>6y^nP)8P|O%Hq7y_G zO`CWkR%#M~1z^xfH3$o7QffJVw3waLy`Addh56A3(`Gd)ZM>A)T11#Hm#ellM4A*c zH?0#Nn&pzT?j7v^^6&o6%Wu3s7!3XqlcY5$6~`!ai7MER0EL->S(uyk`r++6Z-4i@ z^X2lH{e#oV%m+b`c-gs1G>yYlfC~U1v7CI1U3C=YT+Ek=ln-$MERd)VDNx*_a&W9HU5=8(u>hG8j;$wza8h0|;?SAcqL1ZqFM!mt8Rsc3DO*N!E{?J-wW)GOz#v1OygP zS>cVok^q!a2qdV0aJ85!tq1$pRP{lQc74D1QE5?qussUXf(KdsEkP^E2(2* zoY`q$=KvAM`MEH4TZL0Zh{D|Y!8{zcfkVKYFR_4?u6kM{?|>3pscr?a`RAf;>wA}Xcgng%Q!^Sj2_IN2-a zIJ&7PdDjRj&CQ5tRGecCLTt3!+1syocj}#8Weq6}TZ>)CEXtON-9lBOha0xFZOkyw z@DveLDk1Y6G2cd2_NW5P%+7h|+U3f%?QA~#;NkSG+b1UrC#pxLzjOJD(guK#h&iA* zNha#th~1LyQBV|uLeK=1QmYhzB*qO9Oo(Z=kECcj)d?a10LO)tBbP)(1R-QZLR6|i zEu^$pZM&XoTUNt;5t*GmtOr-a^5L}gl?JWUsMiOOrgZ=!L~s83uYKuX{+EN%=;uTg zBv;t=H2?r|TxSM0mLP<1c62m;^k}cwI~||-w$&k91OQT~lm-w)3CvB?j7B>}QKtUk z954kTN)>9c1DPcvvOBsqrtJ5Idwcc%zP9#U4j8r!PyW?K2Jzi)I?#ol1c2!7w)eA% z3OC$uB(d|yL&SI;M6Z^r|4bPMW3_=$+a=L`~g!GdSOcw z{GxuKDT~htUZTq*KvJjx(P*Q|5UL>1dKY{X7Xgqw+zE(*5K%=EMg$~+$N)y6);1Wc zyb)z={DylU1yu(9Qp*FOD6|NVbA+TH#6Q3ZLz>h{Q% z&5+Ndxm`CudiTBcaz%Adn3u~{-xwkl34$Vu*+3ES&HVDFq; zt=e_7oX=;o>HSBK9-W*VPiH|5*41^rKNzSwuV9wuz7o>8-iTf-bG#h)B?l}3A==M~ z6;c?N%G5^bqF>hgy@6GlnWoM9Xg*yx5eh@~8p<-%B(v%}e1ONspw9!c3Y9v`q zAJlcf!a}Jl5?s5S7scLW`9Rp8Odl@W_Qo^MUVZVGX>b$J{GwJ(*7<7VIA)OZC}R

;_RgYKe*awJ!NLrb^RtK+}Hcs#FECK)|1xeCcoJ7PT$dVfw2fI7H z-Q8+uhw@T3@Fc3TptRL*0)QA7yiYbP)!S|8F?QI+j8%!;soRZoYC&M%G;Y0amdoX0 zF&>Z4rjy6VC&$y-G?bydBzteLDuYm($rbCAgKJi^q8yS@TX4=ZN-1;8jm(G1BoGNA zk({(srpB30#zWlryciwqNF_MBd@Orhz)T9tecUG1?5!tT!OtvaROVYyDZM&Y%n$>Ex zSWITqE=I&stpAYoUJ-RLMdCq8DNWNV7Q-U{Hv(*6r#H`vX!|Al|TvB%OJd zG7u1nw6WS$Wj*Tk4+kTyDc-6i`3VF>iolA~?-7840K$Rl6__l+y=YisJkaBy&->yoqv+^F-g1@pKPa$aO_0}KEF!bF;KXo%Y^CdsDV zo;q0ON*ICQT)S8{i^X!Wn9pWslgZGIs_vX07tl2Jv73 zNN9>`HM!@OXXW4!i3GCriioHH2o)8!+^Kq_s@-U5fY$^gaVTf&v(g-OFAGT zD51at(6jdXa1TLF=Vu^{2qGx#C9o>3DRj5uw+N6TWt0?RDx;0IV2oAkW*L-^x`cb~ zg&#h6RDS!d`GQ5@^lbXF%_ zD9tDB25A!qbg0_wFFHI9EF666UDGVbljUqWpU-ED#e?J1MH|K~ovg$Ro>xWRgJDoU z00Rhc8<2-`uAYOR~Tu z900(z?MENona*b3xu$8>>s4ADAa-%;Pl&0QvMj2qLIi7zvMd20_z*a_;1y75?Qs8~ zx3_D0^;W&WMMoBezVmR~5OySt;#Yq5B(2&;6(Ru#4sGk();kwkx13B@%f;Dbd@>$O z;PGPp{>ds7msGuHdlt|*pdi4!Su#GhP6ug;`$g(#xFTz+(Q5i|IeWZ6x&mZ?%uYQx z81yTXmMKapD(NJuR0k93QM6TAkcy5DA+FRVqKpcT0XcverElz9TD8m8Hz){5u5Hvx zmwiD(Wt7qi0pem@BG@#IHo7jAMpc-?tyfB$;DWZcHyRu~_sYE=e&^1|?|<`~`(OOx zZ``@_hy#Q9`EeCpAwUvcQUuNrI5y|}=<)36II{S*ZPSu^v3W%Z34j7~>slfljz(JR z!WO2m5~s>IAA}2Idc)!1;J^+C=|)J=t!+)`N&`3B&h2zc!f^szUQjl!X}j@vn|k>i z2{H$6m#cQQYL`pjG>iFsKA#<*jlK7)roBH|jbSv$%ecR%5Cb4UT6Bx^68G5cJ_)Ox zlTr}%BOo9mvh~i@`RVP+!}oXl`_De}=5trCkuayRc6~CQ`M{gyI({x-bwX46VTfrQKoMD;jSV6w z6@03CmM#(7wLEhOQWQm5ma)K*0|SEdoJLYQ%a?RjrP}$6z7L5o%FpL4RVH51 zl6Hou%d}sN?f|k*B@x!axpuW`=JWM(xm+w_tT$b*8qp6Mv!JVNcZDnn`nb{?h={OA zT8VkH$EF^g{XlDQ6ZwyS`!V1m)gn0(+5A?@9n6=Lozy+ws5NWNVU~c0&HOy(H zbeK-(gF#;@g`gGc5WFz6pt82=59+<^lZQX}-uJ$Tup93|^otNkV^4F~U@JE=2VRdS zl`($ZFtc;508qR|AWG8tgaM(j##pPB3O=w3NYpxSv>w#;&edy0zX$Q=wvwy906+wj z;UUQi)3%}71cI2(C}D7OgWVX7s`}MnO;$i%5t|Vmu-W~XKC?ZJ{{k^^|x1d z4t6d*i$xiic$BTxFAzWxU=*J;p^l3{#wJe(5(Z|Di%oJ6U=e0Yf?djA9`tr*tC{Bj zNUmuBL?Oo2P`YKt5YfA!vyfaE&gQ5J}Y^hz3T3PBg~lV@N`x5cL$?ESq{s>po*5ZsD`$v z-+XcZ7YHbzHK{_8Risc6GK2T52^3M&1{Mh3rEEy7ubX#?Af=TTR=bvgj}}EwU7=%NXk?t^UiE} z_VDz)1Sv{9SmYw4Qinpyb#$sa<`6PfS#5nhTAjH1 zo@<)bY&JPN`}p3+r<3VNk58vg&&{D7UIWty(jk>L5CW&2rUu-qo=9oC*e?kg9F|%Y zL`K>2+V0+sy}jt8n1vZ?Q|t~8Cbs{X*RNIT^*{UaS0CQSLt74Z4z$*Q5btNfIlY4b zwop_VQe+T7WWDy0V=6=r(6%iCDbx}3?AiOUS>1|RK=!Lq>)OS70fH!MJzubw;Kf!3 z^U#mkBM(F<&B_^7C{#rK`Q$@wN~cR^Ft*^pgZ&#b_W#@e{(tQsj6VCBPyehXqfY=U z>+IdOUN_%(>u=`|k0$r-4~C<-Lc{s>u@MoK){$;2rLDF7;Xn|Jq8RS&?OeNNstRHk ziga2hVww-Ifzd^&A#c>(3ScwV&&yI9njfd51?QT@axtIZzjyEbTet3=oUL)t7SHlv zM5Yd85mn4qAqgoE$@ggMZVZO;_N~z@DMBUe0lPqf0Tqx!(rsS8v$ORAFev*62hSld zUwLt~{@g$L)_Zs6M<0?YMxz~-Dh<;oQ6Q`xJ8NnrLxf1oWucb~=Y5Ea@qke9K`^gI zwNIYa* z#W|))jMk>K1#nm_r+|b6rWzosSb(thA(H&GtO5WE6t>vge{OvCmbld~zj=)yeD$4o zmuSoK^*kFYVTU5wO5{U;GFM#D*&JfK*YB}YpYF7wD4`qiog^|nv}L>-}%FLe)!S5 z@BQ$TpLnC(+==oZ0W8^)Mg@R{0IrNi?P_fpymyczlSCZ>5HT)6Zmb;)2Bs)i!9REH z+TPV`<=$?(4~KBfb`g>g31TjPKR01ep0HU7Nm)qPx~v?&robY2=e=u}^Tp)s?Dp;3 zckbUinXVR6!{C)*h5-x*MpTfwQ(R9!FM3UpjkBNtL6pFWL=>VaOtDi|`*kfK?>`Jp z+aMw-lPl`T3pf^~@@*`lRx6}!Z*t1h)3d8r4?p?xp>yze_dhy2-5*@L4y4jr7y(q>vPnRptJ>wevs^Yys1Q0PU=oriT5KwDfm1VEj@AdnRLvJ{E>DgxoSFVyZkSC_5 zlDe%MWp!^ov2EBFKuZ3IY~9*u`;&3)p0@F=n@^?>A3k{Zhd+39ax!y@>dS8TwV(zb zGz)O>L0H9Gy+f?+i9eu;Pz)dl5Q7rZfGS$4VNvhcvLdZ1-Qnlz&cWT&M*@PV**lF2 z5tu{cL#5DKVg_OHj+sU4aw7G^$43Xd0|W5smqzmcbM|IUb{1p9y|a6%<(&4 zy7A61Tg_3cy758oAILQJ0HU0&&2IE#4gTzZ`0xL_<>lpnS%;|G z;1>ZFaK4&OfBEZQ|L#}6e1GfG>cV39Bj+8=sCiP9Byq3TOOmW%pLL3hH?9|p%M{09 zv)Js$oXHxwrEFh=2lJ$LPRG}l@^1#4%x=R_oqNx&X}qbVzIMQ+B z8$xitfhZ3@-+7;>$<}(oEOq0KkE@e`Mw@$kUbRw+^lcP{o{4iENsA8xKLMNw8c zczx;w0KiUdc~mA#DFP@$Cor?C3*{7g=TH&PJjZ;XwFS@NF_1-Z9%tinq;%9YWA8ku z_F0F@AspLKO$4`NwzVfVNl!Hud&}%wydMd5JSeBnUhI7G$=yHslOO)eiY*NU-~mL~ z+Ova$qobpr{`Fr+Rh=gZ%m%;erbYloL`wC0y>7QFL>A=ojq7U{FPTmO?e*8zMmSs8 zH)nrm#TL8{Fo5~BIF}r}MiAOdHw3(A=WS!_>Gb5_@bF;&>xU1IheOtxk2Y}gii4yP z<-G*I)7dUbAR-c{QHdgLP$|;7_3trSE0bs)8Ivp|MIIcE<{u{``}RB(QM}ygz8ah$ zfOkzh(hfZsfgG6;-7_KCJ;M%2!?;J#4HqtU)*>`QzIwzqr5sy%7*fY0Aj5;v?{FwuT{E2??eFcp-1+jG zZ>qZVDyQCcNLM`-l^Ey6@q8Et09th&YDK=%M67~m8Yx4hKoKTMvd~@ZrkN(CP>VpA ziUom|wj&I{1|cqI`O$P_oedc{81FUK9v+u@rrFE!X?=KHg*O)`-KoTlbDAjJ>m5yh zbnD_@efG`vlP_+*^G7TZ2T@Jv8(09qH{QES6bYdw5){h<0-8)3=j^&3rJ6a1H>AJnQv(I*I^EXX&kX z-|H+d>o^ubo;l_|c^9H}#=00+Rbn)H2SB7? zF`|F>?$&4D?2LB5Ntdq!rqEhfhckx)B3`_8qL7S=v;q&#u?T4uPs>x2bgaxX6mnbM zza}LRG1ewEX(_@wVsA-Dp}0EVM$Yen0MW6tfN0{v+Nz$O#rZOl67hh9CV{y3i;utf zo4@_|-~GuCJDtuyD{0MTGvL@Co}Mhm>9zIs<%NYbOGTinY7pslx@j6~6HS|DVSWA9 z+jolPCDOXhv(gszZvf%5cRODN{6;e>nKN@7?|8!QsgXL^<`Y zq@4>wS;?w$Qag4n8r^~hAy3VuiPWOUG~5VY=x0D%Myj2wlL1tdjm zefuT|@+cnE6@aj7lu5k^1G4b6bOBYFUHOPAN;~VN9RMJ(2zVkeini948|Od%>XQx7 z>Bj*-C^F z+s|m;j!IZ9+m`br0Nz{D5i{GmG_gfe00cmQD$16s)9L^FXFpqCU-{^}-}z_FL+jb; z#Hy@LPfo|1 zY=I(qebe!qJoGG{f*v6dGJ88OpFV&7#e@50Bi&ASY3rSdUh^ugm>LJA_0Ef%H%5pU zHm|hmCSo8#03;D)KSd;EjMAhCl_o?Kn!TRjVZp3ac$EkQI>FQ^m(qA-!^~0<#j!TE zNf3Y#JwW4}f9+^DJDmUm%9~=+9L=#3MHpq-;?*1djZG6LAK(2nv3_~|8mUwSXQBrP ztS3QhAVr8qBcZA*&}gQklcKu@7}O0Af@6L@W%dGMrMq!%owc@dbs44InJ8**JsYOR z+i#OH>>FXpdmDqpXC)owy$vJ{G8!~-jA=|xNmF52u?QGanKlz5_8dq7 z$Idg0I(Kvc5k!%SOkP=*pa6xyStr@zQsVn>Ie&0A`Teu6)4W^sE)Xc0nK8?p@(k&P z1qBsgWGIf)x*h}j2&o3&{*{@b%M2ZBA4R0J(UDI&u9=Di@@gJiFponK5ClR6NFt6@ ztW45WqqMUUB>60iqLj%n?%nDOOB{NRIk|LI9<=2s{Ho+o?zDmE0w7ZT<)!!Ey_qI^j}NwSMVWL73BiNV8%htHO^_ghQj}%c+1asy zj8KI-V}6e33YkEsBZ5)JDBU1=SCdW<<~b|8nb%cGxPK9qc&|-nqM{lfnK+v@l?*5< z&Z2zz(X-v(-+grR=Cx>Mk@SxQ*5k*IyG3^?%lDo<8x05jq8mj~S=Di_y+|c;f+OqQkEDC}fNqg7CN+;UX0ED0rJz{NZ z23GRA?FRq>^q|MJRCS|@G(^9SVrk2BisIzLFg+W?D7R z?%m7MG|#i(@0iB%-r4D$_di%(Usq8C2(8IPYr_y4k~#M!bA1b(tA4`wLtWmMnt@aH zT#ZJ9!=o2FFFt$l;K`||?q>hW71YU;ec4P;2Pge3`QdloJ2@MCby_OOKverPk(5$E zN@=5vF-dHqc&Xo6Eb>fi@!}dEj7IY)inOw#NonVNQ+cC3r!gP{p&+0F5pVzmpamh| z?bt3w396kL`VT~yXfwtz1cq8ibu;zMO3~{S-9`kBD?h0lRAGaqin0tKT-*YP#+Ze* z{;kT#QS$lY`zx2;(Md0~Jpjmvd_Gh|agUg#9qSrxP;9#bDfE)C-2@qNA0n>-uy&ii>>f=3DC*FOpI%RL( z1ADH=twwf6AVE3g>E-Es> z3W<;uC_~z46DLWs*z0sVX}6mq1BnQL(&QT_0ESegv~!-6aki-|576XAYBVvh6-mQI zCFoeZ7YYWLh>Re>*p`U1r;nKENF%Ycs1pDR+na^dx7IWWVCAW7Tss`7H7bpyA(Rf{ z1%MPTt#wJ!$=TV97k5`K-iecr17JoF3ZNRc?M0}Mz(ldNl_CMHyhyv0M*x9X{Of0p zfQSk(Hc{;oPI6%{O5^KKVXm*7>n;IF3p=Cr#`5Li*{cZAA|dH^y$vcAWwx38`nUJ~ z)knAAefRc1nzZ!6{=s1XP+q=b?}JZ)*Sc$>OV@8~UAdx-4v>j&-Q&Ye{+gn}`F>HE zceLR=Aiz1O7ytomRhOgj{?5xM&!0Vbd2ou0b^m5!3P#NyoNJJ+mIkZwa%Qfrt)@mT z^?S?3?kk}}Y4O^l7Sa^OCQ3S4zSzkZ`k6_!05D1rp9GCe2#QqciPV6~+nSWAOIw#V zF%cknkg%1C1wo`?ZyT$nmAm^eaw@aV7Xbxgtu>MJP0+ow($@LJyn)e)pa_CaYU>42 zh(H^Rld7&fbKNvmRh5&8b3TaKE^I6e$HU#npKe@zN5vgP#h?%f1m*Q5Ac&}y@!on@ zE0Y-}7RJ^9I@|)Uk8r3%j8c(~YZ!TUBoWN7RSfROe)f4v`%OR)?)5k8ayT6wo6ZW6 zV)7ycEToJ|x=*+Fe)HSkU%!62D2jh1unf|I^N)^?@;Dhzr<$nK?SALO53gLiMoP^M zeQDl{F(4q${1;lDpnd%FliOypwgqQkYs=y2VE5It?d{)u^>r1kVE=uqI$o4#L3SVx z*b%V!dUX8!JDa`rrM@9hY4pyG&1g3os{Wo6i-|>DWW_>}r)gvo9cKmzy|93|u(vWG zb0W;5gF8GbAaYG*;&e2!SsIbn&a)sQA+U%CAVee<2_&O{_X5=RPVK=qO2t~2z|(O_ZPRcQX*Kp_QWy(pYxpzUkWjglF}sA zc;%|n8fH?2P_oR8N@1pZhnYGGL|#As_Sv@J zsCZvbro-ct{k^@+@Jj*n^rBG(IH&LewVm-p&Mrp-W-sY*`Km!{bMj1NP&=cv1;Oo!iX*H;zEC^zmTN~A+oc=zN+onVD#$M@%HoG?N^U6&P63iNs=z4`9hvABuNp+MOe{o zOsuu>-UgRxg(!l4cAnek@H)wYLQJBpF2}+i+jUX1qmL-C$ zM;Wx*Mh%PckbLOisB#2Sj&Ph_7AjD&U2clAAb1J+aG?Qv>}`gj*37C zY6*~fO9vu|2LxbjozrBl%xF`abGEL}4i9$scJJQ5e}DTGcdqNL4=YF;8`QUq;$!j| zxv`LA0MBB_n+4sxe!1usI*LT3uM|eF)|aCzi|eDBMy21KdQ}*mDAG8SlZHZ@J&0qe zD=RfS?*SmwW=u8d$X6rpEqk)1!y@(`ykm+~TRk#^%)D?Bh^Q6|LaJTa(8AS8lzL}@ znUo!u5PR>+a$HyA(fD}(@Y$=Khq^0E9i2qx_Kml0zxSOh7cUxP+9`oBo#7o@ z$LvJJHhgkC{pByeI38B<@|BvYae6o&JbU&zd%tnvts?IyX;14UO$ul0D2fnJAqjzF zCL|xI&GwwMUj!Iy6UBMpZQ4Jbweg|8M5Hi7geV}y-bG0_N{VuPloSi3w0OlJfF2d; zv_CCRfAO2U-}~;ZJ9plO*NyAn39M=`$l|#1z9{m4_0jjQ+`g^jxc$8|+z(r}CMbd0 zhJ^X`@Du{ zsm389+Ip(Bv9(1OjY?)L!OS;x#u!RF?_kBx*7mlARM;2fTpdLT5CxXeIeUD(JsKWP zrf1GJ!{H(6NNc&dxxTTuc>VnkS1(`H=anacPC<=CP>Tr15iYOf-}~M>pMU<~aBxtp zUSr)UI*W^o8%filSsPIx@y^ zyPccYuHSt3JyR5~_u?TS3y6e?mUAe=yiRQ4PzX04qKL3_GsIm(wNLTD6|Yj z*n43NYXMuV)p1?frm{ilqo{Q{NtG$dB2qm)OVUo$cvo5z$KVCLz_|_YB7sW*6fjCv z&Nl1~&u9?=MJms`z%Fn|VW`53&+|p^+}`dt({h}4SAb-x$Jf_Zd)=bn@0r$V>^w~o z&S%1fvRGThy$Is1K0Rg6#xLEA_5Rwql!YdYB8W$y%nO2+bLxaB5Afb*2#3R*ImAhq!E2UUkE{)qHKm= zeD(6bt?S?+`UeAxocE~c`sFKkzVp86cF`CJW=1WH{JKEHRxcJNi)L?N)~|*yuIuUP z>8oeYfAzbM|Lkvn`{fbA#t&Wpt)Y)fD;_`@r4%W|z`=l0H7F$AN<6vgsB&v#-9(Aj zk&Yr|B4wfoeWJM>pT#k0O#FhSc<PUwTGAtl~ z#^}QMy~9@*dKcnYHBRydrlq5ZwpQ~jQ9G|DP3fIu+qk-Rzzhi9`ygG{M2Rt_Hp(Q{ zR<)wKcBZ!0dF4C>O#w){80y350(Vrj!f zS*KWGB5wUGspu>X2PaXKE21Wh3jq?M|2tYFgjO4%P3pY^7NP(OwJYOhgT%AH9(pi# z#&r8D*A8BO;hL$5QcFB-J8+o6$=K%Gdm}sv{I2)kzy7W zH@i6z1;X@e&%&$`85kK95g??-jFi?UttKZ%M`i8Ir1qU$Jcw@ZN zz+Pm=#EDi0n+8Nkk+2iCh>7#Iw{T%;X$vT+xk5yl*%4saMBu&izN+idczkklIygIf zxwBJFONGGBdiMQ=g?_J(09q?Sxpi~pv#*|v%hBTURXvS`#S|CQ#?}h2ILpmYZVs2#};5W74J^q{T9k_GssWGD^p2(qDh_;Lc{ZoYLpN>d~PW*`JVe>pJsnw#Ul*f!oXM$7xj$;rv#(a%2q?a%H# zcHP^_l@B$>jc4x~1lC3;Nu*Luir9PSm<5F~ctMa-M5aj6QI@k0XT$i<$XmcksoyMuo2NKG^Pf5fRVK0)$Ypi%}DzM$Amq zm_)p_wkmCuOzP>lOidIvKZbvV+gS$S7I-AWc8GbFo&iAEDii_`w%%C~$g*ydb`+}G zSrTDk>sb^DV%0Qk{j(P@o;`c^=$nVOni|4RmYFDOynng#$~t>tb!DSlTv+TcEG@^8 zCPMab``XnX|LyNE)t68rqsZrWLj zOxH8#sqQ9be^?SBP#AR&_L36*6?7NQb3}v)kf^bCj?#o2T^=G3d%LJ^#EY2T`yX0#8Hwr(=Zpyz_Gy^>mpxJ_EFp@j0RKRn(B zZ^^`{LK8|sRHw9IpvO;ls;Ua^z26S3e4!t7i?65MUOU^iAZH**P+HNVFnDVx!{M{1 z&py5X@F!nwH_3(K@;e?Q=b62o&B;()L~)Uzo>`kZK_X?Mu$Q!(q#JqOiwvPub>o_| zt+ig7WMP-4v~DwG@&c7|RaJP8ZvSX+d*#Y|P7@?>WxY5UPObAu7_P7oQ34=dy{^s7JgKD(2a*0%|h!jzp6qC`wd#iP9NHt;Y z5t5(#D$H(147V}@DMLz&_e84A4M<_1a9jQhA(40l1kYYT@?xpeU6FdC(hlp`OGTu- zH~<7B9ru2J@A=D@JGXA-T7P>9phm3&k~2eZ&W$4DES!M=dGb@?Q`^#FZ z_6Q;g02yPlOvgzz95~l(u6CXt92xRnlQ0yS>1EO3unG%k!rHXgEyt|Ra4cbwL99%` z37xGPZ(`@iqq3J~5^y;`J5U@J2?g9iWdAPpce?+S zRMqQUzP5Q`vD?v_j!p;n9zT8f?8Rg<@!lhnj#MCFK}TVoTZ|+Cle!+4<-yV6Qm=pg z(#7i+FTTBQK6~=;R7Kt1s!}N-C`D;nSX(O-=VM@dm5dGkb7=-0C3}5fK?ONkk?J z#Z<6t>m<=~nlD6#>q=Y=CP74N{ctk6y(EkMUhvKgwKS44NlK)ERJOG2Y`w7pN5g%t zr#dQ}LJ_#O)~RZDRyA||APb)x>i_^?U=~nFvDQRNM`<-V$ z@%YvL;pbmJI5;|S&g&>nvP4HQY6aA~IC*bTP=o}~HkJhtWPgAE^yK8_%blwiHZJw? z{)i25kfar>Pp&7-WySS~CV+b^H2U%8{Tan=@zTwKmi zU)1gW_r}tU_Dev5oZ291r6nnCyhv$>daNT;8~5CkJ^}l8H`+qc84${pi{DWLhFolBFg|No%DMkr7#w zOCzyO9YoDe)Ntgdm1``6U}2)+>DkAxcKf}4tm47my(mhG?m9tiw5sc26lEsLvAtqo z4m!*};jnXRb}JIOx>P2?c`1fKt-zpNhr*03`~Ls{AOJ~3K~zYgl4)GU;?uZ@R9p`a zx}7ygn!}Dv@Cb;c5$V~>!{d{|%F4>Odc$DS6n=T&8bdN{jc!ZsXYeuU;)r5Y`WH}Of@0|0_IZSg?Rhw%|Uwre`;^MmLuPK9rgZ;Js^73(dIGM`1 z^LqaDa`0jU0ff}Vp1snstwxd*AW%&kqqGV84p;~^&^2F^F1N+A8{sl`ma=sL(%qwR`XD`>%HQ1R(8pOcW!qj>tq3#o%pir_)T! zyBiA^mb#t7#935>dOqOOk$v*~`1r6sJ3S+&#ZIRjjXX?14PV?l-Mab1&f@jh7(+A} zA9lK05m`Tja9)FtA)00d4GNJG0AUX^yP6q(G;jJDNQIp{ATVvpg|xT4a&0u+GYC#; zPoWM25KuZ&CfVIPy!Z8k8`rP=p}>0m5zL21gGRR9WH}uS?%sRwfB*F3?J;#Oe27Wk zGJ5fXGRsl`07yV8c&)3jJ%lLEOpM-VWOUoPg|1$_vr1w31O+PC;Z79t0`p zc`_O7?(a{gQx-|%ctH~(UR>*yqtmpvtd**&sWGFg>)rB&9oEg8sS4+axk$?<#Y$DA zy-96#Hs3qzEt^>f2q4s^=+Ht0pRG2uM8vf-AS~jVYBU)grAdY&d1tBUtvIxVXw5FMUWd%MGX z_YNLCJgY`FGEr)b^WyFJ;Q1F>-Z$}59O=%67LKWNYVNxc{u`BinsPZW5#W9HWC={0!}lZZI)C)288dG^))hfki2Cll*jSyf8wYg?OF*H(($ zeEsNYZ+X+CMZdpzc(`}<+G^GG?!Gv3=T*JmvULR!0fiVT4M{#7@7rb^#|a^ZS*!3D znS}&UFf1ewg0s+~wyijTX9n@p@$t#wb8!tyytH~X@2mjA>2ylcl#}D8Ivar_t@A8b zguoM{8Ni`sVHgd$8kAMl0K#YY4!`s6%F=2YM+y~yVKBV3*u8$a^TFFoFLwsN`qk<7 zlYz64r7>ZBa{T<@>BkqYf7Hpc$#hswPkTvU&r*F*WVWM&0%6fd5Ttkz6Q@mGY7@0X zx3|+%(R)ou|)!{HxDDc`=DLKGNMykJ?(x61WA_hDf6zgr)U5GNd)2(qfk4v>(OD z@ao{eix)~oODK>*fC$*tq8J25Rg*$>ZF5;G<(bEm>C3(SFYZ4$n^sk0MWkwK=iK7b zVwR?iw0m+&-rH*Y^x?xR?|!G-=?%_KysOt1lY`Uza6ENy7}b9(fq?)RMIuqrUA5I1 zi$z2dL51jNvnuK=p5skFGN9E^s?aU`U5 z94AB=N504uI6gfno5~`hc&j2wi_D)aIXwYFq%65Xxd5| zh=5yXWb2(}*NKue>y5_8dA2}A0`2}uM9kBqUzlDeJ2^SIeEIVE-f#v#w7yFW&Pi2q zRUPi_fBDt@|L52Dn(ShF@du7l>%3#;mWW1WD9R!osQ@TMX=IYfsHmWLMaSg8)~>Ry znO05RWU31Z1d0~~455hEgx2%nWU`vOD;GA(vV8J#=lAy?+<*LJ+PEysvMgO%T-sP) zTU%YZu(7eSwAAf(y>&-B+iMF)|M`~>)-G(ty?&e|!{O<|!s?B!9>0*mv})M9c6|~C zyck+`3<$sw2~m_U)T0BjHAY#`{1`zqdb5qS9M^vPQ* zOG|mA6he^H0x5`;=tQ>Gq=@n8W%l^dNG0i7zh@$Ia`05;TZCC;G=eoMvDURgA_B-r zK~~<5ZU>%D008NHeyHX^`Y#0PN(1WBN`7C{>4$MxjB#ndSJ^sBEw z{p#Mds+z_D;?m;c55NEYtqU8Ai~TfBqbO2D04O3cT}q7ETs=B?@p7}jP!zq#kMDJg zUa!b5t`v5c$=Zq78TLLa8QSX+RFIVl0%Bc`@_YdhSwt)JOd#w51w;i31?2qtjy9_! zS?4?`l@wSy@fJ}Oc{e#Z+P+s$k2^(M(I~91Zm2*0Xtju=y9&HxXIoW;x9o$TXys0i zt7ki-_upHO;?O)Xgl0$~p+vECyZqe`I)mft6hu^Rfu}RR-2Rv zS|KJd2nY3V2osob%`G3wUg&kLJ-f5QbOyD727rLpTSb&5T}|4SLnu~Ywt5o)5HXJM zM?d=B7hgWI)}Bv+GrvObrK+T^%HinIy$65wo6o3jq;7w=jWiQz@lJP2JSf6q`@Ky8GpW z`=iMuiIbZ*umACn{`lhNX1~|d=5=-r4+Rn9yyz`1UAwmSv%mdj_3E`e?^f0H=w$E0 z#cho&sX!tPe{3(E0=!e!EkfJV=uEtI+|o1BrsGWE!cmfzY#1AWMY>iA7#- zc0owe?(l5K4)#+MM@b<~3GBE$J$d=1D^HV%5SR!R)VD6{KmFJ1TdR3lwM^6U>Z?e3ju!ZjdIJoWrsu*eW0>5*+_nTiHLERK_bm7wG z{_*E~ukPimx3Xfz5D+5I_&D2?hBUS*``mRN=B#FKWsXY_%m%SA)c@@tI`%9Qo1|YX z59+ZhmH~0*_yPb%gx1&AK+u>sz;_J<;8?24mG#r@(I;Pg`SaiWx=L1hSKs4o(F=)p zfQX6+MWAKuNuiERlqE@?E_L$7EX}pz+C#%)fo+Qk33J_)CRv!pM0+w&nrb>^=D~E_ z4ELVer;R-wjweg~-be4h_rZrBUD(`eFYK8#v?bONMT8V3o$lt=T9!UO+}>Hfc4cK{ zb2vO*U-L%mm4(E6z%xLEansNY$2mv;qaqSviZiZLZ%d3*Bm#jXQ4kPeChvV1w}ZBQ znrVeVfT)~~A{D1fD?)UYcvlV%>azolz}_kq3EQ>4|Ivr*7uWMB($u~5Z~pWxm9M>c ze(>ntH%GfWfFRb37XfG**?Bd6b!yhGZfQfn9(*aj1Svs0qE?`?g0J6zhxbNj1G}=a zxOM%?>Dk%j%Liv?yS>FrQHwQ%wsQk>eIu~MGgM_Yp8V$bd;iOif46(E+h6~n zw|3Qa3IN8S5+G3m(hfZ$Afi?(ju!e0oBd9%6$nVfSc5ods#XCXM1am&#0!6XD_igEJ{dg!q`aaL-H*Qe!FO)Gb@9sO-r5FWYngd|8XV@zRYV;}>suQ?`Zw?W z#gFeLo$m6|#^c9dO{T+6v5+QuYdsSNGf^N=x@M;H!9WrV3eJ21qpUYReO06dVJref zn5IfZDwwU7I6wD|Ehj?NFF$)W8tmoWEse@!>)BRSLmg+aGPBp+B325Kg@djT=a@wM zaOPtv0YYjo#9>n|B-uh|b?TUs&1J1?@ohhvDOZUpZ)Is|M8PA_Q%k_ z@Lq4_8YNM%Sr1Ad5`>w(T98l0akg3%n?)yP;!;omoPYyy0q)0E7t2yNQ)waC_Ett@ z;3m=X>fpuhi!VEFa((Oet!tZCE?mg-EbsP|(yeCg9O4)l8W>GbhIxEt3(Px zj6#*D0M-zSlwLfC3z_G;O@bjD+ce1{ZT#twnWHG;rX=y`{f*1T_upGCid<=hq`_CN z+}_`-pFDm#9@Qq!vpn~fo%h~JIi4u3j4``A$J^USo0l#EA&>|5KoL>`gn-8sdDIlA zw0vQGb#o)_^*YnViyNzj=IQ8=T^X5v?TG;pJ+MW#I4ha3?FdAXf;V>jy%|^-WWw$K zr2wJU-dEmsqqJKroemDuIGYvk2tf4r$lmqGKmXZZ{^~dT{^IJ552Ip@fdMQwV}qScpPe?;+I>L)+(NRf>jvw= z_RjO#!dk!ogZJ(%bPGZh0G*~@z%}*R+3D%=@#9C24v&roXQ$APUPXDH^}5~u;!>LD zy~Sk{$2V?oJ>5PS9v)I z>A==QNaL{1*k}S!h(gHTYiZc?`L$m{NO7E@w?<)>rj3a-sgvhV#n#|mr1*n(m)BQ2 z6d6<+L4j`&j{D2&x8D1ptjzY~Z4qMj>^M#nXRR`tv>p#9`}-#zNGXj>fJ%e_6;KzE zu^tML0IpoxN{fONW&MSPm6grK?)|f>X(m~;V8~eU&Wo@G@hWVM7NH~sudkTmK5q6r~tGSO0zuNP@QHZ16C1{P>gcupWl600Jmvw^K@KN?#QKw)ni@4a;_ z3`(OgPNswL-V0`*XWcv3uiUu0K}5pdvW!noU);NYd~{fkhbWRnW^18$eRTmqn7L_e zZLRk8aDR7V;>q!G(kT`emp**&%3u8C%grR|beFfcAFnKLEH17ZB9EA7NQ8>eC^{Hn z)zmZY6#>GgZXhse-W#1hFY!okDAkN^DF`!DvKbtu5h&N(77aiooj(%4zM z^L*DiFp)wf}`Fax(eU9cKegF z0_#Fhd*?_4X(UZbA(4P05oJue+{rI>l2{=#k^lsp+5$6p0fit4aYVgBpPV#DC!wYh z?=7HW0pGq2^vt8tAwiWyW_|P8#!9i&*8;w=?#stdPev0KUhZ{n+_;V)z|7v;re^kl zKzT+)2*@m!VN%rxdwY}dD2|Fe@kjeR#oC3Yb~`Vhbi4f|PJ@C!&$I{-kz+n$w$6rH zFQ5tGrijws_+V!`KJ0djaBvkOphAH>4T@5(tb-wRIgCARy3bZ((V5Iie?xt%Mm-2d*$IWDA{MXjA6bUwGHdOs`!T zWkyQ`h9~3%4s@vkbj$)6MQNlH_IBp&i<(Fi{o$)HFIFDie@?5H7>PiXQ7R@<`2SP( zW-*p!S9aLm`Rt_~+k!-R!bdxRjB-v`%GGGDzXuyU50s8U3`$Iqe zunqXbh7H*M5DdWv47=TJsHK+F14R~D1Ic0)t17c9t8&hmBi?+cGi-mH`(8v2Y`_s5lXSWYgZ>%kEp7f9VCkLx* zo1Vdz0h%ophC)e<1NrD=Xp9CxWFP^;!{H$8E^cO%182`EC4gB91V&P%WDrQtx5tIi z#DD+<5QWhcpb@Jupt35ZaOjLeyRp@L^R07nuZeMpQi14^+5&13%H_4ScCR}=8B3)| z5Igdm7%B`MI~7N(D~k(D3q_&RCWBJI3~Xop7{?qOos{Qad~0E2!?&6loQmR&taQARW*Q`*7;g{B)tIrdF4PD3ca@A`Gc| z&BeoVDpl-nFNu{Tc)rKlIXn9F;%eLOX zYs}4HWU|5zN1AIwV06wpXPK>IHqN0bb`S3h0oN~G?C&1L%5~cjA_*Z|jb_wr8HSyM zgLgmt_~Vy%4!*zfA)a1e7 z!ay{Vz*#q~taa>#PEnAtrI0ARJ8(Km!pcmRmXh9LgTe?!h@t^<8K{9I0YuttZeKe8 z=b`YdtLXtw5X&$hJ z+S$8iGkw0t`ee5sbgl(y4@r??p@i2%U+Si-tssr+Vh7gn0Pv{pCt%FAjq`MBha<=A zW&|mY7z7FDSX(cKe0xt*`QNqY*83W0DLO5V3N6`~LmEdhfk2?%WIF zG){%JMxY1VSO>_!!>k}7a|`3Ve7JuQ2H{(;y|T8t(rUI|y1KQ0a4V>$>z$ST(ZNao zaA|2x$bkHyGeHtdNl1jD5_=~jYb|2UK+3?#G0ODh=w74I4ucNXT+-n4$Rr66F)LlJ z4FDs!APAka>@0ypHc=b|vNahUY;1PYUK3S}A_2LIVgV@x6o?qb@#X86Kl#Nchgo5n z!!QViXtY~WDH(-Ap!MUkMAor_`Dg=skQ_5DEMFDj0?+CQKG2EcczJc@crYMzRn1B5 zqznQTk&pyr=fI|VA15K?tb{T<<`^0PKw@BPN?lG737G|=b*v;sVPeXA%=OroQjo;K zQy$CLvn6(~G#W32NzXb>N{c{6Nxaet&vjL*kOepZFaW+^J{w}4p~>o20oJ@m9>Iup z^Me5xoHfoGX0CMAAMCM;mb+;q4vAd19fyGs2+p}u+xKsN{L`O*c>mF3DTLFSgbN6K zM~x7q5Luo(V=ds=XvCJ3vOgI8^}Fvc_Ihu=@^Y`+yL@rs?!CuRxD=9l^x(@hZMIt7 z`2q;!FrZdT5utN@GMYGR!NW&@5mANhNq=uN+;27-NNNVP0RSYTD3oaua>m9uLSU&- z$f~M*l0BtS5_Ed zJQ`WYDpX0UDM;44Ry`}|>z3$@6hWh&gYehalj$NYcDq1_AzBu~u*dW|6n^MeFaVxT z^#L;|l9kak>!qGO>TwZ3cE*%bA_2^^F%1!hiPI%JOMt+T#&U6K@vJ*!X07#PGLfdU z+3cJP(`JzsBC<&{xVRFoHYo`J5H!pXR1AJo@hAv(Gxh8m`pQs`{91Ot4+8*5NK%EC ztz*|8ourM$cGx|?nx`Qx^t#<{O9}zNgDn62{(~R?{KK8ck8P2oHEARgMaI}t>#Eeg z8!H5*VOSY$wH}_Fq|HX!Yz1L>Jf8f;kA8e^ZEb5~Gf7RE58E;xPe=Ffet!MNn?fp| zW}`si1)YT=kwk$wJQ+J{0D*uIS*QdlesuricB|X&E;0+Kd8a%LBej|lA!k_$3={++ zjI=HZI1pwuJ_0c9#b9N&$La3q_VNCr?z%Yg2$$v%zd)0ajvVfK4U66o)UzA&c6n)Q*l& zAhUCpKyb}Rex^?k2#oBUHD#F%*%@E2XMiMb6I)$P!ypCV)#di$;v!KEmoUZ*2E%vW z{phE^_)6ya@#>ZNFm-Dkaj}PCP>*mL3+QGQ@VT#=G4<8st60Z`;1FvxqF1Z% z*T7k;t#hN%@!I4holfO2tpuYhCDBdO1HbcI7yVCqm#jSl5L&a()lQKZ4Sff@FdT(X0t_v?88*5 z!H>;E5rVb0Kb+LVYCwc8PP)4fzc@I!*J!j*M4rChkNLA>L6}6!7~?DgNEs%6D@^GAV|An56oDEHN0l)^ zCn@q|XXI*;*&5|{ zI!`m}Ftf9Hacrs_(~buPhG810$W}Q7iF10Pop|+V@2CzA4*%r;{hL4i%bOzEkV}o= z`cmK!rJ(IKvC)NA11vyg!7*r8lTgmfgMReiZ-v%@Wsm?wpO#x+33mv zB%O^QZaMp~6`>NiwzNouWs%?d^6tak-7oGx#3Em4##wRFh~UzCaO2{-)%r*8-rt)} z8PQn~6c7Ya99Qg^gaWCJ9Sx7OB2OD>97ZU`-r>G3D=Md`+l^NG`6oZW{_0!JX0z69 zqPn!q3IXKGViGDkI2u{!7>H4%X{XoQdT{rnPJ3Zt={zv`p=j_^iwr_i9Lln+oFO7I zWg%@2q!*VLMPxZ-sX~AdF#;5D%6C|&^F*|=w)Cyv{EZKO{6SF^QpoOdPfDq+nM|{G zx83fwlQgb-Ds@EbY^kTmCBPi}`;^OqC=44(L>^MYR+m#1L_!796J7h^va?YT0s&cP zK|b+(wbjmW^2}UJj>p5jUTe{*7?_n*-G$AA(Xn$bqUq%eT`6Tz6b~Og{&zom|6l#^ z_R&}b$vG9b<)yX78|%_AZj{hnV4=WQeM5qx0tX;N$Sr6GAVAlc)w)nvS!bOi0s#PS zUR|pL07O6{DHXG|z+sS-#tC-qL9~|`3a6CN@MY+~ZzW>_t z`eM=_W;a`Mf7e!(B_azf$hy+#%2J>}C_zY}WGu%cJHbh-+3L1iQ5qkQ4pReBq--co z4)>BcmP$a)b`1zD1v-aeDB7*im~=SKjWLc1MbPbSA0FPhbLXRWXPGf_mOM}c92;E{ z38{p&WHkyQ*;WDP7F^m)r+U(AH3$VeWS}~(n?aYne@6fns*9J;-@p4{G8{LW>B3Sk zPU6vU95+%1?({lRiaNMpPYY!y{c69eayVL+4X+v)FoWA);C&`5Zx7oA&cFhIN0dgaYmU;p}>QPS3xkzj?ZP-ixu z#>%{KzR~NVRCYKxxqttG*81v8SHno_@lh|755K%Q8l12-gwB@XJ9FSoZ0K`B$zeKIorR{^=FOLr&8eMoU2W!|^-r2b`9*>wo2qHmIm^f1?Dm}Km zaI&?y#!fg>7mX0bEP1YxbBI8My~WPgzV(KRRIA%e(%3rNUg*SWtiqryD{Cz{1}~{v zmicgWurqd!fDwH@?q+Ug&VVK5`pPOHFjxSsbrwZUDV681l0?9s`P#1#z^qJq){VhD zA(&Yf357yT#s}l!p7+=UB959uh%y@vP7Z$j(_j5>|NEc))BooG{NS^Zh}UEgBYBcs zkSnX93=tVb2qAz)2Si>0N=iTh$h*xTE0CGluDblktN72dem-d>5N1SAd`Vs*21{?hsne)s#A zuV1ki{_Icw^8FvbhhROsKRKu-S*+Z@_uUKkce4+E@mLr7;e&^g#Dy!{kH30U_IJB+ z`_AW|Tz~b=w3%X^w}W#IkpzK2lu|U(z?gVEDXexHgI;fIG}`^@t6yHZnuf`;W3--d zkZ~|PRx0wd$Z6b^RiDr?VriX-8&PA$Icc?()`E1vb{4Ljx)%fxf>u_SF1)Z^mf8U< zE-h+fh)5fwl#0Tz?!s4jHJ*%4N(3o|fZ24-Q!$q3SVTV;kJg#Wno0$c*MVVAEdET> z*^6}}*7yGtznZ-OG7^b=ay%Iw8l$ZUloVkQ1As2agW=>~{G0cJAabG=#UYWXLpWe| zR;Jy+_p*UlNz34A#*rNef@})b!pXo#gmrbamVyLeV9ksc5>=z<7=h}y$w){-rcndY zIcqfFR8J!?u@VRX*7lE&Z+?1n(jP3g>yeedn_*I(Q`ztQ~RPwwvRWVgTg0-S4i(yBjR zZ7Z~C5#ey?v;i3|Zg0N4v;Rpx-bGAN zMa<+F;>N^D+|z%`~*0V#_*bvfXKkuF^UyM#uYTipv|#YZtmvLt!XD0R)6f zw0aAx>uW?*6y?p&?)>U!pA>mj7PiQ>HMJy8|9JBG=MM#zuU+r_!Ee9NTU08dufDo- z@8N^O7DqdGHhSURTOaN3?KrCiIw|F7cv6-*Gx)JcLXZODNF_-qq$sTl)Afs6mlS1H z-Zxdn>=0Op$XGX-jEpgeKtiz7%r=OlbLY1^-7b+Nx#V1Eirf?hn~Gfpe!la`gNPc< z#`?K6rNnqL8ILDH5JZ9Mwo|ai7P%?%Do0%eU>Iv`1sh|EqJWu^GSBMg%*M~w z18|@sK|&Bon$zUeJKcQiLwtHWtbfC7t#QsWIJSm_%Jb1^d{EkQT9pW3bwNU?upv|^ zWFTY!0M1!!iX!h%j=pTceK|KQA^@~VL;@Uwv0yDo0ze=DLG>vy*Ls5p5E440D{z^i zcHk*P(E-Eh$Qm&_u%!f7Ib*7fO-1N{c`_VJ22&I+pK_76B)@#A*Xl(S5CDKuAOZjd z;L4(Z>*m2PKDhP%PkuQZOr@080wU>(gG2~P+PDvY_F#E2{n{JbH!e5a?_T=hI}fLm zh>bWLjRR}aX5+ON*1!7flRyP)>#IsYpn~J0{iT&n0O85p30WY7fnw%t%2N|B##`;S z-a8yjMjSU5kR$+wVf*0Vej2@?L=-5clth45ns&}@s8$!4fw~|9UG)J=Kp{frfKdd9 zA=U-(+C4}~olbMQ)GN#CWdFpvd}*oIYo)HpOreVjSuNREHm8);`84+|1amwU`{WHk z3K1zPjGl~7(xlypQ$<9;g8UG&d(yOd23MX}h0x9Fu88QIW^0@^&Y4DQMaf`1+-8?u-_q!NBZXHaSpe&TqCapI>_IrLSGTer1MvKp?>mtV0$)1vLN;B!ZAo z319&z0T~86W@0khwydknRXe~MmfW{P#jYs+rLZ{u#MkC24 z0NZVf&QeS{XCml^)uJNt3CSXR7q@9y6I?#r*P>>Le#_0fmD-fsm#5J%$C&ThN2C{@B- zds;+?1X9T$P}eWh{x&Y&z(8AQmKq^*Pfukw79#6c1SzK0tqnM6^u*8^rm z>9`F5Ru@ocb|@K8T2crk0q0r+j0j<2_%0LI}f_DmCBgQ}b~lD3p8P_fa4lmQ@Gomrb**l2#^jTe9L{cmq>ZZw-srIf^34IdOcVI2Vm zfB?b*SWp5)031RIUij1>b7Y}x_2Yb`2FC@)|c>Sd%%i_b24%e5iY@Of!#k=o*{LwF8e(9ADK6~)t zFMfIT>WiILa{vDL!Tp;X8&`x%fPn;(1x1dL!%$h~NeO%sY;WSmn!WSjaopY@3IZwG zt%ZZboj8sgaZn<9c3e|dN=g7=aO}`Q+qgtJi)AH?Oa_EODO5yKdL?u4)oq4Y${>kl zr&WlsI02`f)96{?(A7$HjO-liqD0S5z+NMd0T3O`njr##@@$~1OsSxjj;go783*Rs zI1hkkr4S#-cwmi>^tGv+v(6broQw{v)kJbU++XZ$NFl*#WE+a|%J$Otzw^c`H!fee zu-#}hf*_c)`#mwTL%y6qfB-@QRS-##0?0v5pe0xXf`I_60pq~mKt`7&qg>l&;FVYF zLKh+s32Q1_45`d8XaZa5L8Zp9v;Bj^GRwFs6+yEds>t_T1Yj4a2NWVDAOa=Dr7M>& zU3>iH=XbQ$K^QVKAju#g6@+mptf`F4O1XE8TjzsT0ui|zFD_^cdwbIxFTbHw@cuhL zK0Y~o{pz{Dc>fbYaP8`g=Qp>$`0DfheYw1R8HKF%Mu`JyoTDfZnHCO{YN) zSOdbpa6l)Uls`R{-Gl{a61 z<^1`rD2n_@6*C)SW=-fbxvR5wPNOR&kOCykn{ zEUSi!z4xJ?~y9^X3NtWedoP<#jHZnkz#UA^2CJn6K}o3C#C z3%t#s0KGJJFds@YzZK>`efM1(9fI|@Rf zH4nzD&B^-u!Y7~JR#75Vn8e9o07a3FMi@BI)}jh22>ksG;3+B?7+fv*8~`ZE0auLH zlvd!OZBuSqIJwAAmImT}_WBq_WnqWhGTH&EiHf%b|R-=&V7l8Obr&Zf_+` zTh;+ljXq{(!`88#GeH7C&A(s?{kUP7F9qvKE5*;3|7`(6fyvZ))62o zVT>m+P>sQG((Q<=moI$&<-^X>xj?CQvvqj%a7tAmm1P$+Qx(PaqIgf^^JhPi>WNd3 zP@wSTD35Zk18-LYsJO0p`v09az#x?b-_@zAFbch2q`AVmkB%5C!@8=RD}ihnsXz&= zH#>h)^<*s0vs2FnLS&zc>FZxQPtO5JLEJ`CWtmACRb?i@Ap$Vpx%>EE{Kb1&!H@Tk zjB%xA>%bUSRkqS>K|JBH*cfdL%xp_2pc+sJL;w;32TFh>s5FenP&f#P*jb}<3X-WY zky0K<174+tWT(5f8$>Q2O15@-G8!Knea$JViXutkyfjjUfiP9c)=7yWP!lNx5d%di zLm&l0A<6c_(p%s9je8Gv_wGMxHyaGFu)3UQdDKjhWSj;nidyZY(x_b$gbl<) zR9JI5i&KY=m?RygB2#5(wbPX@D{CA(HyMnx@uZkcRge&>=}D!H01B}7TMC2%3V{4) zse*WI^W4>!uZN9>ws!O4`Gu9`-pW$iZih*-w6gr-waeXw9tt(h3{V831F{!}(e`HV z-h;1>PmYC@%gZaEg3e;^%~!9-u`sS^gbIwkvbnI*C?-c=WTT_9EVXu(X6pc54qSnf z#<_m~_~n;g45blJDoK%2%z#2!CP6^VU@chZ2Nlj(l^1dcUua_MYApGgf&ut$fcIps zo@GQpoB`;puADZ4gb-5D6L3wB25`=nRaWZB;zCzRDTLrz9m2P;jI(viq25Gm6rB%Q z(2I%Tyc2D7Ic7Fi7rM%TMATS}TPq?+hSPy{&N`e-r)35Cbfjb~Jn#!o8{LRAr|O(5 zisCQ+^k41WdrUJ-F4fJyS^aJKr4kf0wlD6fr_d!6Tp?#*&I(?`)Q1mR+4md zF}1~n0g7@u8o4|x?|ylA_t6dtap|o$QO1KqGdL+ND<%XHfKVs`5CMn)DL_;}GD_2L zeeXNJ`Fnq-wYZeD+Y4)}TbC}hx}7UGuJ)D|drM16qdCp=(aFTJ1R0@ds;IZN+-o=S z!Q%%;Yb9l?-E|~3FKqtdANiXLlI?&+;^zR8=lxvD9e^3bg02VRQ_Z{p=befSU{0 zGqbbwgbSD#UAd0h01%mk)cI)Ae>mzt%=7V7yNOng8bp!NTB$Z-;50kWbK+)lG4rgD znfoXG_x}1_R5ARvBO#t>L*QxV00NL93ZV|Q0fHdP^C7b%#6ekxk_eI6`P(6g(nfm) zcE1v)FeJ;eJn2ttRqpQW38h|r`y27n^5f4x>+g^6eR|McS!_fI1V|E40!bhRD8N^b z2|K;s^;d3u{Nb-A)4b7X6XCh-t+bH>x+IAiVSjgjFwC??sU#AUNYZBS#+7w#8oH{K z5MdAyG9dOAIxk(j^625CgPp1>%XDpdD~_a4U)?);bnvA}7h9c0sYpU(?FgXJYBmOg z@@z6KrsvLW0?DG(Jo86f_p+S>hcIIe_T$G$Pd!cT449oYfQ+sL zXVTUpk^+FJxu(lBfdDmIGl&2J1%wa^h{7~bQBmbmCWW!3v9X-HYYc)oY4#kT9_^6~ z3hQp)zSB+}DS6|ymoL6}Su8GH`ucC&{HeY9{@&^f8yDM4l1#l7qC^znr`QM)K`ODb zw*K{R|Hj=f@5mtB*jh&fsbsU693CDi6*9wcoK?of7=VySL6G#eH;t~Ag$jLHQYqyX zbGq#$l$#QrDNAQv6oy;ti>nLmTetT<{o>x#PI}#58VBqmDN4suvCXD~t<^=RCx?#@ zF7?vgodd8TJ8;HwtvAcF^*iIYW;XN$Kx<}!vu~KKMIs?pm=Yj5-%0}@DTNYP)89YQ zit#Z~S>-~Bi`(aJ-_5PD;Jf^u@6tMBEpQ#l*MG!}1mc;7jz4{?b5l;q6;TiXTdi%F zED|=YE*(V`6Hs{h@|FMMpZxn$%BrdcgTZt<-QVB;)h9QP`;+5-J}xG;VOeYUcJ`tm znhr*6E%ChZ{q#}CTLM%701yZarD%0lr{jJUMviGxdBgH!RVYLtgmNT@{oS;+3@E;O z{OB7mU;fVbzPYfz9w;HB@ao&&n&ic&@BHOwA3RvTaIV#>hyYO_Nnfo#3&jiqeDT^f zYg|zl$44jKUbobFRh7;(cLTkO9=1Z}F_qk~o}UFf#HxRXl} z;2abRWSz6l)_^CT*GysFS%MiR3ueFqc2uj+2y)*pyOLN2W?zi{=kbxcC0&6J(XvfQ(ky!7gg z-a?mZnOb6$gbJ`FWK`de_57)2=LkrlNaAF5d9|44!{a_$R~C63s_k>#Z@hlxYp-1| ztHEe^AZ!WQbZuo}p_QDR91@!AH(p4W+EJvOtAaQR8_nl!Rs4*hJ^PdSB}Nj0q^M(Q zK}d*_kSK08P+)zzXV5M{M9&^D9P~%SOWT``B(c^KiCKB9mH;w7ESV*uZr*i!=0@U3WFTecF zZ+^3W_Jh5HySMKIDjXd5r^693(^+GB`Vs&NNRs-ySWzpAC&Pp3bc6t!UB9T@jJSwU zG@A?UcF#ChTCGuJmhbKEWjT3yHMB;?@#^;ZxBuYx7Pqf{{K3wbpWHQO;&q5?q}AfHn^%f=|@;u<98FRXKHEX)+YbWs^& z;v@-GptV*?$uI;VI^Fov#no?p z5e84;f6pd{^A-HH?(BJXCZouc0+JM7>|;iEITMsbu#{MkJ^pUS8U|zjJ4rjjSz05q`P5ckjVb+F4DAY*8p7 zf<#^a+FMKOYrFUE{OV^vRmm?ez7!_SCe`x~r$dZ@fJjj+Hn-2893MJYS{pckK+558 zoTRbVP%Bqjr*o`I#TJlf3YX4-vo#O}{1kxEl}?k?Ig5xY3{;>Lg-8U5aT+cycg~+* zFSV}n2?(*gx|)||S(VMSap}rs1AKIHk|wcl)cpT6o_UP*_gP!{HE^h$xQYhj;Irs>0dX{W_;50$5uDLB35z-B!0O#>N_8(9RC4(zv?a zMJPM%Wwo%_jgwFb0>`#wT`F76N<_0lQ7zXnJET&Cq(VhP zFp`vtND30@Vp6=H zoxKA-V|b^R_?gQKb)jcIOE-J}==o<55H0gzzrVV)w9xCxAaKrBRf+R95dbq<=d?A? zF5ryY$Yar7{OH4vAKbb}4gm0QaI}4CYiW5&5aBF@pTGUn z&jgWzejoAz5WwoNLNxhke{f)&2?c>Q)5&mab$ziH5~5mO z1*-cVDvB0YR>G+J;O>4B+ql`3Dw@6bxu!7G%}Amk5J5n{`uOAhz1=95>~u2;Ql<3B z78A5a)SWv&Z8IBOJF7b*h^CVXFj{MwA&f$=8d|@1@=gK~K`9}nA|xObf_!7Kk)~;! zR8vafUt>}{TjY5}hV>mqo)*P7XvWj&$!K`(%H?LO1prl9efq_& zr8b3aWSIJOprvV9%^%D3w5}+*##c1aNFJJKkG}ir@PF*MIl-e|LF#SxV_u zVISXrboS9NCX=oxJn@&Cy_|kw&d%gZ!vrRaN7`L}!|S1X4bp ze)h?yrmCKAPiKy}2^*`muvfd$ZpBIe_+e$WWp)gMsvMT3|FbYk8_jMg18t0P9K@}I z@%ZkelcE4!1k?cbOjXeU7yunbxUiVsc~J}vzroDI6p+@L)Bc-Exm-|+lLIP=pUC+L4h2#C|NSm^blBqkw% z*;&U(Bs~38U3S;j`nu>Gja8#2<~P=vg8#=W3u z$8=$%rKA)>A|jH|Rvq@$|Mf5L{`ddOU;OpoymN4HAf0nx-M;(j&wgc1g;EhwLh;VS zo!y@%JAIg zjg|YKj*D?-%B7^0m!l8Axc%zYt<4x+RZ$SuYe);Q-;3_<{?D^oao9h_MRoK5PVchuH%Mt1x3s@ErjI7fZf zzg_5c!_ZGLfHPKWYYbB$1+ina48W|dwajwPij1ez?6WMewq$=bOf^wloLk*oU0ah< zVy$e5QpzX_B$S&yB8oAz6zIE;tn(uhfJoA~s(OUEk!IA{^&MMYJBx%D~8$ z!5R<(fTcrl6|-eD&Q!M4w$hH79Xb~-rE5zmNjghjKi5uwW+`j$Qwu^aEG>G)GATrT zOP_hq^P3Cd%>O?2n@zu+F47aWH4BlRc)-&($?du}>xtnFKgUe)7>FRb_SSlUou=knpq11a#|{H$Qyu7lXlo zXDWDqOBCPfz$-68yGf0-dtv?3;m)n$Xy01n7>sj=+4LyOnL&tPsL{R{W5nl z8f*L0Pj4O_ms}RJl!bvESX)Exy?I%O+F(&qt}(YfG3Gd{cJp$-(tT$qk-ok?L+7R>E$l2)myiZgA?rfM)KZ4=RQ=b}* zr+AxxoTeZ1&)}u(7=dZ_^ZE~GAD+0<*BWZxFMsj~z|I<{wKcZTTH9Ht8yo`{RaLj5 z=TQNin(=8g=yNqTda{oUxocOId#Z@AvfOF6+JsRMge$AdFWtEOkN)U;=hvs>{+CAQ z%#KJSG7&ilXgnN`jtA0{%`*eD5`u{C-@bSM&i$I?{?{F|U^GqP;yFkYxw?EYkn-s8 zep%*zmbI{UKbvIQFkq{>uyyWQlD3CcnQI-VoqGp|cOIVDZ0e<$ff=j=9nQH`v{d1UkS<7?}D zHugMgNoQ@VHj%n$!sI`IX%=b0EZ(9sG_=pXiBF&KNrwvPjB&ZKbm z{O5o4`^!C(jUQW6An3Wn75~Y9{14y#tDk^zN{UcOYpr9)&JBjcz5V@f{pPoPiu7x3 z%K=zG2aOm51;+y#4o@B)J}h+}rCmm}%%w5os%!>9q+}X5LF-{vYEuS66jd=A4qkuZ zT)NOB6@WINO>IxcTa^RrYJodrY>_)-*v$(y^CauEqg(GQPwh_6IiB$}^$*XK&E{Xv zg7F&4@>F2{l!d{}!s<}HeiCo8UT_i+PcbB?Zt1B|R*V6-h21+JhN*X@#Pn{8;w*5v9Z4V-EY5kbnxJd&p%aRur4{)1n=_aSS` z&a$z2kr!2AjGktbquqly-~QUd;=*$&+hD$u7Qg{AL;+Mr93BlHAMfQ=u2d*gR_9-nZP5yEgXd}V86VId9Loq7j!7PPJRM91JPcy)SXoG~!VcxEAP zt)qtZRmM|mdyd^aWm9J&r+Jc8U*>^3naH#ntfYwewLFd24GnlI^XPY|_7T ze-B6rQb;6z@Vy`W>_#oh;a?Bkb&Md2T4>T`if!`mA zo-l%12k=>I!1)87Ht{E}+Svy@8^=E3`f7hwm)1^uA}9cntTm=Afq9r$AAIrv!bOsi z4~>kj9uJS>fPm}k#ZdEsN|K0ZneE)LTs0XyK0e%0!0U@EQQS)61zkpu9~C!0-T(7H z|2Mz<#iu6+)030_x$VxYuU+*yqqnVIxATp!y^@6X%P;Tb6$l|I&!)&63Hjt(M+7uR z|LjNae)Nlv{6=f7^qh+UU#H2CJ{A1d$f1=ao(3u!#R&;pY&Uk7+Ge`5R{mh=^$@_Nx2tRF3L}CV? z!Y3XDh={f<5ttE;v3aG9_2ozf;|H{+|3T$yBp{$OzSgb`5)(P*T9=Lg^VhyUTXn`&UnF^&5pa8Nn>MB^PH zlnN2@!RUwo@-O@SzPGBQqocAcoja3j;pryg%aCazzxndHH_op)T@XoQ^!VgaRZgt4 zg*6XGql0ONLcMVQwaxXbRz#Y@v87*r`LLX1Tvgy4IO|co;H=LOS?eOttuZ*4iJD&@ z2Cs*X^n|JXEii)FiR1jM`4s{L5Op|J2efDMvYE4}x1rjz)nU&pNSgibiC6kHL8`yq z=lA^?zf@-uR1Z2K3PK@({1+bkK#0iBIb8vOb1pB6yfh9xyx9pthgJ&IwiXYFLODPC zc@WR_Tsxa8D~iH77Y7lds3+NgqzaS@JMHC8dlg7bvym}4pC3GHTdS)pfA{x)`=9)y zzklsQLZjixTJ!882og#tiQ=Q5{QO`4`M=Hc900N`+uz^!-R#qcQoZ5Sgvt(<7Fs`e z^FlX{r^DSa=@_k#cWzZ>#*UqHcZMfNS=LB8*Dt-^m4tmi;_5Xj2t=&L#_#kWxWi`6rM` zfBc{Qp;&7zRntN|VSsf8F1;AN<1aqF6*iJfS1yHN_|Zon9Ut|VmzVr(J|cnxuIe<- z`MRc~I8K!_)0~gTgSfRKq#Evh0VGHU2DqPRmSLgM={CBD{k_5z%^==hTV7deO9ZxV zZ*NZ^NFj~2MOgyl$#BqawFt$GIAXS_{sMcEdY=>`0N|N<0sjy@8~Hq8OaMSw%Uu${ z>E1VsmQL;RtPwwBI$nNxwu?S(edJR*dXDux|L~1Ldg_7mZy3O7U1d{X2Z)or_{FU| zhvO)1t{_SPaMnIL>F*qm1p*=(3t4H61M(cCgbZjHwDa$rv&m@EKWHj@;oRkBql+XE zg^+;?Vn7Uo$T?^ExX{JY($ZUByA}lM=`*gox52_fj~?E;f5tb?_cT76#+0M%&;F19 z=i!5gL}W|*r~mx_{`BKdE8owb&En1`fj!R=BB%@JR<52~PZ6vxRM<$`Ym<`))4{GY zm9chrGTxa^TkX}?uYKF1s!)D;=fOBD8G+eJA}J&zILCyP=ee#l%{EaBWvn&M zT0dM#bv@JXT;y#_5d7^XRc8#(x4hH6f?50G3o|okMKKsp>vXy1USMPG89&R@T(hfj zNcD32>jdN+>dLS)7dJ0=T1x=LKpp`MwL?bhbllKU7?y>bOp0H7`iRmuuAxrXpZ{(N zQIZ%w+W+tV>3{p5{^LJdKesU&jsENZ^1u4s|LE`i&foo=#l;25l4~q%9d8f10#c!l?boR+AuH=x)7u`8S3~U+o=a4;~$Nu5bD( zys?&uN?j=-2BXnpr(0uEJuSGu#?0suaO`Qdaqg?;wuXQ+dMKz#6A))zS~zXadcheY zoV7<8fSt3}8eL_hNj{yHS(Z&E*(9rqVmum6Mq_P^HU_1Ln@u2Ub~{O$ia^D2(py|e zl0-=ng+UkuLWtRp>EY+hPj&@Pt!fs-J=gr^R7AQgOfLS<0N|dZ1DT`DnY%^>>JLK?-{iJvQ0S-I(;ul3&HZ59Vh7=M>Q8OG4XUAaB zSQ=~Z-PK)t)vddo<=fH^-?_J{H(!;L z%kpS`S#-LcqR``UvAEdzB5XbDYma{)6A!Q7{a1hTFaPuxw&`g;;IBN)+uAaR~TrE4x%6cN8mG_e(weX2ZYl?_9>4175jgY zPp;zhfz{f}sT764cPaoPR!^B@fM}g_##EImMx)K^H}>w|A08f+lZn$hpG(s(RtMOAGUJGl$oIc#3A`WWm31Q$sXL z>3jk{ib62X9u7vM$pm9^&iPS*hMaZe-$FK(OVp9UbW3)AbsTDK!l=2J^`tm0tneWM zFos0VxXH@eLSDoN`;#vraRB&QXDJC|ukxuc8y@)+vGxxb;_$Hl`DZuJT-YcpwRh*? z=AHX-BU@Tnk`OpzcB;*^J|*J-P=tXj73}V9R@QP6FeZYy5hfi~495M3lfmv}as)C# z5lI;)Xy&__HaS=w1_3!|Dn-_5ZJNzSz2BU&*F-e+n;v^0(;VEX!FtF!a&{8p)&JF* zU;iW4xqLF&zI*S^C!b&Y@lW3U{ts^a`q$gH?+kYL`rCWuWNfsSoW)VpZZA41$-x@=9-^_x^_;-MD@G(uE6; zs?~NBaz}M%7|ul?f=CD{g)pWX3?5#2W!;)!YwPIT`Q^3s-WObqOa+yBr1{(rvqTW=k#EKUxGA6@;p)##jCIls8H?nu@%D{8eS0ze~etjx8SmoDCV z__+}gml0!}bJ^&gan@AjXmD`1k#$XOAz+iN)juwBG43@Yhon_yjR}~@(v&eieG*gN z#sBFXF+{Jn^1z~L(yiXWsCK6r^EEOv{R|*X#l#3mM(f=N4?lnZ!@D1UlJ}3$xg?G{ z-S+5sG#-wO(SozA-C9{&PSZ4wLLs@}yq3Bnl5<%Sb+b(AZa>d&KG^)|+DEUw{#v)w zzWTw3lRQt77-rne6h==1>x&NG(WRIeRn1UooV~lX{rRmsMWG!|`|uMP?7AQC`s<(HzPGh|u)K68%jTUE)}bY|4*XcK zm4(IUCaN&{!S00H3}Mi1l1|MD$MdG=v8KLsNN`G5YxM@U!=?eelztmxE(Q zjw6O~=nXLdk)tfjHZGoBT3u>28z<9aeE8z=&X^zyxQN3b4#P;wPv8FK=KcHI2Zxig zGS=4a{3%`h%=dfb`kw)*`cliX9F4%adh;VV=pX;|*H?FT_K8ByLr1`fh`# z%HRouKJx>fh^Y|~fFnmslf5GVa+N;bJ$&<9FMo3V&N#odxKL${c_tF)7!f#!&LRqpyD^(5(H%I}&!ML<$x_nVS<3}il@>BU#t9ru zyPpI3gvu#uYqmu_dg+Mh8+n~jr$nc|fvhpd2Z!(c-A`_O@Sz?Iq!iW=a#oc}3LXZ5 zv$ol3u5PStT-XStJU#FToI9CEa|&df^Pb?EU~_M0a@1FB+CXc%x=#Y9-pihZ*7UxJ z$XQd3M`Z2ni`Kf^n-A~ae_)IWqBi3aYONI*SMxIUu7Lo+81TBN4yM(0gkZH%<235Q zsT3F@GM^$ts8p%Vcy&HYk_6E*hFU*GM}PVd>}gEb?4C|Ta`p>>h%?54ug80m7otE%JV@@lH zPS3w3WHq*c5F*JodUL&Imgo5=54Vj{Uz`c3*Y??_kgP2S1E){=##nOu{iDD5!4G%$ zkGKe;C?RBCrv-qOJLy>i-W6xUGj)W_SB@egku~`qC*6tzM~cV~)Ii1r=Yj!(qp}=N z^1b=qi(wctMx9Qhe>D2yTX_;yC(-GYqGO!%M<{%6WdH>eQb;u^e);oXFvgab)_c9@ zC%O3i#`RlwZtfo*j`ONgRvV`+3C4O^XK8*TD@&St^TquY%e4TkT({m#7`Hv=Tj znbH~&jWJdmMC5`uyPdO_&d<%y1ybTk`URO_TyoAuy*U0QoQ4Sg+c;xU5X?0)vc|W% z`!Zxb8&yOq&#kW^@E1=sp5)g)`E=*t2#~cJtuP1)&_QkQjB``63hJl;eQ$6^upG%#HdFIpZv9Tc=1D#G@h~@9=Pcf1D-r%`D`cx8oRU#X9;4l?Z&s zj*=r}08Aif#04>&$+r=JA;&HHriWylyq#`1B0clXx%h4k#%4gVHJad2=j{1RXtPDXwX4o4=rsPV*j zAf(^`^>9vd)6(Y0{WS!}Fhue8&%WR8E@zEirxldtVi*7tY>1+yZhzKCJS-l%Yi+XTd3B*v`c4>>e&Jb?4iO6cP!*`ZOdm&WJo?Ru@__ z!3Yt#0M39j1aJt%kO4RXAJ)LBta*L?9u5FTDO==mBQ*$BrTkapI82gQCP{C3p|>z6 z8AHZ+5HKMb7s!xk`udTtfOW_aB;zuWQE0VRlfo%Yh~&ru$s~@#kp2IKmdYn;Jf5Kn z0Ln_;xO3<24?iZJ^}1`$={z3>vIVuJL+i-UiM#3DO!WDB&#D6_Kp-?~6ozpixI5)~ zGRB1nIO9l^PmZd)2rksrL-Mz89cQ80Q z;*!U4Y7JTqS}Rp4G6f^YdA-p)k8T14l$;%H-@AAFqu0LnopwWvC;WIACb0}7Ype;w z#&~oz86WB@7cvS_$|!lhw{~OCl~Jb3p&>9bGEi_^5wW6l_a2sK&#Y{$blA*CsN2vX z5rOpzd4~YR86pSHfU!E5<_OFWO$B7kO{*PzQ^wN>_LYS)kyua?Hq|O@eCfJ|svuigJzG?2G8vA>V~{Kc2Ed#H>&SQVb_mRmYD7ke z0GyB^_^_R<)#O~jSs=JO)izSdPzdSYobl)&jNq$ZeJ#tfni&X0oO8nqMiai8acULqp)K;y{iI0JDE= z_74Y2uL=er1t9Y+UwwnqsX*7KM;IUyW5^gW0M1aQ%KrF}DTvbT-iBu?Y6nmX_fpH*5_m9}QC zw-7|pmt~!whAOSf!SM`L0HCzKzxD8^?_50?>hAn`DWl`jc#;>^IX2~s0nm6J5g7n5 z;LLYbnfg_J9+EXBLIBPRbeu7mvdxGriW1H_A~<7)Bw>>J0K^O=k z#!)&D2Ws-k%{x~vpA&OI5Qd0YH^qax+!ANr12O_&h{S*eV%k!C#tk|D%*j*QrweHa z7#E9cYnE|`6o!G)dam1!!Z2uLy_MzO(xM1LR_{Zs2G7)#!lM!4jJ&8*yU19`qNd67 zqO$J%#SJMR*8+Sgs!UmygJVxFc)zMr>XRF{e)zLr9F8%{mN<{bdC?#Al~(QMe2{dt zHIBdnDo1bv$rCT_DF^yCRKBAj3q#@zI6t$t`dIJK zFFmYQYD!so3s_d_?t=%vy!ye>$jW$L%A{1P(t1)%IK#3kM&))G)2f(Lv z>H((2EHH@?hX>uc?A*CCr#Gi04gp|i`{*|U*2&tO zS~AW#FqmHgj*x3xGr$a?HM$%f zq|GzMXgiCWSrYlR2_mUANErbzA{dVkxAtznaqhAd;b~L^p$tQr=hd@r0iMQVJ#io) zgVr2eScAm`;y`c_kaNzOBJb;RY;_4_%hA4b2IIM#n_EYNeJ0#|2Nyd05yBZzYenQ} zT7?S$jN$p-$_r;+(#BX8SwKtVtkqiIy1#kz?q*pjXAPkPj??~QKpaqV37DS;HGEP%|qI9oMJ^R1TJx-MZ^#RjwpM6AT zo!dViEG#djjmDR+^Yr4aQsrPk(@54>yLWW-v#am@VOe43+Rv$f$z30cxcOE5(wXMmI#b-QUB#wfw zay4yQpYE{&3IehKCw37KfsnvX0fuR_dEw=k-`U-^4C8ilJjw+ZQ|0CAt8>0|9qszfg6lMfq)g*+qXA1Hqtaj#CALDbQ?R{M^&jr7C#+WlgXs2DqkYu zoU5v8JRUpeCX)#fNhysl1=s zUSHliyMC^nvDKd3+E-=Wb;gHLpj0Vk2;c`$IYbO(_`=4Ok8ZwWFtDyNRx#iugX{P1 zy!%0t#sPyfe9SM;FD(m+$Y=E`)1ArhgzQ;cZ|!59`V0e@adH3z$i@1l=gZ;o!S

R^i+Sbz3^{OQ5L!FW6-B5NH-J{XKmRX9H)fj9zXK_np>=k{ngC{P>H^bRZXb{_<0w+x(rvn`xag7iGFpT3%U;9RK`_{D&uE|Ez5pX~F_Oyy&hJnYz!P-35 zfAdo)taF3WXsGndU;FwOGgw4K);VjOH9F6Ig`hEZ=kV}@&u+Z^{>Ot+p&`;%2obIJ zmU_K2&1Nr(8k`G6?jv94ECClHVqjD?GI6KY5&}%4MYqAxwzPQ_~7u7dacKR^@BhC z55}1~LS_sIoORCmmd^EzmjiEl0KwOcOjUuLF~PX+k6EhnPJeV~Y@@CiJh*=R@c4V* z{Py|t&m~E)G|!cmM*~+W=beu@Zsx_PD91q%dv*i?ga}twE)~`Is2KH$3viCCW8%Ty z?$53&hV;r6y>{`Ej3Yk|!^d8ALYmxDRb^9C$!c{C)^Zs{=4%jUTbc~RrHymjhyBC7 zJ>r6K!4Ozo6N^yChg6e2r>yNvz;mi?GF4jL+ub|=-1CcTYhMsW5)m0=jdt1?t*q8W zRF-OI|LEWT;KvVl`(s61b{Bf)WW+a0D zW+BDg+`J=FW!XxTpq{DBeeQX>6zFwbryY0SpJdvLgp9S@hewUx!lf%O2T}NVoz3)c z);g_CRas-5(azeU&^vphzxvUy|MI6-hdS*nJh!m){L1R(tkFx-P7q~MMw|;jR&6He z^FFd8XLPwY(jx9LkvKqxBv}3IoI^*<5t^zPZ~rQ0<=pbCRaLgrD3cnT0)Q||8flv| zu61?$-nGNy-7IS@E}Tu1rk_54aIk;=!s6WA9OwMO=D|nT?p(QYt~=L!DzK(QnocW} zPCpeqc{sB-6s8V6ku%mF=i{xyjN&uuc$>%>m1~uAAwRl#_xTIAmKGL+Kz3W8jU4BC zQaFa-02r|-%7|Q9jw1viBm+cbY1VDE7RFUED*W(cmsgcxwsh6x(+3tl{Q9x|>enDn z8^}38-Nb>M1GJAg$e!o(j*GGf6t53Sz{5r zkQf;g05BRK5)kJC0XSz-lnjrzlu^p)nA_p-C|8bHEW*~^-3QmM-v|%`DWWj&TgkL* z5u9vIh(xDlQ%~g1XP-aW!0NvQVbGrI9*@VLUB7{hB~irvn8&HFK06oTREG`VHDTJh zky53U7hZTVNz=#5g<%?SIBT3z&RXltWL)eWjz9T)@9lT*ezuj%=GorzrDkhMiU^qi z07M=HQ5Ys!oHf!$GifAIQmV?jy1|g2uuDYRREL8R3z|%3jtN%lg#2AHOWaiB{^l>c zp{nrHWWOf5!w!p*Lo?PrjihyWmjXmvU=N*?YV^arD6 zrxV68^F1(pap1Jym4BCkp#!M29$A|$E-$XGN-wQ>>Rx4xHHL_kQb)&=AOG^&#~(l3 zA8;e*($-24ri=+Gq<;=k5Vf*)J8flgnuM_w(#I3yA~()XJwE^da@J_oAB{!S#vlV0 z)Y5D}`GlBtSdRD9(Z`q1oK3UNsI=Bpoh%MH0TD9Z%sPRP{lVV$-rdE8jU;Y9cJ;mi zNu$Zmo!d~_{^Z9Wz4FTW)wTI&0BiM9y-l&{Qq@k?Nt}M7zW{K~8Ry1Tv2fwa>u>$u zcfR{aD@$i`O*?y=YI4+Yql?{kPEyGxV@z9+2y2Voxw&>A5dZ_hly*gFiM+q%sY=0+ zk4J~r8O9`IJc(0qR+~~;XVq9&g+nQ2=w&L#!NZ-0*YDgN4kuB-IRoLFn$(ba3Z!7V zIi39TWRIJg_7f>ON-3M|R=3wX9*y!!Ib@@v;4%<0U_#UiddA4B6ahd6CW>3j%Z+A> z3-P31RhvI!oi*0j!KireqdPzQ)n|JL7-VN;V@ZT5=TdMUhfyGdIEvcMRy%7(QRJt| zoi-+;Vq&bR!w4dB&i78~509CMi3cE3oVxP}U>zD~i{sl%8Lcd?I7iyqByw>OOYYyL zV7|R*jJkRE10rg-7O>utPkWQh7(D9lUAVZ?Y_|UPhwq)guyX#w^3#F!^2#OOgdC?V z<4Nvfx(&iKsY^iGno*UDX8Sw8^H0D1?cdMRb{a;D?dImupzPm$uE85|ybDbhcTJTu zM+k(7zGGd{mo4s6BT_&N_QM>@yKn zN{M(5n5dmWB2C=rvO4_e{PJQPMb_H97)im?DB%ndFbv}~ZVX0;4|i_0TML3qKO@YW z(W&ubjPt>u=yc_|^Jo6%2k%|Hv~uav+S7sc%Ifp@i~~-mdx5N@QdhzJ(&evy`;Y(Y z|MBH3--x0FeJfRKwr~C_o@}nq^+uICz%DT^W#o*~#tjC8b|aXdn-2pKN<_pg35Fx3 zl?C*(NU5gfxm0R0o*W601VPkkb$mnZ{lgtbicks>S*y!xY>gry>+HeN;jO!yx9&aI z-QNQs#u#S^bJc0jKOd#@GKc~hL}8dEQIdp# z5PXKFsUex(NF4+#qlc3bP0h)PFG)0+90Tyf!v}F|oynMxA%S&htSd&_Em5q_cNqf$ z+TVMill@GwUF^?Y+A}m_}g=UO_zVMNwzK{HJ)Zx3{yr(!6`;aAjrw`OE9i z0M^=b_0RgG<;;_vE)0NgkWRTSAxYo(&OdnT&EH*Lza&KffJ9j7YJdOX)t~MjN&k? zisPMwZKHE3W2Lk=RF- zi)sOv{}M+QghA5CTHWsa((>ZU^1{k;l4V>>L%*kaRN&jMJ6DwM-ot~puYSI>Z`0O= zw7C?9Nf^p7kYOOASPH>sukp+mJn?j=w%rkpi%F@+$Z{`7D<8W-%P@IjcJD1;z#EqLsF)axxtEgCJqk&Lz{gYkE;N?aY*_;@Bz0 zyWQsH7tTB#Sg);L_Q;%4Ri`_g)aSC!jCHl~@@xOu@BNeC{;hw!xVSF41Vkdx+T6PR z@qhStf3h6OPS(n+Y6p8GWu2*njDjF7^P|D>?xZkD9BrIg6G8?YqX>Mjoa3QSB@qEQ zA_Qb2U|cX{hliU&#&MK(o2fR{-r;T_xFzPC(?&aT);g^!WDJ>f&MBpij)%AJJ-l`E z_Vt@L`p5mEEI4NX!0OT+Jk6j#@=Q(x?d0zWGtuc(ROU-dfNi)mhBniS$1ObwrQR$&Z%5jgDF{ zvnM|s?pZbIAMa(|b0V4}-&zJ4Rf7x;7qeOP4Az>mnsho#jPX#0S(NsN2bN}&%yAYm zPgiBCAR-_V+gk@*R$&;w`r4(Zt^j{^{c;VcleE0QTK^bGJ8J^D@#4#W|9AhCCXuspe8^ZZ80-#5qYLLYI_*{{ zIcLZP3eE{|G*QOX_Rx9d024yUFighd14lNF(@@~<{w6ZUSjdr=)xefRC9%V7PjoUlJNy=qUh&YO)B#~h#8E0NS=!eEX;p0t@=?DjJ_)4Wl`NVU=j|c^P zdNCg;RpwO@x6X=W!6&>Jk{;a6pr3|5f#xSxxw6Vz%^v5RAuo#YC?BzzXlxqAb-Gzu zoG!>A0ARqhrUwu2Hk#>MZ@u!=b>Od`dEwM{<&8RkG8Wkt6C4j(+(4Kij?e-rC$k6h@ZZapf*N_tvPGJdQXU=+9_}1Id~k64_PtxT?+*s~aB!@YG1hS@{Mr!!o)GDe0cqNN z2BuR2euKjg7&F@Cxw-e?=z~x1-MBlnxP-hJgkhFNaU_Lc2tbakBeGA14UZmvuO$Ei z=V&k*SGt-#B`$nNG z=fX$e&N*XkrAuQpFy@D96EbHB8Fz$^XfzoN$D_euyt{X_y>qa&wYT|TCod|(dZsm^ zuU(mjUQcd9Gc?l}*?VxkcJ+@W zq~H5TfAIQiFTVQH6)7@pX0m8fs`})!5B}dj``_lmu${F87X;MLC#x^~!^MU3K-7p5 z3pkz(s=>qOS6AMA?Yk>W=K`7B-h9BLW~Bszx-Q3{YkNWmwCM(~Z0>KkB;1cM+9 ztSLsr-Bz<5hq5Tg1X9Srzdl-5&N&1q%84?SvD!KZOgQI^amHCdVI5RPkMq&d@$q0Z zJ~%qQzxm+P&#vEpu>D|b`>20B91JT}8KV)gmTjIcnkGM$l^y}vEV?0b#^|zClSw`t z9~~Dr?;rm3>ZfkI&} zM>VPb36|x!$Oql_LZwyISkVv$Xei%q@mz4m7)FuQ+SYm_L}N5EE`(H7URDzX6hb~` zVNcyCcyGn;J7>==5(GQjTPrKwtdTvIW)N&DrS+iqPIRReZ+9-g_Qu7FOV3?8CuKCr zYb`lLu;BLgxBmLC{?v_*lWx!7pCby=UT@)?;KBUddXjdEVq8u-rVp1kUR+;3&$)2K zO2dBq_SGnAY^-MOW-|#SAY`ckO!7+aALal6NJ-N`WL!ExZI!WBTjz*LArL_uQ!y<)7l4i-YBymx;fO&XclP(C6mc9T zQQT~%opxhoc_~TaMx)Wp8flvFX$?3`^KB>MDV4x_Je{;3fN|kZa;#?HPP^&TsVkrVI7lG{Md|l|KT^k_w6^n*GyZ+QkHhsm(L9<`0Ky> z^|#)5bA8z@&UJ!70ALjH)#ZlZY<`Dg zM|(-ymO)I`R8?-RcGiYr5`~GeN)@?tMsOa;$Pyr0vI@`%ZsMfb>8x?av?;BoQs={o z-aG0WqY*v;fH;ytAhIm!wAu^3`QF@ooJ2_!H?mZ6A*2B3s_}R{=ugJu+o3<04Gf)>hwI=&f|xS(1i+$PE$LM6uFxFJ4@I@ue4{ zFtP<&H@#J&QdQSJe*fe5eiCzu(~=kfs*GNm?{%7z02v48!00j%<~wh`eDP9qaERpy z1P>c&>-n`azq2s5%D09K<;ob(Yg}MFvh|lh~qerGK!*Bqmjl@97brZDvJJKFdXOOyeuns zG_F|CnVUOzcKxNf?tG`)2qd@GX=A4|51!awrq}|{Y&?N2m!$a#WV%G-)H!cyuU)jN>LEBWI?`^hbs%5>dZDHq$Y!G_!7i zWvQi%8d>k=jm;a^Z!Iq`&7_gS%UAtgHBsfUsh)f8wbx#I@tbeG+3j|zWJcq(I)SWn z*FJvlU;nHBH35`d&V;!HB;(}brAwWLFb)~ovMj6W=;h1LU3ukFBa?B2utj;v0~z%? zy>Gsh{N+18|J5h=N~ND$&)f5BobiAm=WJmv5dwGi#uWlX(gyrU3IyJ0EM@8T?$JSU zytmj}TWu~4$H!%nTdN(}STw6D=ZrHZtkn#CPETlEC|y96$6*>r8Rv=8${2-+gV7Np z3L$q7cD(Md*_eyMz)^@84n_xA0suBI2F_ZJ%qXR`1_#zya*hC`jJZrJ5@~bo@^fEb zTv+RLn^_uKM_OBLoV9hj=&5_m@$Pwhybi5ZET?`<{^UcEVo~XAdVY=(9GIJ zUPMv@<&qNsIOnX@N|!}33d6Xp#;EdUv)Ao5!-#Kh_v;XXTT^XPX_li7Lx9Szo#E#_O-V_06|Boes0CQoethx3aD( z%b))3U)teuD5R%D{8=2)i(h%;+=UB4h|EBs9of-v_~5zkekDmF1dL)>UdFNv_WRZm zwX?>zuDtd)SAX)+jmqTN>rqRy2Io8pkuz+hf$Bs@{lXFg2nNnsFxD~T>noR(*1KDi zbTXWa8i5EZR{HcV0AyK5^1xb+$ck#rFt%1}T{2``1b|qo!i@kCqzub)%((ynt(9}u zSj~_f_4ff;v)PTKI4>=dNq#SjrJ3Xr}Un;1A?7?`ESHiJO(MK9d@wJIZIj_YwZ+^PiH6)QE(xH*g7kvL^r1Dkhz>maNw*p z$AjHKhC&3x(P69A%d$C+Y|tMAqnYWEfwaaqs_{;hubg}Q7JTIa5h0=!L2v$yDido@ zE=w?EM3h^F-S$gg`O1Z}tH1NT-)^;A%yCAdmuARzw`9TYE*R%=&!qyI=jCYqvh$?VHWJ zpEf(|$966X#C!uJr!)#!hYpV>dF6m}#=sysi(%B*SbJ`N_xkSM?YtPZn!Q%m<;bKA zDpfLMjC0O8G9D)lYjmZHByDO{S!X4e1f*1HomG=T5X6jeM^=iEF{X8;ROtwWkPi4@ zGEqvQHL(cdAO=UyIgSz#te_}}K_)1(m6glg&Pu1#Yc!%(J8{+(rLo2t=Vk;IePQRj z)0@@_r2I@c=d2|!FshUSfJeEsCmuEcctw*cqzE`zQvHw_vbrcoWtB@A7;6~gbDfna zN;x9=UPE>YWCS1wNPdR2qpA5jRm4>{U1nl>U*tEYI7Of5sDVX9g|1vPTe@`V^2Kvs zfAy=aW{UxLB^o;muE|>a-uu7${=fgf1Q0_;r#52esIX<)Y$C69vy-$mI@PlUnNK5aJ54c2pMlSB%mAR=!hY& zCo0BC`-SJq9{v~rr;6*$k`gJbqO5-f-#QBlHXD4-{p;>=d&09%+h}Ts+}`M zF}!%`Vi26vKZ}W~`k*$>z4YP>m;c_kzt?DF960IeUIiXi=E2tezyA0ChKdPiybfoe zUJPZlVtj7CD>!oq01yUpZDn<_*GjVlUCkPMzfcHlw=s&MK_ZTht8M!wD${2#yV|W-2@E*04lAFwY6L0Win6az=((#6ON#$a-&M_^5I}#>B2v?PHR!h zux2YfbwmJOcZI+ZopVISsB%2+2QrGnjF|6INWNLQ$C57q@UU~vj7I&XrR=-kdySnc z!sB0i`Hi`AXD>g0`OU9>W2x8keeRw0yoKk8UH`cM7k}~RpTGCZkjvR35fEdnnKcaA z?#jwH-uzY|gr6Bo0AUb-o6OB+95@n!1zWdL1_EFlIcHpO0AK(zNR6%z54PHA!l@h; zMPUPSjF1_JNDhItb=IM==qu)=jJXUWrpt0T9QKPcSGugKveJdomC}_l+F1)g#%dy` zROOr{qBLzu8FDVHwT!bUZUkXMWVI@gu`o;+V_^^nfi%`AU1>{}z>pAedv*22mF4Gq z^JkK{nWRyt6UT`(#wlg3JsC#(xZ#@`urDx(FqKw02gW$>;xMRlZFyB_Yi2npkF1)| zo+fkZ_|bz7$Qq@}$#AeYp7a?LNt|U_*Q08hxns2S6=(>H-D?$Y$0-R{xWL? z4p|zH$N%o%{+oAy^q;aIqEqJ9gUb;q>o(4vkEEDRxds3XB>(jX?Vm?dTQgFnO0Y_euu@=Bs01!+x z+Y2vih=Qa3Jh@3xI^d&x1i*4PByx^i95>RW#koYrlQ1FYqzsC3Y>h!g#svbOOa{Z@ zfsi3%B8?kTM%Ee5kh2y5Q(6ngTl1?+3md)pwJ2^zL6XF2qZzeYk+rTY{1~^X5PF6s zzk=>JvWQfp%I|Q*IRbKWb#{`7w9zw`-JRqBV9Ft$9NzR-O6B=T@JXqLjmCVl(dAsu z00q-TIsi<4a{>bEj4^6==l0g_?cwOCRO98PRR&T9GLAwS$bNr3tyJI)(E*jkI0#4o z-GBeb=g)6EmemngE?;@$wQn@CCL$wW2tU18BC1sNlOO%yS3mkIK@O48$>v_O3`E2O zxxBpWPdj7Jfb_c`ex$Wp{==1sN7MD5EhRvm4a6uG3%x8WrDPWcql3epZg+7hlcQ>X zJQ%B_$3%$4$pzLKF67 z6hR!N&RMNWA%Zw=hH(Y}Rh18i2W6EP#i(fY;wTd$kU{{EHfC|*>|A%b(_Re17+4U8 zX){YZ?L-D#DQk`M*6#~Iqz(B~(^4G`dDok!IOS|HX zF($8cRp~-iMO7*1v{pqvAaX@9w9Z&#We{^2nu&JKT5E-nK@g>J(>P;{&Wm9_83-9H zFJD~fok`QC6dqE|g5f5ci zsq5E2JafjK*?3jTPzD?jj8;CGWQ_^K$=S2>LD*tUJo&g#Zon6w~(h>>%E3@iY1JPR5d zV`Fwkx0lj@3lV{o+(c2*A~3}K7)@tXO;a-FkOOjZc9IXqqoYcfoD0S{W4x*+fUv%P zxz$?8vMy&L4%1GfnfL8u8{=dqurALzEI7dWgR#tUYzvr>L)RHhQfe-}qVAue!KIotN zVEAAdFbo+oESrQ3iKZy=A(QJ}lB?a>o!RBg^z`(jx*nMkK4eyR&+L+*`Y_MxuFA~F zh%X`|zPNkm$E38YOA+3?T@)|8Dc>wyuI8s_kFKtsS9O`%#8{;f*hAgcr>74GgQMy6 zzSdeK@va3C&(68(umARVIJo=x@l}>4uRf0^L>82;cH9g2R)E|8`S3UY^bdb8<%SUb z76aTIk%*M_W;i(@w9X?l0D~ZS@0aV%^JIuPxLH zHKPiC0s(6RG8&RZX{(e%qSz}2WHg-mqqjEIT0pA0bZu>Iujo(n-biU&nzwV1EqPlbZ zHYpwYGa!W$9qgV-y6nAe18iY#uCKp+`t*O7i}UGpJe0i1)3)Ah$^~()*w&b{R4KmWUaF<7nYqMv#1gKFGnMrdIUbn+1>iquVWdiLz!{`H@$ z*@aSy1NVk>x%@X3^|~sH{+LLI{|kZ!aL%8;oK;noC#f+x`@D5PIS_E<{Qji=?C0?BJ`Q5@u#sZk8-k^v2`yu}(4~Wpr{hIm&ES^lg7&L7-{aIq$y9 z6x|T6o2sX?K_R>9_Qf}ZOac{4RBj;5yRHYpL%-bwVJlP!Ns=-r;ZP3&ktm2NbdtDN z%OEx}ZBxy!UOapLf3w+HNKHE&kH>jNu69kaEZ3Kd>#OB*e)0G*JKr{g;gpctrb+wq z&QIU0s;aJQrPRsELMc@gMUo_K+bX59EYq{aVo(e{`?hVHwz*y`KK$r6zx&AOu1eS**%WA3QP$-uvO;VA!8F4Ql8TY!QgMXfKO* zF;gRSrWXNbG+Je;ZJhI-J&4lTbn?cqe{i^(y*z*V&9etSyPx#mz%(lor3f<(&RaD1 zi+nIY`)pNSraA#6K|#PED}tc_JBYA&=5n)~&0i8xQA{S2ds>CAf{-NH;rL)sy5R_d`$$9ru{S>7yZlS;_UKbwR-;P z@1LN~i~clAi_xU`?+=!reEdx>O@%#*tXJ20p%0G_lu`HIeB-_M-qT+`{NiWt{MGz= z{_!Ut{pqWR%gf8tr{CD7O-vGCRn%7zvftROUc9pDa4-RgWI16bude6kvx}%Uk(Ba8 z7@Q-K*m{z--tHEJ#C`zr9$Jw#$%bp^y+Eke)kQu@Y(5yyt`^IA^=LV7H@cTuJ1mB2 z+7ke+?d0(7#Z0Z1FEg8`SwD25XA$;ZB%-6LE;rS>aaB=F`o+YiJ*|_los}|&lj(R+ z^m>Wb6bA1cJO7=OJ+GCl?bjO`shd!UF5aG034}xI&LsyHaj|$1Lb}WAP_FEKI7FM; zIRH1nREXk-S$FlXKY0KA{3Oq^UTO<%4hDlFQJS1^-13MB!d9vKheym@waud3T&*uk zw*hH1*vUcW`mU~)Rk^Iz*O{eM@xk3=*EA<5C;HbP{KxzM{>!sJeYLo{=qZzElOs`L zIfgi*JrUT}t886g@!pTeli_f*r4S+z*ECO_Jvo2zB+)%oX1Ttunx#@x0CWs~OC{8K zh+SA7U{~lDFfOK6d*;?ViPOq}XiPd8-5vH1TGv$d`sC&FvYcNoPmDH6k}{yML()3Q z=c{>9Kw?sb2!N!Gc<&p(u9l6fLD9kJerkG3=`M8~vot?GJerIL*6Q#ivBdb#lBxij zx-@2qN?y92oVP9o5%HeeHukLy@Ao3kIp>`KB#8om%`2RrakFDAtl0Wj5mViOU!b)L zi4kp6&Mr?bE}kX|`$hlGWSSCaVgd&uz61FvkSIMV`iO`Wx;SuI*Xz1Uti5xX^hfO6 z(&;Ll_Vi(<2;6M;^z`&Gz^8*kKlB^B z7cfk1@uR!W!C< z;l0y9+a>7ih+t$3h(9Azh=o zcZ^t2p?c%)kBl;e016PKZt6ci{QU9b2MRT!66QP~Iadcf;Tce1|LB9%HjTd9f?IeQ zCYZ=*-Fhc6{sjWS!K}u!c5QTYceK_%%h`M9y=TW^UxeyD_H9$QZEZ~2>kS}wyoe&D zlo|EMhX>Qqprnz zKBuZ2QH*QvLHSOL$7o<|6eU{+kOEbVKxp}e;+o~<<+H{0i{tTh((4%#a3!%Le9aK0 zlT?Vt6$nCDM4Gy3>xPK5)=1=qTjw{`rmE{<-cO7H1Pu`L<6#e*V@OAc0QB{K5CW2- zEK4a6o{1pH)%@bKFMhw-lnkTBit(69yBbguEt&b1&MSVnd$ z6b8<+ZQ>I9phkfUABbHEC8G$Tu&XaGpI%&^4zu1cO%*pN4x#=2f)3g1LeC<^z~F@g z86wneQ#Gw)zbs3mRMoU;VymX|-fMr;eU0lczz}`)t+d_nZV*ExX}MgtZKFvk#H!x> z@yk!nUOuzdBBqyDXGJk?+RAyyUbgA~Rb9Qfcrh6rS=$37ZBQ%h9(8XOLo`}K9*)we zocOMXM@XUU6fGbiDpH<(R6&Re6%cs#;UrfnO++j#%s_&SBA|3)jU5lilhL5)*?=V2 zhapP<0k6*TEl!JDvlkx8v-p@~x(ziTATS6~ET7ySHg8|f`?{%C<*M~Je&51{X{FS6*Pf>`g`SS zH|ntbUIZB!WYg5^s%pK@Y?^1;!El%;JzuTXn^K3O{a}5%fZJA!LqJ+ulk;8#ioP*c ztrktwFh-6FAfo3_zW&v(ela~b8WsJ;YX1I*|5aBTYx2#eqF70WRb8_%O5}XE))u|~ z)naCoD$hrV+9c#fe1nKU3Q-7qCm;j?Bm{{q z0g+u>m9xu>CsnmP9E@_K6juZ-;eJs5hshQIdtbNhrmm~DA*FJg4vNAiNywBK_xl2| zcb5*nsccKY0w)@Rfbn(eMh=|;3d9sTcE5f2kltOD&T{+t8 z;Jv*R5scE);dClo*5!k<=a?RN88l6s7)wNil%~07&mcR1CBE(DO+juuHF6?tzu z8kj_N7wk=C_of?b*+qt5UE`>*SeSPLh=_`aNcJ_Kbfw-Ah_iU_*PHeE)!DZfC*^u! z)82S|r(@d=8{xHV&>;cl<_S_<6r@nvEt`w;Zx+`t5>3;=gnX@`CSvwcN&Z0D3Kn*? sDa)!BfKgs#*7lM}{<(h{?qmc1A4v3VXX!Y_j{pDw07*qoM6N<$f`1$bvH$=8 diff --git a/base/src/main/res/layout/fragment_about.xml b/base/src/main/res/layout/fragment_about.xml index 2ca991c4b6..1ae3fae328 100644 --- a/base/src/main/res/layout/fragment_about.xml +++ b/base/src/main/res/layout/fragment_about.xml @@ -259,32 +259,6 @@ bind:title="@{@string/about_team_jambl3r_name}" bind:url="@{@string/about_team_jambl3r_url}" /> - - - - - diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 101b05b7b1..37624bfa5d 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -650,7 +650,7 @@ Automatically select the Key Mapper keyboard when you resume your key maps and select your default keyboard when pausing them. Hide home screen alerts - Hide the alerts at the top of the home screen. + Hide the alerts at the top of the home screen Show device IDs Differentiate devices with the same name @@ -698,9 +698,8 @@ For triggers and actions Reset all - DANGER! Reset all settings in the app to the default. Your key maps will NOT be reset. DANGER! - Are you sure you want to reset all settings in the app to the default? Your key maps will NOT be reset. The introductions and warning pop ups will show again. + Are you sure you want to reset all settings in the app to the default? Your existing key maps will not be affected. The introductions and warning pop ups will show again. Yes, reset Reset all @@ -737,7 +736,7 @@ This may add latency to your key maps so only turn this on if you are trying to debug the app or have been asked to by the developer. Use PRO mode - Advanced detection of key events and more. + Advanced detection of key events and more Light Dark From d3921d6f6eaf88cd66c6b9b55a20d2d786da41dd Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 22:42:57 +0100 Subject: [PATCH 159/215] #1394 delete reroute key events code --- .../AccessibilityServiceController.kt | 3 - .../keymapper/base/BaseSingletonHiltModule.kt | 6 - .../base/promode/ProModeSetupScreen.kt | 1 - .../base/promode/SystemBridgeSetupUseCase.kt | 5 +- .../RerouteKeyEventsController.kt | 146 ------------------ .../RerouteKeyEventsUseCase.kt | 63 -------- .../base/settings/ConfigSettingsUseCase.kt | 4 - .../base/settings/SettingsViewModel.kt | 25 --- .../BaseAccessibilityServiceController.kt | 8 - base/src/main/res/values/strings.xml | 19 +-- .../io/github/sds100/keymapper/data/Keys.kt | 5 +- 11 files changed, 6 insertions(+), 279 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 292078944d..1f1b43b6f9 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -10,7 +10,6 @@ import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.promode.SystemBridgeSetupAssistantController -import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeRecorder import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController import io.github.sds100.keymapper.base.trigger.RecordTriggerController @@ -20,7 +19,6 @@ import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper class AccessibilityServiceController @AssistedInject constructor( @Assisted private val service: MyAccessibilityService, - rerouteKeyEventsControllerFactory: RerouteKeyEventsController.Factory, accessibilityNodeRecorderFactory: AccessibilityNodeRecorder.Factory, performActionsUseCaseFactory: PerformActionsUseCaseImpl.Factory, detectKeyMapsUseCaseFactory: DetectKeyMapsUseCaseImpl.Factory, @@ -34,7 +32,6 @@ class AccessibilityServiceController @AssistedInject constructor( setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory ) : BaseAccessibilityServiceController( service = service, - rerouteKeyEventsControllerFactory = rerouteKeyEventsControllerFactory, accessibilityNodeRecorderFactory = accessibilityNodeRecorderFactory, performActionsUseCaseFactory = performActionsUseCaseFactory, detectKeyMapsUseCaseFactory = detectKeyMapsUseCaseFactory, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt index abade43b31..5c0783bdc1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt @@ -26,8 +26,6 @@ import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCaseImpl -import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsUseCase -import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsUseCaseImpl import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCaseImpl @@ -153,10 +151,6 @@ abstract class BaseSingletonHiltModule { @Singleton abstract fun imeInputEvenInjector(impl: ImeInputEventInjectorImpl): ImeInputEventInjector - @Binds - @Singleton - abstract fun rerouteKeyEventsUseCase(impl: RerouteKeyEventsUseCaseImpl): RerouteKeyEventsUseCase - @Binds @Singleton abstract fun bindConfigKeyMapState(impl: ConfigKeyMapStateImpl): ConfigKeyMapState diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index 4bc9324af4..04060fdb54 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -370,7 +370,6 @@ private data class StepContent( val buttonText: String, ) -// Previews for each setup step @Preview(name = "Accessibility Service Step") @Composable private fun ProModeSetupScreenAccessibilityServicePreview() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 27ba263962..98a9c712cf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -153,8 +153,8 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( isDeveloperOptionsEnabled: Boolean, isWifiConnected: Boolean, isWirelessDebuggingEnabled: Boolean - ): SystemBridgeSetupStep = - when { + ): SystemBridgeSetupStep { + return when { accessibilityServiceState != AccessibilityServiceState.ENABLED -> SystemBridgeSetupStep.ACCESSIBILITY_SERVICE !isNotificationPermissionGranted -> SystemBridgeSetupStep.NOTIFICATION_PERMISSION !isDeveloperOptionsEnabled -> SystemBridgeSetupStep.DEVELOPER_OPTIONS @@ -163,6 +163,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( isWirelessDebuggingEnabled && !systemBridgeSetupController.isAdbPaired() -> SystemBridgeSetupStep.ADB_PAIRING else -> SystemBridgeSetupStep.START_SERVICE } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt deleted file mode 100644 index 9dc545c9ac..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt +++ /dev/null @@ -1,146 +0,0 @@ -package io.github.sds100.keymapper.base.reroutekeyevents - -import android.view.KeyEvent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.github.sds100.keymapper.base.input.InjectKeyEventModel -import io.github.sds100.keymapper.base.input.InputEventDetectionSource -import io.github.sds100.keymapper.base.input.InputEventHub -import io.github.sds100.keymapper.base.input.InputEventHubCallback -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector -import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent -import io.github.sds100.keymapper.system.inputevents.KMInputEvent -import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch - -/** - * This is used for the feature created in issue #618 to fix the device IDs of key events - * on Android 11. There was a bug in the system where enabling an accessibility service - * would reset the device ID of key events to -1. - */ -// TODO remove this feature because it is extra maintenance for a bug that only exists on a small amount of devices. -// TODO update changelog and website, remove strings. -class RerouteKeyEventsController @AssistedInject constructor( - @Assisted - private val coroutineScope: CoroutineScope, - private val keyMapperImeMessenger: ImeInputEventInjector, - private val useCase: RerouteKeyEventsUseCase, - private val inputEventHub: InputEventHub, -) : InputEventHubCallback { - - companion object { - private const val INPUT_EVENT_HUB_ID = "reroute_key_events" - } - - @AssistedFactory - interface Factory { - fun create( - coroutineScope: CoroutineScope, - ): RerouteKeyEventsController - } - - /** - * The job of the key that should be repeating. This should be a down key event for the last - * key that has been pressed down. - * The old job should be cancelled whenever the key has been released - * or a new key has been pressed down - */ - private var repeatJob: Job? = null - - init { - coroutineScope.launch { - useCase.isReroutingEnabled.collect { isEnabled -> - if (isEnabled) { - inputEventHub.registerClient( - INPUT_EVENT_HUB_ID, - this@RerouteKeyEventsController, - listOf(KMEvdevEvent.TYPE_KEY_EVENT), - ) - } else { - inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) - } - } - } - } - - fun teardown() { - inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) - } - - override fun onInputEvent( - event: KMInputEvent, - detectionSource: InputEventDetectionSource, - ): Boolean { - if (event !is KMKeyEvent) { - return false - } - - if (!useCase.shouldRerouteKeyEvent(event.device.descriptor)) { - return false - } - - return when (event.action) { - KeyEvent.ACTION_DOWN -> onKeyDown(event) - KeyEvent.ACTION_UP -> onKeyUp(event) - else -> false - } - } - - /** - * @return whether to consume the key event. - */ - private fun onKeyDown( - event: KMKeyEvent, - ): Boolean { - val injectModel = InjectKeyEventModel( - keyCode = event.keyCode, - action = KeyEvent.ACTION_DOWN, - metaState = event.metaState, - deviceId = event.deviceId, - scanCode = event.scanCode, - repeatCount = event.repeatCount, - source = event.source, - ) - - useCase.inputKeyEvent(injectModel) - - repeatJob?.cancel() - - repeatJob = coroutineScope.launch { - delay(400) - - var repeatCount = 1 - - while (isActive) { - useCase.inputKeyEvent(injectModel.copy(repeatCount = repeatCount)) - delay(50) - repeatCount++ - } - } - - return true - } - - private fun onKeyUp(event: KMKeyEvent): Boolean { - repeatJob?.cancel() - - val inputKeyModel = InjectKeyEventModel( - keyCode = event.keyCode, - action = KeyEvent.ACTION_UP, - metaState = event.metaState, - deviceId = event.deviceId, - scanCode = event.scanCode, - repeatCount = event.repeatCount, - source = event.source, - ) - - useCase.inputKeyEvent(inputKeyModel) - - return true - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt deleted file mode 100644 index 7e12942192..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt +++ /dev/null @@ -1,63 +0,0 @@ -package io.github.sds100.keymapper.base.reroutekeyevents - -import android.os.Build -import io.github.sds100.keymapper.base.input.InjectKeyEventModel -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector -import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper -import io.github.sds100.keymapper.common.BuildConfigProvider -import io.github.sds100.keymapper.common.utils.firstBlocking -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Singleton - -/** - * This is used for the feature created in issue #618 to fix the device IDs of key events - * on Android 11. There was a bug in the system where enabling an accessibility service - * would reset the device ID of key events to -1. - */ -@Singleton -class RerouteKeyEventsUseCaseImpl @Inject constructor( - // MUST use the input event injector instead of the InputManager to bypass the buggy code in Android. - private val imeInputEventInjector: ImeInputEventInjector, - private val inputMethodAdapter: InputMethodAdapter, - private val preferenceRepository: PreferenceRepository, - private val buildConfigProvider: BuildConfigProvider, -) : RerouteKeyEventsUseCase { - - override val isReroutingEnabled: Flow = - preferenceRepository.get(Keys.rerouteKeyEvents).map { it ?: false } - - private val devicesToRerouteKeyEvents = - preferenceRepository.get(Keys.devicesToRerouteKeyEvents).map { it ?: emptyList() } - - private val imeHelper by lazy { - KeyMapperImeHelper( - inputMethodAdapter, - buildConfigProvider.packageName, - ) - } - - override fun shouldRerouteKeyEvent(descriptor: String?): Boolean { - if (Build.VERSION.SDK_INT != Build.VERSION_CODES.R) { - return false - } - - return isReroutingEnabled.firstBlocking() && - imeHelper.isCompatibleImeChosen() && - (descriptor != null && devicesToRerouteKeyEvents.firstBlocking().contains(descriptor)) - } - - override fun inputKeyEvent(keyEvent: InjectKeyEventModel) { - imeInputEventInjector.inputKeyEvent(keyEvent) - } -} - -interface RerouteKeyEventsUseCase { - val isReroutingEnabled: Flow - fun shouldRerouteKeyEvent(descriptor: String?): Boolean - fun inputKeyEvent(keyEvent: InjectKeyEventModel) -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index 8bd5304bf9..51783a0e22 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -79,9 +79,6 @@ class ConfigSettingsUseCaseImpl @Inject constructor( } } - override val rerouteKeyEvents: Flow = - preferences.get(Keys.rerouteKeyEvents).map { it ?: false } - override val isCompatibleImeChosen: Flow = inputMethodAdapter.chosenIme.map { imeHelper.isCompatibleImeChosen() } @@ -213,7 +210,6 @@ interface ConfigSettingsUseCase { fun downloadShizuku() fun openShizukuApp() - val rerouteKeyEvents: Flow val isCompatibleImeChosen: Flow val isCompatibleImeEnabled: Flow suspend fun enableCompatibleIme() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index a245072a76..6683453555 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -46,31 +46,6 @@ class SettingsViewModel @Inject constructor( NavigationProvider by navigationProvider, DefaultOptionsSettingsCallback { - val isWriteSecureSettingsPermissionGranted: StateFlow = - useCase.isWriteSecureSettingsGranted - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - val isShizukuInstalled: StateFlow = - useCase.isShizukuInstalled - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - val isShizukuStarted: StateFlow = - useCase.isShizukuStarted - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - val isShizukuPermissionGranted: StateFlow = - useCase.isShizukuPermissionGranted - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - val rerouteKeyEvents: StateFlow = useCase.rerouteKeyEvents - .stateIn(viewModelScope, SharingStarted.Lazily, false) - - val isCompatibleImeChosen: StateFlow = useCase.isCompatibleImeChosen - .stateIn(viewModelScope, SharingStarted.Lazily, false) - - val isCompatibleImeEnabled: StateFlow = useCase.isCompatibleImeEnabled - .stateIn(viewModelScope, SharingStarted.Lazily, false) - val defaultLongPressDelay: Flow = useCase.defaultLongPressDelay val defaultDoublePressDelay: Flow = useCase.defaultDoublePressDelay val defaultRepeatDelay: Flow = useCase.defaultRepeatDelay diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index b7e54b5b4f..4627eea10b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -22,7 +22,6 @@ import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCa import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent import io.github.sds100.keymapper.base.promode.SystemBridgeSetupAssistantController -import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.hasFlag @@ -55,7 +54,6 @@ import timber.log.Timber abstract class BaseAccessibilityServiceController( private val service: BaseAccessibilityService, - private val rerouteKeyEventsControllerFactory: RerouteKeyEventsController.Factory, private val accessibilityNodeRecorderFactory: AccessibilityNodeRecorder.Factory, private val performActionsUseCaseFactory: PerformActionsUseCaseImpl.Factory, private val detectKeyMapsUseCaseFactory: DetectKeyMapsUseCaseImpl.Factory, @@ -101,11 +99,6 @@ abstract class BaseAccessibilityServiceController( detectConstraintsUseCase, ) - val rerouteKeyEventsController: RerouteKeyEventsController = - rerouteKeyEventsControllerFactory.create( - service.lifecycleScope, - ) - val accessibilityNodeRecorder = accessibilityNodeRecorderFactory.create(service) private val setupAssistantController: SystemBridgeSetupAssistantController? = @@ -359,7 +352,6 @@ abstract class BaseAccessibilityServiceController( keyMapDetectionController.teardown() keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_ACCESSIBILITY_SERVICE) accessibilityNodeRecorder.teardown() - rerouteKeyEventsController.teardown() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { setupAssistantController?.teardown() diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 37624bfa5d..cf63e7282c 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -655,27 +655,10 @@ Show device IDs Differentiate devices with the same name - Fix keyboards that are set to US English - This fixes keyboards that don\'t have the correct the keyboard layout when an accessibility service is enabled. Tap to read more and configure. - - Fix keyboards that are set to US English - There is a bug in Android 11 that turning on an accessibility service makes Android think all external devices are the same internal virtual device. Because it can\'t identify these devices correctly, it doesn\'t know which keyboard layout to use with them so it defaults to US English even if it is a German keyboard for example. You can use Key Mapper to work around this problem by following the steps below. - - 4. Choose devices - - 1. Install the Key Mapper GUI Keyboard (optional) - 1. Install the Key Mapper Leanback Keyboard (optional) - - 2. Enable the Key Mapper GUI Keyboard or the Key Mapper Basic Input Method - 2. Enable the Key Mapper Leanback Keyboard or the Key Mapper Basic Input Method - - 3. Use the keyboard that you just enabled - (Recommended) Read user guide for this setting. - Enable extra logging Record more detailed logs View log - Share this with the developer if having issues + Share this with the developer if you\'re having issues Report issue diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index be74b2a00f..907b46ee23 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -60,9 +60,8 @@ object Keys { val fingerprintGesturesAvailable = booleanPreferencesKey("fingerprint_gestures_available") - val rerouteKeyEvents = booleanPreferencesKey("key_reroute_key_events_from_specified_devices") - val devicesToRerouteKeyEvents = - stringSetPreferencesKey("key_devices_to_reroute_key_events") +// val rerouteKeyEvents = booleanPreferencesKey("key_reroute_key_events_from_specified_devices") +// val devicesToRerouteKeyEvents = stringSetPreferencesKey("key_devices_to_reroute_key_events") val log = booleanPreferencesKey("key_log") val shownShizukuPermissionPrompt = booleanPreferencesKey("key_shown_shizuku_permission_prompt") From 7ee0cd7e89eb7e4f8a031e1a9b1bc9f32c146f26 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 24 Aug 2025 22:48:18 +0100 Subject: [PATCH 160/215] #1394 delete code for showing ime picker notifications --- .../ManageNotificationsUseCase.kt | 59 ------------------- .../notifications/NotificationController.kt | 44 ++------------ base/src/main/res/values/strings.xml | 15 ----- .../io/github/sds100/keymapper/data/Keys.kt | 2 +- 4 files changed, 5 insertions(+), 115 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt index 752b54d27e..f4d0d2c1ad 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt @@ -1,73 +1,18 @@ package io.github.sds100.keymapper.base.system.notifications -import android.os.Build -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.notifications.NotificationAdapter import io.github.sds100.keymapper.system.notifications.NotificationChannelModel import io.github.sds100.keymapper.system.notifications.NotificationModel import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import javax.inject.Inject class ManageNotificationsUseCaseImpl @Inject constructor( - private val preferences: PreferenceRepository, private val notificationAdapter: NotificationAdapter, - private val suAdapter: SuAdapter, private val permissionAdapter: PermissionAdapter, ) : ManageNotificationsUseCase { - override val showImePickerNotification: Flow = - combine( - suAdapter.isRootGranted, - preferences.get(Keys.showImePickerNotification), - ) { hasRootPermission, show -> - when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.O -> show ?: false - - /* - always show the notification on Oreo+ because the system/user controls - whether notifications are shown. - */ - Build.VERSION.SDK_INT == Build.VERSION_CODES.O -> true - - /* - This needs root permission on API 27 and 28 - */ - ( - Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1 || - Build.VERSION.SDK_INT == Build.VERSION_CODES.P - ) && - hasRootPermission -> true - - else -> false - } - } - - override val showToggleKeyboardNotification: Flow = - preferences.get(Keys.showToggleKeyboardNotification).map { - // always show the notification on Oreo+ because the system/user controls whether notifications are shown - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - true - } else { - it ?: true - } - } - - override val showToggleMappingsNotification: Flow = - preferences.get(Keys.showToggleKeyMapsNotification).map { - // always show the notification on Oreo+ because the system/user controls whether notifications are shown - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - true - } else { - it ?: true - } - } - override val onActionClick: Flow = notificationAdapter.onNotificationActionClick override fun show(notification: NotificationModel) { @@ -92,10 +37,6 @@ class ManageNotificationsUseCaseImpl @Inject constructor( } interface ManageNotificationsUseCase { - val showImePickerNotification: Flow - val showToggleKeyboardNotification: Flow - val showToggleMappingsNotification: Flow - /** * The string is the ID of the action. */ diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 4f605fc21c..92a99cca3c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -51,7 +51,7 @@ class NotificationController @Inject constructor( ) : ResourceProvider by resourceProvider { companion object { - private const val ID_IME_PICKER = 123 + // private const val ID_IME_PICKER = 123 private const val ID_KEYBOARD_HIDDEN = 747 private const val ID_TOGGLE_MAPPINGS = 231 private const val ID_TOGGLE_KEYBOARD = 143 @@ -115,6 +115,7 @@ class NotificationController @Inject constructor( fun init() { manageNotifications.deleteChannel(CHANNEL_ID_WARNINGS) manageNotifications.deleteChannel(CHANNEL_ID_PERSISTENT) + manageNotifications.deleteChannel(CHANNEL_IME_PICKER) manageNotifications.createChannel( NotificationChannelModel( @@ -125,28 +126,10 @@ class NotificationController @Inject constructor( ) combine( - manageNotifications.showToggleMappingsNotification, controlAccessibilityService.serviceState, pauseMappings.isPaused, - ) { show, serviceState, areMappingsPaused -> - invalidateToggleMappingsNotification(show, serviceState, areMappingsPaused) - }.flowOn(dispatchers.default()).launchIn(coroutineScope) - - manageNotifications.showImePickerNotification.onEach { show -> - if (show) { - manageNotifications.createChannel( - NotificationChannelModel( - id = CHANNEL_IME_PICKER, - name = getString(R.string.notification_channel_ime_picker), - NotificationManagerCompat.IMPORTANCE_MIN, - ), - ) - - manageNotifications.show(imePickerNotification()) - } else { - // don't delete the channel because then the user's notification config is lost - manageNotifications.dismiss(ID_IME_PICKER) - } + ) { serviceState, areMappingsPaused -> + invalidateToggleMappingsNotification(serviceState, areMappingsPaused) }.flowOn(dispatchers.default()).launchIn(coroutineScope) toggleCompatibleIme.sufficientPermissions.onEach { canToggleIme -> @@ -221,7 +204,6 @@ class NotificationController @Inject constructor( coroutineScope.launch { invalidateToggleMappingsNotification( - show = manageNotifications.showToggleMappingsNotification.first(), serviceState = controlAccessibilityService.serviceState.first(), areMappingsPaused = pauseMappings.isPaused.first(), ) @@ -245,7 +227,6 @@ class NotificationController @Inject constructor( } private fun invalidateToggleMappingsNotification( - show: Boolean, serviceState: AccessibilityServiceState, areMappingsPaused: Boolean, ) { @@ -257,11 +238,6 @@ class NotificationController @Inject constructor( ), ) - if (!show) { - manageNotifications.dismiss(ID_TOGGLE_MAPPINGS) - return - } - when (serviceState) { AccessibilityServiceState.ENABLED -> { if (areMappingsPaused) { @@ -409,18 +385,6 @@ class NotificationController @Inject constructor( ) } - private fun imePickerNotification(): NotificationModel = NotificationModel( - id = ID_IME_PICKER, - channel = CHANNEL_IME_PICKER, - title = getString(R.string.notification_ime_persistent_title), - text = getString(R.string.notification_ime_persistent_text), - icon = R.drawable.ic_notification_keyboard, - onClickAction = NotificationIntentType.Broadcast(actionShowImePicker), - showOnLockscreen = false, - onGoing = true, - priority = NotificationCompat.PRIORITY_MIN, - ) - private fun toggleImeNotification(): NotificationModel = NotificationModel( id = ID_TOGGLE_KEYBOARD, channel = CHANNEL_TOGGLE_KEYBOARD, diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index cf63e7282c..9b4c01e35b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -555,9 +555,6 @@ Toggle Key Mapper keyboard New features - Tap to change your keyboard. - Keyboard picker - Running Tap to open Key Mapper. Pause @@ -612,9 +609,6 @@ Make all key maps vibrate Every time a key map is triggered - Keyboard picker notification - Show a persistent notification to allow you to pick a keyboard. - Show pause/resume notification Toggle your key maps on and off @@ -622,12 +616,6 @@ Turn on automatic backup Periodically back up your key maps - Choose devices - Choose which devices will show the input method picker - - Automatically show keyboard picker - When a device that you have chosen connects or disconnects the keyboard picker will show automatically. Choose the devices below. - Automatically change the on-screen keyboard when a device (e.g a keyboard) connects/disconnects The last used Key Mapper keyboard will be automatically selected when a chosen device is connected. Your normal keyboard will be automatically selected when the device disconnects. @@ -696,9 +684,6 @@ - Keyboard picker - Tap to see the settings that allow you to automatically show the keyboard picker. - Root settings These options will only work on root devices! If you don\'t know what root is or whether your device is rooted, please don\'t leave a poor review if they don\'t work. :) diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 907b46ee23..4b5ea54454 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -12,7 +12,7 @@ object Keys { 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") val showToggleKeyboardNotification = booleanPreferencesKey("pref_toggle_key_mapper_keyboard_notification") From 9b8ebd2a7eb9b2bc4ee505f4dea14a64a4d2d3e5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 25 Aug 2025 17:25:32 +0100 Subject: [PATCH 161/215] #1394 use system bridge for wifi actions --- .../keymapper/base/actions/ActionUtils.kt | 13 +- .../keymapper/base/input/EvdevHandleCache.kt | 34 +++-- .../keymapper/base/input/InputEventHub.kt | 132 ++++++++---------- .../sds100/keymapper/base/utils/ErrorUtils.kt | 3 +- .../keymapper/sysbridge/ISystemBridge.aidl | 2 + .../manager/SystemBridgeConnection.kt | 9 -- .../manager/SystemBridgeConnectionManager.kt | 49 +++---- .../sysbridge/service/SystemBridge.kt | 12 +- system/build.gradle.kts | 1 + .../system/network/AndroidNetworkAdapter.kt | 6 +- .../aidl/android/net/wifi/IWifiManager.aidl | 5 + 11 files changed, 124 insertions(+), 142 deletions(-) delete mode 100644 sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnection.kt create mode 100644 systemstubs/src/main/aidl/android/net/wifi/IWifiManager.aidl diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index 903c562c44..5d502d4ef1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -572,12 +572,13 @@ object ActionUtils { fun getRequiredPermissions(id: ActionId): List { when (id) { - ActionId.TOGGLE_WIFI, - ActionId.ENABLE_WIFI, - ActionId.DISABLE_WIFI, - -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return listOf(Permission.ROOT) - } + // TODO show action error if pro mode is not started +// ActionId.TOGGLE_WIFI, +// ActionId.ENABLE_WIFI, +// ActionId.DISABLE_WIFI, +// -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { +// return listOf(Permission.ROOT) +// } ActionId.TOGGLE_MOBILE_DATA, ActionId.ENABLE_MOBILE_DATA, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt index 9b6923a5ba..e8e55a76d0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt @@ -1,9 +1,12 @@ package io.github.sds100.keymapper.base.input -import android.os.RemoteException +import android.os.Build +import androidx.annotation.RequiresApi import io.github.sds100.keymapper.common.models.EvdevDeviceHandle import io.github.sds100.keymapper.common.models.EvdevDeviceInfo -import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.common.utils.onFailure +import io.github.sds100.keymapper.common.utils.valueIfFailure +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.devices.DevicesAdapter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -14,24 +17,27 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import timber.log.Timber +@RequiresApi(Build.VERSION_CODES.Q) class EvdevHandleCache( private val coroutineScope: CoroutineScope, private val devicesAdapter: DevicesAdapter, - private val systemBridge: StateFlow, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager ) { private val devicesByPath: StateFlow> = - combine(devicesAdapter.connectedInputDevices, systemBridge) { _, systemBridge -> - systemBridge ?: return@combine emptyMap() - - try { - // Do it on a separate thread in case there is deadlock - withContext(Dispatchers.IO) { - systemBridge.evdevInputDevices.associateBy { it.path } - } - } catch (e: RemoteException) { - Timber.e("Failed to get evdev input devices from system bridge $e") - emptyMap() + combine( + devicesAdapter.connectedInputDevices, + systemBridgeConnectionManager.isConnected + ) { _, isConnected -> + if (!isConnected) { + return@combine emptyMap() } + + // Do it on a separate thread in case there is deadlock + withContext(Dispatchers.IO) { + systemBridgeConnectionManager.run { bridge -> bridge.evdevInputDevices.associateBy { it.path } } + }.onFailure { error -> + Timber.e("Failed to get evdev input devices from system bridge $error") + }.valueIfFailure { emptyMap() } }.stateIn(coroutineScope, SharingStarted.Eagerly, emptyMap()) fun getDevices(): List { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index b53a2d9c71..aae9fa80e5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -3,19 +3,21 @@ package io.github.sds100.keymapper.base.input import android.os.Build import android.os.RemoteException import android.view.KeyEvent +import androidx.annotation.RequiresApi import io.github.sds100.keymapper.base.BuildConfig import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.firstBlocking +import io.github.sds100.keymapper.common.utils.isError +import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.IEvdevCallback -import io.github.sds100.keymapper.sysbridge.ISystemBridge -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnection import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager -import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent @@ -24,12 +26,11 @@ import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -40,7 +41,7 @@ import javax.inject.Singleton @Singleton class InputEventHubImpl @Inject constructor( private val coroutineScope: CoroutineScope, - private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val systemBridgeConnManager: SystemBridgeConnectionManager, private val imeInputEventInjector: ImeInputEventInjector, private val preferenceRepository: PreferenceRepository, private val devicesAdapter: DevicesAdapter, @@ -52,34 +53,14 @@ class InputEventHubImpl @Inject constructor( private val clients: ConcurrentHashMap = ConcurrentHashMap() - private var systemBridgeFlow: MutableStateFlow = MutableStateFlow(null) - // Event queue for processing key events asynchronously in order private val keyEventQueue = Channel(capacity = 100) - private val systemBridgeConnection: SystemBridgeConnection = object : SystemBridgeConnection { - override fun onServiceConnected(service: ISystemBridge) { - Timber.i("InputEventHub connected to SystemBridge") - - systemBridgeFlow.update { service } - service.registerEvdevCallback(this@InputEventHubImpl) - } - - override fun onServiceDisconnected(service: ISystemBridge) { - Timber.i("InputEventHub disconnected from SystemBridge") - - systemBridgeFlow.update { null } - } - - override fun onBindingDied() { - Timber.i("SystemBridge connection died") - } - } - + @RequiresApi(Build.VERSION_CODES.Q) private val evdevHandles: EvdevHandleCache = EvdevHandleCache( coroutineScope, devicesAdapter, - systemBridgeFlow, + systemBridgeConnManager, ) private val logInputEventsEnabled: StateFlow = @@ -92,11 +73,19 @@ class InputEventHubImpl @Inject constructor( }.stateIn(coroutineScope, SharingStarted.Eagerly, false) init { + startKeyEventProcessingLoop() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - systemBridgeConnectionManager.registerConnection(systemBridgeConnection) + coroutineScope.launch { + systemBridgeConnManager.isConnected.collect { connected -> + if (connected) { + systemBridgeConnManager.run { bridge -> + bridge.registerEvdevCallback(this@InputEventHubImpl) + } + } + } + } } - - startKeyEventProcessingLoop() } /** @@ -114,8 +103,9 @@ class InputEventHubImpl @Inject constructor( } } + @RequiresApi(Build.VERSION_CODES.Q) override fun isSystemBridgeConnected(): Boolean { - return systemBridgeFlow.value != null + return systemBridgeConnManager.isConnected.firstBlocking() } override fun onEvdevEventLoopStarted() { @@ -123,6 +113,7 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedEvdevDevices() } + @RequiresApi(Build.VERSION_CODES.Q) override fun onEvdevEvent( devicePath: String?, timeSec: Long, @@ -249,6 +240,7 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedEvdevDevices() } + @RequiresApi(Build.VERSION_CODES.Q) override fun grabAllEvdevDevices(clientId: String) { if (!clients.containsKey(clientId)) { throw IllegalArgumentException("This client $clientId is not registered when trying to grab devices!") @@ -260,31 +252,31 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedEvdevDevices() } - // TODO invalidate when the input devices change + // TODO invalidate when the input devices change. Or NOT because could be an infinite loop? private fun invalidateGrabbedEvdevDevices() { - val evdevDevices: Set = - clients.values.flatMap { it.grabbedEvdevDevices }.toSet() - - val systemBridge = systemBridgeFlow.value - - if (systemBridge == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return } + val evdevDevices: Set = + clients.values.flatMap { it.grabbedEvdevDevices }.toSet() + // Grabbing can block if there are other grabbing or event loop start/stop operations happening. coroutineScope.launch(Dispatchers.IO) { try { - val ungrabResult = systemBridge.ungrabAllEvdevDevices() + val ungrabResult = + systemBridgeConnManager.run { bridge -> bridge.ungrabAllEvdevDevices() } Timber.i("Ungrabbed all evdev devices: $ungrabResult") - if (!ungrabResult) { + if (ungrabResult.isError) { Timber.e("Failed to ungrab all evdev devices before grabbing.") return@launch } for (device in evdevDevices) { val handle = evdevHandles.getByInfo(device) ?: continue - val grabResult = systemBridge.grabEvdevDevice(handle.path) + val grabResult = + systemBridgeConnManager.run { bridge -> bridge.grabEvdevDevice(handle.path) } Timber.i("Grabbed evdev device ${device.name}: $grabResult") } @@ -294,63 +286,49 @@ class InputEventHubImpl @Inject constructor( } } + @RequiresApi(Build.VERSION_CODES.Q) override fun injectEvdevEvent( devicePath: String, type: Int, code: Int, value: Int, ): KMResult { - val systemBridge = this.systemBridgeFlow.value - - if (systemBridge == null) { - Timber.w("System bridge is not connected, cannot inject evdev event.") - return SystemBridgeError.Disconnected - } - - try { - val result = systemBridge.writeEvdevEvent( + return systemBridgeConnManager.run { bridge -> + bridge.writeEvdevEvent( devicePath, type, code, value, ) - - Timber.d("Injected evdev event: $result") - - return Success(result) - } catch (e: RemoteException) { - Timber.e(e, "Failed to inject evdev event") - return KMError.Exception(e) + }.onSuccess { + Timber.d("Injected evdev event: $it") } } override suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult { - val systemBridge = this.systemBridgeFlow.value + val isSysBridgeConnected = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + systemBridgeConnManager.isConnected.first() - if (systemBridge == null) { - imeInputEventInjector.inputKeyEvent(event) - return Success(Unit) - } else { - try { - val androidKeyEvent = event.toAndroidKeyEvent(flags = KeyEvent.FLAG_FROM_SYSTEM) + if (isSysBridgeConnected) { + val androidKeyEvent = event.toAndroidKeyEvent(flags = KeyEvent.FLAG_FROM_SYSTEM) - if (logInputEventsEnabled.value) { - Timber.d("Injecting key event $androidKeyEvent with system bridge") - } + if (logInputEventsEnabled.value) { + Timber.d("Injecting key event $androidKeyEvent with system bridge") + } - withContext(Dispatchers.IO) { - // All injected events have their device id set to -1 (VIRTUAL_KEYBOARD_ID) - // in InputDispatcher.cpp injectInputEvent. - systemBridge.injectInputEvent( + return withContext(Dispatchers.IO) { + // All injected events have their device id set to -1 (VIRTUAL_KEYBOARD_ID) + // in InputDispatcher.cpp injectInputEvent. + systemBridgeConnManager.run { bridge -> + bridge.injectInputEvent( androidKeyEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH, ) - } - - return Success(Unit) - } catch (e: RemoteException) { - return KMError.Exception(e) + }.then { Success(Unit) } } + } else { + imeInputEventInjector.inputKeyEvent(event) + return Success(Unit) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt index 2dedb5f341..4c2dff014c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt @@ -173,7 +173,8 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { KMError.InvalidBackup -> resourceProvider.getString(R.string.error_invalid_backup) KMError.MalformedUrl -> resourceProvider.getString(R.string.error_malformed_url) KMError.UiElementNotFound -> resourceProvider.getString(R.string.error_ui_element_not_found) - else -> throw IllegalArgumentException("Unknown error $this") + + else -> this.toString() } } diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 7578496fbd..f1d44011e7 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -23,4 +23,6 @@ interface ISystemBridge { boolean injectInputEvent(in InputEvent event, int mode) = 8; EvdevDeviceHandle[] getEvdevInputDevices() = 9; + + boolean setWifiEnabled(boolean enable) = 10; } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnection.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnection.kt deleted file mode 100644 index 2955a1fb4a..0000000000 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnection.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.sds100.keymapper.sysbridge.manager - -import io.github.sds100.keymapper.sysbridge.ISystemBridge - -interface SystemBridgeConnection { - fun onServiceConnected(service: ISystemBridge) - fun onServiceDisconnected(service: ISystemBridge) - fun onBindingDied() -} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 95656610eb..77f3b65443 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -6,7 +6,11 @@ import android.os.IBinder import android.os.IBinder.DeathRecipient import android.os.RemoteException import androidx.annotation.RequiresApi +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map @@ -23,28 +27,19 @@ class SystemBridgeConnectionManagerImpl @Inject constructor() : SystemBridgeConn // TODO if auto start is turned on, subscribe to Shizuku Binder listener and when bound, start the service. But only do this once per app process session. If the user stops the service it should remain stopped until key mapper is killed, private val systemBridgeLock: Any = Any() - private var systemBridge: MutableStateFlow = MutableStateFlow(null) + private var systemBridgeFlow: MutableStateFlow = MutableStateFlow(null) - override val isConnected: Flow = systemBridge.map { it != null } - - private val connectionsLock: Any = Any() - private val connections: MutableSet = mutableSetOf() + override val isConnected: Flow = systemBridgeFlow.map { it != null } private val deathRecipient: DeathRecipient = DeathRecipient { synchronized(systemBridgeLock) { - systemBridge.update { null } - } - - synchronized(connectionsLock) { - for (connection in connections) { - connection.onBindingDied() - } + systemBridgeFlow.update { null } } } fun pingBinder(): Boolean { synchronized(systemBridgeLock) { - return systemBridge.value?.asBinder()?.pingBinder() == true + return systemBridgeFlow.value?.asBinder()?.pingBinder() == true } } @@ -56,32 +51,24 @@ class SystemBridgeConnectionManagerImpl @Inject constructor() : SystemBridgeConn synchronized(systemBridgeLock) { systemBridge.asBinder().linkToDeath(deathRecipient, 0) - this.systemBridge.update { systemBridge } - } - - synchronized(connectionsLock) { - for (connection in connections) { - connection.onServiceConnected(systemBridge) - } + this.systemBridgeFlow.update { systemBridge } } } - override fun registerConnection(connection: SystemBridgeConnection) { - synchronized(connectionsLock) { - connections.add(connection) - } - } + override fun run(block: (ISystemBridge) -> T): KMResult { + try { + val systemBridge = systemBridgeFlow.value ?: return SystemBridgeError.Disconnected - override fun unregisterConnection(connection: SystemBridgeConnection) { - synchronized(connectionsLock) { - connections.remove(connection) + return Success(block(systemBridge)) + } catch (e: RemoteException) { + return KMError.Exception(e) } } override fun stopSystemBridge() { synchronized(systemBridgeLock) { try { - systemBridge.value?.destroy() + systemBridgeFlow.value?.destroy() } catch (_: RemoteException) { deathRecipient.binderDied() } @@ -95,8 +82,6 @@ class SystemBridgeConnectionManagerImpl @Inject constructor() : SystemBridgeConn interface SystemBridgeConnectionManager { val isConnected: Flow - fun registerConnection(connection: SystemBridgeConnection) - fun unregisterConnection(connection: SystemBridgeConnection) - + fun run(block: (ISystemBridge) -> T): KMResult fun stopSystemBridge() } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 804d2a774c..c00b00243f 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.IContentProvider import android.ddm.DdmHandleAppName import android.hardware.input.IInputManager +import android.net.wifi.IWifiManager import android.os.Binder import android.os.Bundle import android.os.Handler @@ -58,6 +59,7 @@ internal class SystemBridge : ISystemBridge.Stub() { companion object { private const val TAG: String = "KeyMapperSystemBridge" private val packageName: String? = System.getProperty("keymapper_sysbridge.package") + private const val SHELL_PACKAGE = "com.android.shell" @JvmStatic fun main(args: Array) { @@ -166,7 +168,6 @@ internal class SystemBridge : ISystemBridge.Stub() { } } - private val inputManager: IInputManager private val coroutineScope: CoroutineScope = MainScope() private val mainHandler = Handler(Looper.myLooper()!!) @@ -179,6 +180,9 @@ internal class SystemBridge : ISystemBridge.Stub() { } } + private val inputManager: IInputManager + private val wifiManager: IWifiManager + init { val libraryPath = System.getProperty("keymapper_sysbridge.library.path") @SuppressLint("UnsafeDynamicallyLoadedCode") @@ -194,6 +198,8 @@ internal class SystemBridge : ISystemBridge.Stub() { inputManager = IInputManager.Stub.asInterface(ServiceManager.getService(Context.INPUT_SERVICE)) + wifiManager = + IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) // TODO check that the key mapper app is installed, otherwise end the process. // val ai: ApplicationInfo? = rikka.shizuku.server.ShizukuService.getManagerApplicationInfo() @@ -282,6 +288,10 @@ internal class SystemBridge : ISystemBridge.Stub() { return getEvdevDevicesNative() } + override fun setWifiEnabled(enable: Boolean): Boolean { + return wifiManager.setWifiEnabled(SHELL_PACKAGE, enable) + } + override fun writeEvdevEvent(devicePath: String?, type: Int, code: Int, value: Int): Boolean { devicePath ?: return false return writeEvdevEventNative(devicePath, type, code, value) diff --git a/system/build.gradle.kts b/system/build.gradle.kts index d38a35332e..b70b399c4d 100644 --- a/system/build.gradle.kts +++ b/system/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(project(":common")) implementation(project(":data")) implementation(project(":systemstubs")) + implementation(project(":sysbridge")) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 58604ca38a..d86a2b318c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -19,6 +19,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -39,6 +40,7 @@ import javax.inject.Singleton class AndroidNetworkAdapter @Inject constructor( @ApplicationContext private val context: Context, private val suAdapter: SuAdapter, + private val systemBridgeConnManager: SystemBridgeConnectionManager ) : NetworkAdapter { private val ctx = context.applicationContext private val wifiManager: WifiManager by lazy { ctx.getSystemService()!! } @@ -131,7 +133,7 @@ class AndroidNetworkAdapter @Inject constructor( override fun enableWifi(): KMResult<*> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return suAdapter.execute("svc wifi enable") + return systemBridgeConnManager.run { bridge -> bridge.setWifiEnabled(true) } } else { wifiManager.isWifiEnabled = true return Success(Unit) @@ -140,7 +142,7 @@ class AndroidNetworkAdapter @Inject constructor( override fun disableWifi(): KMResult<*> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return suAdapter.execute("svc wifi disable") + return systemBridgeConnManager.run { bridge -> bridge.setWifiEnabled(false) } } else { wifiManager.isWifiEnabled = false return Success(Unit) diff --git a/systemstubs/src/main/aidl/android/net/wifi/IWifiManager.aidl b/systemstubs/src/main/aidl/android/net/wifi/IWifiManager.aidl new file mode 100644 index 0000000000..3aab61dd86 --- /dev/null +++ b/systemstubs/src/main/aidl/android/net/wifi/IWifiManager.aidl @@ -0,0 +1,5 @@ +package android.net.wifi; + +interface IWifiManager { + boolean setWifiEnabled(String packageName, boolean enable); +} \ No newline at end of file From 655b351964c9eb1b9e9484bd4d3ab8c78c8d3835 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 25 Aug 2025 17:55:28 +0100 Subject: [PATCH 162/215] #1394 create card explaining what PRO mode unlocks --- .../keymapper/base/promode/ProModeScreen.kt | 123 +++++++++++++++++- base/src/main/res/values/strings.xml | 7 +- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 55bbb9e038..13857bcb81 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -1,6 +1,11 @@ package io.github.sds100.keymapper.base.promode import android.os.Build +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box @@ -20,8 +25,10 @@ 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.automirrored.rounded.HelpOutline import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Checklist +import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Numbers import androidx.compose.material.icons.rounded.WarningAmber import androidx.compose.material3.BottomAppBar @@ -41,6 +48,9 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -66,11 +76,19 @@ fun ProModeScreen( ) { val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle() + var showInfoCard by remember { mutableStateOf(true) } - ProModeScreen(modifier = modifier, onBackClick = viewModel::onBackClick) { + ProModeScreen( + modifier = modifier, + onBackClick = viewModel::onBackClick, + onHelpClick = { showInfoCard = true }, + showHelpIcon = !showInfoCard + ) { Content( warningState = proModeWarningState, setupState = proModeSetupState, + showInfoCard = showInfoCard, + onInfoCardDismiss = { showInfoCard = false }, onWarningButtonClick = viewModel::onWarningButtonClick, onStopServiceClick = viewModel::onStopServiceClick, onShizukuButtonClick = viewModel::onShizukuButtonClick, @@ -85,6 +103,8 @@ fun ProModeScreen( private fun ProModeScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit = {}, + onHelpClick: () -> Unit = {}, + showHelpIcon: Boolean = false, content: @Composable () -> Unit, ) { Scaffold( @@ -92,6 +112,20 @@ private fun ProModeScreen( topBar = { TopAppBar( title = { Text(stringResource(R.string.pro_mode_app_bar_title)) }, + actions = { + AnimatedVisibility( + visible = showHelpIcon, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + IconButton(onClick = onHelpClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.HelpOutline, + contentDescription = stringResource(R.string.pro_mode_info_card_show_content_description) + ) + } + } + } ) }, bottomBar = { @@ -129,6 +163,8 @@ private fun Content( modifier: Modifier = Modifier, warningState: ProModeWarningState, setupState: State, + showInfoCard: Boolean, + onInfoCardDismiss: () -> Unit = {}, onWarningButtonClick: () -> Unit = {}, onShizukuButtonClick: () -> Unit = {}, onStopServiceClick: () -> Unit = {}, @@ -136,6 +172,23 @@ private fun Content( onSetupWithKeyMapperClick: () -> Unit = {}, ) { Column(modifier = modifier.verticalScroll(rememberScrollState())) { + AnimatedVisibility( + visible = showInfoCard, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + ProModeInfoCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + onDismiss = onInfoCardDismiss + ) + } + + if (showInfoCard) { + Spacer(modifier = Modifier.height(8.dp)) + } + WarningCard( modifier = Modifier .fillMaxWidth() @@ -463,6 +516,62 @@ private fun SetupCard( } } +@Composable +private fun ProModeInfoCard( + modifier: Modifier = Modifier, + onDismiss: () -> Unit = {} +) { + OutlinedCard( + modifier = modifier, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.Top + ) { + Column(modifier = Modifier.weight(1f)) { + Row { + Icon( + imageVector = Icons.AutoMirrored.Rounded.HelpOutline, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(R.string.pro_mode_info_card_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.pro_mode_info_card_description), + style = MaterialTheme.typography.bodyMedium, + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.pro_mode_info_card_dismiss_content_description), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + @Preview @Composable private fun Preview() { @@ -476,7 +585,9 @@ private fun Preview() { shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED, setupProgress = 0.5f ) - ) + ), + showInfoCard = true, + onInfoCardDismiss = {} ) } } @@ -489,7 +600,9 @@ private fun PreviewDark() { ProModeScreen { Content( warningState = ProModeWarningState.Understood, - setupState = State.Data(ProModeState.Started) + setupState = State.Data(ProModeState.Started), + showInfoCard = false, + onInfoCardDismiss = {} ) } } @@ -504,7 +617,9 @@ private fun PreviewCountingDown() { warningState = ProModeWarningState.CountingDown( seconds = 5, ), - setupState = State.Loading + setupState = State.Loading, + showInfoCard = true, + onInfoCardDismiss = {} ) } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 9b4c01e35b..a03b035a5b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1568,7 +1568,7 @@ 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 and volume buttons in the correct combination for a long time. + Remapping buttons with PRO mode is dangerous and can cause them to stop working if you remap them incorrectly.\n\nIf you make a mistake, you may need to force restart your device by holding down the power and volume buttons for 30 seconds — consult your device\'s manual or the internet for how to do this. %d… I understand Understood @@ -1636,4 +1636,9 @@ Pairing automatically Searching for pairing code and port… + What can I do with PRO mode? + 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: WiFi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device. + Show PRO mode info + Dismiss + From 857a228782c15613f4aa344c22e8769466b36a35 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 25 Aug 2025 18:02:19 +0100 Subject: [PATCH 163/215] rename WiFi to Wi-Fi in strings --- base/src/main/res/values/strings.xml | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index a03b035a5b..213db998da 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -151,7 +151,7 @@ Description for Key Mapper (required) Flags Sound file description - WiFi network SSID + Wi-Fi network SSID @@ -259,18 +259,18 @@ Flashlight is on Flashlight is off - WiFi is on - WiFi is off - Connected to a WiFi network - Disconnected from a WiFi network - You will have to type the SSID manually because apps aren\'t allowed to query the list of known WiFi networks on Android 10 and newer. + Wi-Fi is on + Wi-Fi is off + Connected to a Wi-Fi network + Disconnected from a Wi-Fi network + You will have to type the SSID manually because apps aren\'t allowed to query the list of known Wi-Fi networks on Android 10 and newer. - Leave it empty if any WiFi network should be matched. + Leave it empty if any Wi-Fi network should be matched. Any - Connected to %s WiFi - Disconnected from %s WiFi - Connected to any WiFi - Disconnected to no WiFi + Connected to %s Wi-Fi + Disconnected from %s Wi-Fi + Connected to any Wi-Fi + Disconnected to no Wi-Fi Input method is chosen %s is chosen @@ -805,7 +805,7 @@ Your device doesn\'t have a camera. Your device doesn\'t support NFC. Your device doesn\'t have a fingerprint reader. - Your device doesn\'t support WiFi. + Your device doesn\'t support Wi-Fi. Your device doesn\'t support Bluetooth. Your device doesn\'t support device policy enforcement. Your device doesn\'t have a camera flash. @@ -902,9 +902,9 @@ - Toggle WiFi - Enable WiFi - Disable WiFi + Toggle Wi-Fi + Enable Wi-Fi + Disable Wi-Fi Toggle Bluetooth Enable Bluetooth @@ -1607,8 +1607,8 @@ Enable developer options Key Mapper needs to use Android Debug Bridge to start PRO mode, and you need to enable developer options for that. - Connect to a WiFi network - Key Mapper needs a WiFi network to enable ADB. You do not need an internet connection.\n\nNo WiFi network? Use a hotspot from someone else\'s phone. + Connect to a Wi-Fi network + Key Mapper needs a Wi-Fi network to enable ADB. You do not need an internet connection.\n\nNo Wi-Fi network? Use a hotspot from someone else\'s phone. Enable wireless debugging Key Mapper uses wireless debugging to launch its remapping and input service. @@ -1637,7 +1637,7 @@ Searching for pairing code and port… What can I do with PRO mode? - 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: WiFi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device. + 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: Wi-Fi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device. Show PRO mode info Dismiss From 0c52160d29880aecc7884d8043ac54359ad40012 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 25 Aug 2025 18:25:41 +0100 Subject: [PATCH 164/215] fix: navigating back from choose action screen after replacing an action works --- .../sds100/keymapper/base/actions/ConfigActionsViewModel.kt | 3 ++- .../io/github/sds100/keymapper/base/promode/ProModeScreen.kt | 2 ++ .../keymapper/base/utils/navigation/NavigationProvider.kt | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index 236f682808..d08ace4625 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -186,10 +186,11 @@ class ConfigActionsViewModel @Inject constructor( override fun onReplaceClick() { val actionUid = actionOptionsUid.value ?: return viewModelScope.launch { + actionOptionsUid.update { null } + val newActionData = navigate("replace_action", NavDestination.ChooseAction) ?: return@launch - actionOptionsUid.update { null } config.setActionData(actionUid, newActionData) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 13857bcb81..8fe23b3339 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -76,6 +76,8 @@ fun ProModeScreen( ) { val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle() + + // TODO save this to preference repository var showInfoCard by remember { mutableStateOf(true) } ProModeScreen( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt index 402ace3c87..5b3bf43a13 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -106,6 +107,7 @@ class NavigationProviderImpl @Inject constructor() : NavigationProvider { // wait for the view to collect so navigating can happen _onNavigate.subscriptionCount.first { it > 0 } + Timber.d("Navigation: Navigating to ${event.destination} with key ${event.key}") _onNavigate.emit(event) } @@ -114,6 +116,7 @@ class NavigationProviderImpl @Inject constructor() : NavigationProvider { } override suspend fun popBackStack() { + Timber.d("Navigation: Popping back stack") _popBackStack.value = Unit } @@ -122,6 +125,8 @@ class NavigationProviderImpl @Inject constructor() : NavigationProvider { */ override suspend fun popBackStackWithResult(data: String) { _onReturnResult.subscriptionCount.first { it > 0 } + + Timber.d("Navigation: Popping back stack with result") _onReturnResult.emit(data) } } From 9c18c6c2ca494eb7bc8ea3a449b2fab6d30c1890 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 25 Aug 2025 18:37:11 +0100 Subject: [PATCH 165/215] #1394 only show the PRO mode info card if it has not been dismissed before --- .../keymapper/base/promode/ProModeScreen.kt | 16 +++++----------- .../keymapper/base/promode/ProModeViewModel.kt | 17 +++++++++++++++++ .../base/promode/SystemBridgeSetupUseCase.kt | 12 ++++++++++++ .../io/github/sds100/keymapper/data/Keys.kt | 3 +++ 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 8fe23b3339..ca13cd039f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -48,9 +48,6 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -77,20 +74,17 @@ fun ProModeScreen( val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle() - // TODO save this to preference repository - var showInfoCard by remember { mutableStateOf(true) } - ProModeScreen( modifier = modifier, onBackClick = viewModel::onBackClick, - onHelpClick = { showInfoCard = true }, - showHelpIcon = !showInfoCard + onHelpClick = { viewModel.showInfoCard() }, + showHelpIcon = !viewModel.showInfoCard ) { Content( warningState = proModeWarningState, setupState = proModeSetupState, - showInfoCard = showInfoCard, - onInfoCardDismiss = { showInfoCard = false }, + showInfoCard = viewModel.showInfoCard, + onInfoCardDismiss = { viewModel.hideInfoCard() }, onWarningButtonClick = viewModel::onWarningButtonClick, onStopServiceClick = viewModel::onStopServiceClick, onShizukuButtonClick = viewModel::onShizukuButtonClick, @@ -190,7 +184,7 @@ private fun Content( if (showInfoCard) { Spacer(modifier = Modifier.height(8.dp)) } - + WarningCard( modifier = Modifier .fillMaxWidth() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index d3fab1ca56..f516a053d0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -1,5 +1,8 @@ package io.github.sds100.keymapper.base.promode +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -59,6 +62,20 @@ class ProModeViewModel @Inject constructor( ::buildSetupState ).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + var showInfoCard by mutableStateOf(!useCase.isInfoDismissed()) + private set + + fun hideInfoCard() { + showInfoCard = false + // Save that they've dismissed the card so it is not shown by default the next + // time they visit the PRO mode page. + useCase.dismissInfo() + } + + fun showInfoCard() { + showInfoCard = true + } + private fun createWarningStateFlow(isUnderstood: Boolean): Flow = if (isUnderstood) { flowOf(ProModeWarningState.Understood) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 98a9c712cf..4041a22a3f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.base.promode import android.os.Build import androidx.annotation.RequiresApi import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository @@ -146,6 +147,14 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( systemBridgeSetupController.startWithAdb() } + override fun isInfoDismissed(): Boolean { + return preferences.get(Keys.isProModeInfoDismissed).map { it ?: false }.firstBlocking() + } + + override fun dismissInfo() { + preferences.set(Keys.isProModeInfoDismissed, true) + } + @RequiresApi(Build.VERSION_CODES.R) private suspend fun getNextStep( accessibilityServiceState: AccessibilityServiceState, @@ -171,6 +180,9 @@ interface SystemBridgeSetupUseCase { val isWarningUnderstood: Flow fun onUnderstoodWarning() + fun isInfoDismissed(): Boolean + fun dismissInfo() + val isSetupAssistantEnabled: Flow fun toggleSetupAssistant() diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 4b5ea54454..5467bd5013 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -115,4 +115,7 @@ object Keys { val isProModeInteractiveSetupAssistantEnabled = booleanPreferencesKey("key_is_pro_mode_setup_assistant_enabled") + + val isProModeInfoDismissed = + booleanPreferencesKey("key_is_pro_mode_info_dismissed") } From 5354bd74ef374495f937eb72b2164cea682657ba Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 25 Aug 2025 18:52:17 +0100 Subject: [PATCH 166/215] #1394 discover ADB pairing port with mDNS --- .../DetectScreenOffKeyEventsController.kt | 39 ------------------- .../keymapper/base/input/InputEventHub.kt | 4 -- .../SystemBridgeSetupAssistantController.kt | 9 ++--- base/src/main/res/values/strings.xml | 3 ++ .../keymapper/sysbridge/adb/AdbManager.kt | 17 ++++++-- .../service/SystemBridgeSetupController.kt | 6 +-- 6 files changed, 23 insertions(+), 55 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/detection/DetectScreenOffKeyEventsController.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectScreenOffKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectScreenOffKeyEventsController.kt deleted file mode 100644 index 8356d6f665..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectScreenOffKeyEventsController.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.github.sds100.keymapper.base.detection - -import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.root.SuAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job - -// TODO delete -class DetectScreenOffKeyEventsController( - private val suAdapter: SuAdapter, - private val devicesAdapter: DevicesAdapter, - private val onKeyEvent: suspend (event: KMKeyEvent) -> Unit, -) { - - companion object { - private const val REGEX_GET_DEVICE_LOCATION = "/.*(?=:)" - private const val REGEX_KEY_EVENT_ACTION = "(?<= )(DOWN|UP)" - } - - private var job: Job? = null - - /** - * @return whether it successfully started listening. - */ - fun startListening(scope: CoroutineScope): Boolean { - return false - } - - fun stopListening() { - job?.cancel() - job = null - } - - private fun getDeviceLocation(getEventDeviceOutput: String, deviceName: String): String? { - val regex = Regex("(/.*)(?=(\\n.*){5}\"$deviceName\")") - return regex.find(getEventDeviceOutput)?.value - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index aae9fa80e5..53c8a3cafe 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -143,9 +143,6 @@ class InputEventHubImpl @Inject constructor( for (clientContext in clients.values) { if (event is KMEvdevEvent) { - // TODO maybe flatmap all the client event types into one Set so this check - // can be done in onEvdevEvent. Hundreds of events may be sent per second synchronously so important to be as fast as possible. - // This client can ignore this event. if (!clientContext.evdevEventTypes.contains(event.type) || clientContext.grabbedEvdevDevices.isEmpty() ) { @@ -252,7 +249,6 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedEvdevDevices() } - // TODO invalidate when the input devices change. Or NOT because could be an infinite loop? private fun invalidateGrabbedEvdevDevices() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index 81ef0fd811..9a01bd3e1a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -167,11 +167,10 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( if (pairingCodeText != null && portText != null) { val pairingCode = pairingCodeText.toIntOrNull() - val port = portText.split(":").last().toIntOrNull() - if (pairingCode != null && port != null) { + if (pairingCode != null) { coroutineScope.launch { - onPairingCodeFound(port, pairingCode) + onPairingCodeFound(pairingCode) } } } else { @@ -180,8 +179,8 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } @RequiresApi(Build.VERSION_CODES.R) - private suspend fun onPairingCodeFound(port: Int, pairingCode: Int) { - setupController.pairWirelessAdb(port, pairingCode).onSuccess { + private suspend fun onPairingCodeFound(pairingCode: Int) { + setupController.pairWirelessAdb(pairingCode).onSuccess { Timber.i("Pairing code found. Starting System Bridge with ADB...") setupController.startWithAdb() diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 213db998da..abd0a544a3 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1636,6 +1636,9 @@ Pairing automatically Searching for pairing code and port… + Unable to find pairing port and code + Tap on the button to pair with pairing code and type it in here + What can I do with PRO mode? 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: Wi-Fi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device. Show PRO mode info diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt index c07b84dcba..8d4c156586 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt @@ -32,7 +32,9 @@ class AdbManagerImpl @Inject constructor( private val commandMutex: Mutex = Mutex() private val pairMutex: Mutex = Mutex() + private val adbConnectMdns: AdbMdns by lazy { AdbMdns(ctx, AdbServiceType.TLS_CONNECT) } + private val adbPairMdns: AdbMdns by lazy { AdbMdns(ctx, AdbServiceType.TLS_PAIR) } override suspend fun executeCommand(command: String): KMResult { Timber.i("Execute ADB command: $command") @@ -42,6 +44,7 @@ class AdbManagerImpl @Inject constructor( adbConnectMdns.start() val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } + adbConnectMdns.stop() if (port == null) { return@withLock AdbError.ServerNotFound @@ -69,15 +72,21 @@ class AdbManagerImpl @Inject constructor( } } - adbConnectMdns.stop() - Timber.i("Execute command result: $result") return result } - override suspend fun pair(port: Int, code: Int): KMResult { + override suspend fun pair(code: Int): KMResult { return pairMutex.withLock { + adbPairMdns.start() + val port = withTimeout(1000L) { adbPairMdns.port.first { it != null } } + adbPairMdns.stop() + + if (port == null) { + return@withLock AdbError.ServerNotFound + } + return@withLock getAdbKey().then { key -> val pairingClient = AdbPairingClient(LOCALHOST, port, code.toString(), key) @@ -119,5 +128,5 @@ interface AdbManager { suspend fun executeCommand(command: String): KMResult @RequiresApi(Build.VERSION_CODES.R) - suspend fun pair(port: Int, code: Int): KMResult + suspend fun pair(code: Int): KMResult } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index b2f4c87f8d..ebf1ad953a 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -115,8 +115,8 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } @RequiresApi(Build.VERSION_CODES.R) - override suspend fun pairWirelessAdb(port: Int, code: Int): KMResult { - return adbManager.pair(port, code).onSuccess { + override suspend fun pairWirelessAdb(code: Int): KMResult { + return adbManager.pair(code).onSuccess { setupAssistantStep.update { value -> if (value == SystemBridgeSetupStep.ADB_PAIRING) { null @@ -236,7 +236,7 @@ interface SystemBridgeSetupController { suspend fun isAdbPaired(): Boolean @RequiresApi(Build.VERSION_CODES.R) - suspend fun pairWirelessAdb(port: Int, code: Int): KMResult + suspend fun pairWirelessAdb(code: Int): KMResult fun startWithRoot() fun startWithShizuku() From 59a844164c84eabdfa04e67b82a6b2d45af9a8f3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 25 Aug 2025 22:06:54 +0100 Subject: [PATCH 167/215] #1394 auto start system bridge when Key Mapper has root or shizuku permission --- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 9 ++- .../keymapper/base/promode/ProModeScreen.kt | 2 + .../base/promode/SystemBridgeAutoStarter.kt | 71 +++++++++++++++++++ .../io/github/sds100/keymapper/data/Keys.kt | 3 + .../keymapper/data/PreferenceDefaults.kt | 4 ++ sysbridge/src/main/cpp/libevdev_jni.cpp | 2 + .../manager/SystemBridgeConnectionManager.kt | 30 +++++++- .../service/SystemBridgeSetupController.kt | 27 +++---- .../sysbridge/starter/SystemBridgeStarter.kt | 15 +++- 9 files changed, 138 insertions(+), 25 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index 666b4a18d8..f3c95266c9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDexApplication import io.github.sds100.keymapper.base.logging.KeyMapperLoggingTree +import io.github.sds100.keymapper.base.promode.SystemBridgeAutoStarter import io.github.sds100.keymapper.base.settings.Theme import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.inputmethod.AutoSwitchImeController @@ -83,6 +84,9 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { @Inject lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl + @Inject + lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter + private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() } private val userManager: UserManager? by lazy { getSystemService() } @@ -190,8 +194,11 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { }.launchIn(appCoroutineScope) autoGrantPermissionController.start() - keyEventRelayServiceWrapper.bind() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeAutoStarter.autoStart() + } } abstract fun getMainActivityClass(): Class<*> diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index ca13cd039f..a9a589600c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -221,6 +221,8 @@ private fun Content( } Spacer(modifier = Modifier.height(16.dp)) + + // TODO show options for safety and autostart. Show different autostart text for root vs shizuku } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt new file mode 100644 index 0000000000..0ca2ef8e68 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt @@ -0,0 +1,71 @@ +package io.github.sds100.keymapper.base.promode + +import android.os.Build +import androidx.annotation.RequiresApi +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.PreferenceDefaults +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This class handles auto starting the system bridge when Key Mapper is launched. + */ +@RequiresApi(Build.VERSION_CODES.Q) +@Singleton +class SystemBridgeAutoStarter @Inject constructor( + private val coroutineScope: CoroutineScope, + private val suAdapter: SuAdapter, + private val shizukuAdapter: ShizukuAdapter, + private val connectionManager: SystemBridgeConnectionManager, + private val preferences: PreferenceRepository +) { + private val isAutoStartEnabled: StateFlow = + preferences.get(Keys.isProModeAutoStartEnabled) + .map { it ?: PreferenceDefaults.PRO_MODE_AUTOSTART } + .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.PRO_MODE_AUTOSTART) + + /** + * This must only be called once in the application lifecycle + */ + fun autoStart() { + coroutineScope.launch { + combine( + isAutoStartEnabled, + suAdapter.isRootGranted, + shizukuAdapter.isStarted, + ) { isAutoStartEnabled, isRooted, isShizukuStarted -> + + // Do not listen to changes in the connection state to prevent + // auto starting straight after it has stopped + val isSystemBridgeConnected = connectionManager.isConnected.first() + + if (!isAutoStartEnabled || isSystemBridgeConnected) { + return@combine + } + + if (isRooted) { + Timber.i("Auto starting system bridge with root") + connectionManager.startWithRoot() + } else if (isShizukuStarted) { + Timber.i("Auto starting system bridge with Shizuku") + connectionManager.startWithShizuku() + } + + }.collect() + } + } +} \ No newline at end of file diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 5467bd5013..2a33d2b521 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -118,4 +118,7 @@ object Keys { val isProModeInfoDismissed = booleanPreferencesKey("key_is_pro_mode_info_dismissed") + + val isProModeAutoStartEnabled = + booleanPreferencesKey("key_is_pro_mode_auto_start_enabled") } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt index cc52283ee7..6f79fa2d74 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt @@ -16,4 +16,8 @@ object PreferenceDefaults { const val HOLD_DOWN_DURATION = 1000 const val PRO_MODE_INTERACTIVE_SETUP_ASSISTANT = true + + // Enable this by default so that key maps will still work for root users + // who upgrade to version 4.0. + const val PRO_MODE_AUTOSTART = true } diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 5e652d4c93..ceeb7dcd3a 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -219,6 +219,8 @@ bool onEpollEvdevEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { uint32_t outFlags = -1; deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); + // TODO if power button (matching scancode OR key code) is pressed for more than 10 seconds, stop the systembridge process. Call kill from here + bool returnValue; ndk::ScopedAStatus callbackResult = callback->onEvdevEvent(deviceContext->devicePath, inputEvent.time.tv_sec, diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 77f3b65443..5d7919bf49 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -10,11 +10,14 @@ import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @@ -22,9 +25,10 @@ import javax.inject.Singleton * This class handles starting, stopping and (dis)connecting to the system bridge. */ @Singleton -class SystemBridgeConnectionManagerImpl @Inject constructor() : SystemBridgeConnectionManager { - - // TODO if auto start is turned on, subscribe to Shizuku Binder listener and when bound, start the service. But only do this once per app process session. If the user stops the service it should remain stopped until key mapper is killed, +class SystemBridgeConnectionManagerImpl @Inject constructor( + private val coroutineScope: CoroutineScope, + private val starter: SystemBridgeStarter, +) : SystemBridgeConnectionManager { private val systemBridgeLock: Any = Any() private var systemBridgeFlow: MutableStateFlow = MutableStateFlow(null) @@ -75,6 +79,22 @@ class SystemBridgeConnectionManagerImpl @Inject constructor() : SystemBridgeConn } } + @RequiresApi(Build.VERSION_CODES.R) + override fun startWithAdb() { + coroutineScope.launch { + starter.startWithAdb() + } + } + + override fun startWithRoot() { + coroutineScope.launch { + starter.startWithRoot() + } + } + + override fun startWithShizuku() { + starter.startWithShizuku() + } } @SuppressLint("ObsoleteSdkInt") @@ -84,4 +104,8 @@ interface SystemBridgeConnectionManager { fun run(block: (ISystemBridge) -> T): KMResult fun stopSystemBridge() + + fun startWithRoot() + fun startWithShizuku() + fun startWithAdb() } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index ebf1ad953a..a7cd4a891d 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -12,14 +12,13 @@ import android.service.quicksettings.TileService import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.KeyMapperClassProvider import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.isSuccess import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.sysbridge.adb.AdbManager -import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -29,18 +28,13 @@ import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton - -/** - * This starter code is taken from the Shizuku project. - */ - @Singleton class SystemBridgeSetupControllerImpl @Inject constructor( @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, - private val buildConfigProvider: BuildConfigProvider, private val adbManager: AdbManager, - private val keyMapperClassProvider: KeyMapperClassProvider + private val keyMapperClassProvider: KeyMapperClassProvider, + private val connectionManager: SystemBridgeConnectionManager ) : SystemBridgeSetupController { companion object { @@ -50,10 +44,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private val activityManager: ActivityManager by lazy { ctx.getSystemService()!! } - private val starter: SystemBridgeStarter by lazy { - SystemBridgeStarter(ctx, adbManager, buildConfigProvider) - } - override val isDeveloperOptionsEnabled: MutableStateFlow = MutableStateFlow(getDeveloperOptionsEnabled()) @@ -70,6 +60,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( SettingsUtils.settingsCallbackFlow(ctx, uri).collect { val isEnabled = getWirelessDebuggingEnabled() + // Only go back if the user is currently setting up the wireless debugging step. + // This stops Key Mapper going back if they are turning on wireless debugging + // for another reason. if (isEnabled && setupAssistantStep.value == SystemBridgeSetupStep.WIRELESS_DEBUGGING) { getKeyMapperAppTask()?.moveToFront() } @@ -90,19 +83,17 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override fun startWithRoot() { coroutineScope.launch { - starter.startWithRoot() + connectionManager.startWithRoot() } } override fun startWithShizuku() { - starter.startWithShizuku() + connectionManager.startWithShizuku() } @RequiresApi(Build.VERSION_CODES.R) override fun startWithAdb() { - coroutineScope.launch { - starter.startWithAdb() - } + connectionManager.startWithAdb() } @RequiresApi(Build.VERSION_CODES.R) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index 5f61f79a42..b8747aca85 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -4,6 +4,7 @@ import android.content.ComponentName import android.content.Context import android.content.ServiceConnection import android.os.Build +import android.os.DeadObjectException import android.os.IBinder import android.os.RemoteException import android.os.UserManager @@ -11,6 +12,7 @@ import android.system.ErrnoException import android.system.Os import androidx.annotation.RequiresApi import com.topjohnwu.superuser.Shell +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult @@ -39,9 +41,12 @@ import java.io.IOException import java.io.InputStreamReader import java.io.PrintWriter import java.util.zip.ZipFile +import javax.inject.Inject +import javax.inject.Singleton -class SystemBridgeStarter( - private val ctx: Context, +@Singleton +class SystemBridgeStarter @Inject constructor( + @ApplicationContext private val ctx: Context, private val adbManager: AdbManager, private val buildConfigProvider: BuildConfigProvider ) { @@ -77,7 +82,11 @@ class SystemBridgeStarter( } catch (e: RemoteException) { Timber.e("Exception starting with Shizuku starter service: $e") } finally { - service.destroy() + try { + service.destroy() + } catch (_: DeadObjectException) { + // Do nothing. Service is already dead. + } } } From 933e528bb9c4cdfaa67801d34c06447aa7d2242e Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 26 Aug 2025 00:19:11 +0100 Subject: [PATCH 168/215] #1394 automatically rebind the system bridge when key mapper process starts --- sysbridge/src/main/cpp/libevdev_jni.cpp | 6 + .../manager/SystemBridgeConnectionManager.kt | 1 + .../sysbridge/service/SystemBridge.kt | 217 +++++++++++------- 3 files changed, 140 insertions(+), 84 deletions(-) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index ceeb7dcd3a..744018847b 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -388,7 +388,9 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLo evdevDevices->clear(); close(commandEventFd); + commandEventFd = -1; close(epollFd); + epollFd = -1; LOGI("Stopped evdev event loop"); } @@ -450,6 +452,10 @@ extern "C" JNIEXPORT void JNICALL Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv *env, jobject thiz) { + if (commandEventFd == -1) { + return; + } + Command cmd = {STOP}; std::lock_guard lock(commandMutex); diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 5d7919bf49..28b78edc52 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -36,6 +36,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( override val isConnected: Flow = systemBridgeFlow.map { it != null } private val deathRecipient: DeathRecipient = DeathRecipient { + // TODO show notification when pro mode is stopped for an unexpected reason. Do not show it if the user stopped it synchronized(systemBridgeLock) { systemBridgeFlow.update { null } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index c00b00243f..2e76025944 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -6,7 +6,6 @@ import android.content.IContentProvider import android.ddm.DdmHandleAppName import android.hardware.input.IInputManager import android.net.wifi.IWifiManager -import android.os.Binder import android.os.Bundle import android.os.Handler import android.os.IBinder @@ -26,11 +25,12 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import rikka.hidden.compat.ActivityManagerApis import rikka.hidden.compat.DeviceIdleControllerApis +import rikka.hidden.compat.PackageManagerApis import rikka.hidden.compat.UserManagerApis import rikka.hidden.compat.adapter.ProcessObserverAdapter -import timber.log.Timber import kotlin.system.exitProcess + @SuppressLint("LogNotTimber") internal class SystemBridge : ISystemBridge.Stub() { @@ -58,12 +58,13 @@ internal class SystemBridge : ISystemBridge.Stub() { companion object { private const val TAG: String = "KeyMapperSystemBridge" - private val packageName: String? = System.getProperty("keymapper_sysbridge.package") + private val systemBridgePackageName: String? = + System.getProperty("keymapper_sysbridge.package") private const val SHELL_PACKAGE = "com.android.shell" @JvmStatic fun main(args: Array) { - Log.i(TAG, "Sysbridge package name = $packageName") + Log.i(TAG, "Sysbridge package name = $systemBridgePackageName") DdmHandleAppName.setAppName("keymapper_sysbridge", 0) @Suppress("DEPRECATION") Looper.prepareMainLooper() @@ -81,93 +82,41 @@ internal class SystemBridge : ISystemBridge.Stub() { } } } + } + + private val processObserver = object : ProcessObserverAdapter() { - fun sendBinderToApp( - binder: Binder?, - packageName: String?, - userId: Int, + // This is used as a proxy for detecting the Key Mapper process has started. + override fun onForegroundActivitiesChanged( + pid: Int, + uid: Int, + foregroundActivities: Boolean ) { - try { - DeviceIdleControllerApis.addPowerSaveTempWhitelistApp( - packageName, - 30 * 1000, - userId, - 316, /* PowerExemptionManager#REASON_SHELL */"shell" - ) - Timber.d( - "Add $userId:$packageName to power save temp whitelist for 30s", - userId, - packageName - ) - } catch (tr: Throwable) { - Timber.e(tr) + // Do not send the binder if the binder is already sent to the user or + // the app is not in the foreground. + if (isBinderSent || !foregroundActivities) { + return } - val providerName = "$packageName.sysbridge" - var provider: IContentProvider? = null + val packages: List = + PackageManagerApis.getPackagesForUidNoThrow(uid).filterNotNull() - val token: IBinder? = null + if (packages.contains(systemBridgePackageName)) { + synchronized(sendBinderLock) { + Log.i(TAG, "Key Mapper process started, send binder to app") - try { - provider = ActivityManagerApis.getContentProviderExternal( - providerName, - userId, - token, - providerName - ) - if (provider == null) { - Log.e(TAG, "provider is null $providerName $userId") - return - } - - if (!provider.asBinder().pingBinder()) { - Log.e(TAG, "provider is dead $providerName $userId") - return - } - - val extra = Bundle() - extra.putParcelable( - SystemBridgeBinderProvider.EXTRA_BINDER, - BinderContainer(binder) - ) - - val reply: Bundle? = IContentProviderUtils.callCompat( - provider, - null, - providerName, - "sendBinder", - null, - extra - ) - if (reply != null) { - Log.i(TAG, "Send binder to user app $packageName in user $userId") - } else { - Log.w(TAG, "Failed to send binder to user app $packageName in user $userId") - } - } catch (tr: Throwable) { - Log.e(TAG, "Failed to send binder to user app $packageName in user $userId", tr) - } finally { - if (provider != null) { - try { - ActivityManagerApis.removeContentProviderExternal(providerName, token) - } catch (tr: Throwable) { - Log.w(TAG, "Failed to remove content provider $providerName", tr) - } + sendBinderToApp() } } } - } - - private val processObserver = object : ProcessObserverAdapter() { - override fun onProcessStateChanged(pid: Int, uid: Int, procState: Int) { - - } override fun onProcessDied(pid: Int, uid: Int) { - } } + private val sendBinderLock: Any = Any() + private var isBinderSent: Boolean = false + private val coroutineScope: CoroutineScope = MainScope() private val mainHandler = Handler(Looper.myLooper()!!) @@ -175,6 +124,10 @@ internal class SystemBridge : ISystemBridge.Stub() { private var evdevCallback: IEvdevCallback? = null private val evdevCallbackDeathRecipient: IBinder.DeathRecipient = IBinder.DeathRecipient { Log.i(TAG, "EvdevCallback binder died") + synchronized(sendBinderLock) { + isBinderSent = false + } + coroutineScope.launch(Dispatchers.Default) { stopEvdevEventLoop() } @@ -194,10 +147,12 @@ internal class SystemBridge : ISystemBridge.Stub() { waitSystemService(Context.ACTIVITY_SERVICE) waitSystemService(Context.USER_SERVICE) waitSystemService(Context.APP_OPS_SERVICE) - waitSystemService(Context.INPUT_SERVICE) + waitSystemService(Context.INPUT_SERVICE) inputManager = IInputManager.Stub.asInterface(ServiceManager.getService(Context.INPUT_SERVICE)) + + waitSystemService(Context.WIFI_SERVICE) wifiManager = IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) @@ -215,13 +170,11 @@ internal class SystemBridge : ISystemBridge.Stub() { // } // }) - // TODO use the process observer to rebind when key mapper starts - + ActivityManagerApis.registerProcessObserver(processObserver) + // Try sending the binder to the app when its started. mainHandler.post { - for (userId in UserManagerApis.getUserIdsNoThrow()) { - sendBinderToApp(this, packageName, userId) - } + sendBinderToApp() } } @@ -263,7 +216,7 @@ internal class SystemBridge : ISystemBridge.Stub() { } } - // TODO passthrough a timeout that will automatically ungrab after that time. Use this when recording. + // TODO passthrough an optional timeout that will automatically ungrab after that time. Use this when recording. override fun grabEvdevDevice(devicePath: String?): Boolean { devicePath ?: return false return grabEvdevDeviceNative(devicePath) @@ -296,4 +249,100 @@ internal class SystemBridge : ISystemBridge.Stub() { devicePath ?: return false return writeEvdevEventNative(devicePath, type, code, value) } + + private fun sendBinderToApp(): Boolean { + // Only support Key Mapper running in a single Android user for now so just send + // it to the first user that accepts the binder. + for (userId in UserManagerApis.getUserIdsNoThrow()) { + if (sendBinderToAppInUser(userId)) { + return true + } + } + + return false + } + + /** + * @return Whether it was sent successfully with a reply from the app. + */ + private fun sendBinderToAppInUser(userId: Int): Boolean { + try { + DeviceIdleControllerApis.addPowerSaveTempWhitelistApp( + systemBridgePackageName, + 30 * 1000, + userId, + 316, /* PowerExemptionManager#REASON_SHELL */"shell" + ) + Log.d( + TAG, + "Add $userId:$systemBridgePackageName to power save temp whitelist for 30s" + ) + } catch (tr: Throwable) { + Log.e(TAG, tr.toString()) + } + + val providerName = "$systemBridgePackageName.sysbridge" + var provider: IContentProvider? = null + + val token: IBinder? = null + + try { + provider = ActivityManagerApis.getContentProviderExternal( + providerName, + userId, + token, + providerName + ) + if (provider == null) { + Log.e(TAG, "provider is null $providerName $userId") + return false + } + + if (!provider.asBinder().pingBinder()) { + Log.e(TAG, "provider is dead $providerName $userId") + return false + } + + val extra = Bundle() + extra.putParcelable( + SystemBridgeBinderProvider.EXTRA_BINDER, + BinderContainer(this) + ) + + val reply: Bundle? = IContentProviderUtils.callCompat( + provider, + null, + providerName, + "sendBinder", + null, + extra + ) + if (reply != null) { + Log.i(TAG, "Send binder to user app $systemBridgePackageName in user $userId") + isBinderSent = true + return true + } else { + Log.w( + TAG, + "Failed to send binder to user app $systemBridgePackageName in user $userId" + ) + } + } catch (tr: Throwable) { + Log.e( + TAG, + "Failed to send binder to user app $systemBridgePackageName in user $userId", + tr + ) + } finally { + if (provider != null) { + try { + ActivityManagerApis.removeContentProviderExternal(providerName, token) + } catch (tr: Throwable) { + Log.w(TAG, "Failed to remove content provider $providerName", tr) + } + } + } + + return false + } } \ No newline at end of file From 647d32596f149cc34fdb589f092c4ecf865b80ac Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 26 Aug 2025 00:22:13 +0100 Subject: [PATCH 169/215] #1394 use IO dispatcher in RoomLogRepository --- .../keymapper/data/repositories/RoomLogRepository.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt index 4ff6368208..f6c4baf36f 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt @@ -19,7 +19,7 @@ class RoomLogRepository @Inject constructor( private val dao: LogEntryDao, ) : LogRepository { override val log: Flow> = dao.getAll() - .flowOn(Dispatchers.Default) + .flowOn(Dispatchers.IO) init { dao.getIds() @@ -27,18 +27,18 @@ class RoomLogRepository @Inject constructor( .onEach { log -> val middleId = log.getOrNull(500) ?: return@onEach dao.deleteRowsWithIdLessThan(middleId) - }.flowOn(Dispatchers.Default) + }.flowOn(Dispatchers.IO) .launchIn(coroutineScope) } override fun deleteAll() { - coroutineScope.launch(Dispatchers.Default) { + coroutineScope.launch(Dispatchers.IO) { dao.deleteAll() } } override fun insert(entry: LogEntryEntity) { - coroutineScope.launch(Dispatchers.Default) { + coroutineScope.launch(Dispatchers.IO) { dao.insert(entry) } } From 7f4391a1660852e76283707fda34e004550d3fea Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 27 Aug 2025 22:01:36 +0200 Subject: [PATCH 170/215] #1394 add more notifications when finding the pairing code fails --- .../keymapper/base/promode/ProModeScreen.kt | 7 +- .../base/promode/ProModeSetupScreen.kt | 3 +- .../SystemBridgeSetupAssistantController.kt | 137 ++++++++----- .../base/settings/SettingsViewModel.kt | 2 +- .../AndroidNotificationAdapter.kt | 194 ++++++++++++++---- .../ManageNotificationsUseCase.kt | 14 +- .../NotificationClickReceiver.kt | 28 --- .../notifications/NotificationController.kt | 161 +++++---------- base/src/main/res/values/strings.xml | 9 +- .../notifications/KMNotificationAction.kt | 45 ++++ .../keymapper/sysbridge/adb/AdbManager.kt | 18 +- .../sds100/keymapper/sysbridge/adb/AdbMdns.kt | 3 + .../service/SystemBridgeSetupController.kt | 29 +-- .../sysbridge/starter/SystemBridgeStarter.kt | 2 +- .../system/network/AndroidNetworkAdapter.kt | 18 +- .../notifications/NotificationAdapter.kt | 9 +- .../system/notifications/NotificationModel.kt | 31 +-- .../notifications/NotificationRemoteInput.kt | 13 ++ 18 files changed, 438 insertions(+), 285 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationClickReceiver.kt create mode 100644 common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt create mode 100644 system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index a9a589600c..11e73b8b0e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.material.icons.rounded.Numbers import androidx.compose.material.icons.rounded.WarningAmber import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton @@ -358,6 +359,7 @@ private fun WarningCard( OutlinedCard( modifier = modifier, border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), + elevation = CardDefaults.elevatedCardElevation() ) { Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.padding(horizontal = 16.dp)) { @@ -439,7 +441,9 @@ private fun ProModeStartedCard( Spacer(modifier = Modifier.width(16.dp)) Text( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .padding(vertical = 8.dp), text = stringResource(R.string.pro_mode_service_started), style = MaterialTheme.typography.titleMedium ) @@ -522,6 +526,7 @@ private fun ProModeInfoCard( OutlinedCard( modifier = modifier, border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), + elevation = CardDefaults.elevatedCardElevation() ) { Row( modifier = Modifier diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index 04060fdb54..ae086391db 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -224,7 +224,8 @@ private fun StepContent( Text( text = stepContent.title, - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index 9a01bd3e1a..0b51d55dbd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -17,6 +17,7 @@ import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsU import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.KeyMapperClassProvider +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.data.Keys @@ -33,6 +34,7 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -40,6 +42,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import timber.log.Timber + @Suppress("KotlinConstantConditions") @RequiresApi(Build.VERSION_CODES.Q) class SystemBridgeSetupAssistantController @AssistedInject constructor( @@ -66,17 +69,17 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( /** * The max time to spend searching for an accessibility node. */ - const val INTERACTION_TIMEOUT = 30000L + const val INTERACTION_TIMEOUT = 5000L private val PAIRING_CODE_REGEX = Regex("^\\d{6}$") - private val PORT_REGEX = Regex(".*:([0-9]{1,5})") } - private enum class InteractionStep { // Do not automatically turn on the wireless debugging switch. When the user turns it on, // Key Mapper will automatically pair. PAIR_DEVICE, + + // TODO add tap build number many times } private val activityManager: ActivityManager = accessibilityService.getSystemService()!! @@ -111,6 +114,19 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } } } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + coroutineScope.launch { + manageNotifications.onNotificationTextInput + .filter { it.intentAction == KMNotificationAction.IntentAction.PAIRING_CODE_REPLY } + .collect { textInput -> + Timber.i("Receive pairing code text input: $textInput") + + val pairingCode: String = textInput.text.trim() + onPairingCodeFound(pairingCode) + } + } + } } private fun createNotificationChannel() { @@ -163,55 +179,65 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( @RequiresApi(Build.VERSION_CODES.R) private fun doPairingInteractiveStep(rootNode: AccessibilityNodeInfo) { val pairingCodeText = findPairingCodeText(rootNode) - val portText = findPortText(rootNode) - if (pairingCodeText != null && portText != null) { - val pairingCode = pairingCodeText.toIntOrNull() + if (pairingCodeText == null) { + clickPairWithCodeButton(rootNode) + } else { + val pairingCode = pairingCodeText.trim() - if (pairingCode != null) { - coroutineScope.launch { - onPairingCodeFound(pairingCode) - } + coroutineScope.launch { + onPairingCodeFound(pairingCode) } - } else { - clickPairWithCodeButton(rootNode) } } @RequiresApi(Build.VERSION_CODES.R) - private suspend fun onPairingCodeFound(pairingCode: Int) { + private suspend fun onPairingCodeFound(pairingCode: String) { setupController.pairWirelessAdb(pairingCode).onSuccess { Timber.i("Pairing code found. Starting System Bridge with ADB...") - setupController.startWithAdb() - + onPairingSuccess() + }.onFailure { + Timber.e("Failed to pair with wireless ADB: $it") stopInteracting() - val isStarted = try { - withTimeout(3000L) { - systemBridgeConnectionManager.isConnected.first { it } - } - } catch (e: TimeoutCancellationException) { - false + showNotification( + getString(R.string.pro_mode_setup_notification_invalid_pairing_code_title), + getString(R.string.pro_mode_setup_notification_invalid_pairing_code_text), + actions = listOf(KMNotificationAction.RemoteInput.PairingCode to getString(R.string.pro_mode_setup_notification_action_input_pairing_code)) + ) + } + } + + private suspend fun onPairingSuccess() { + setupController.startWithAdb() + + stopInteracting() + + val isStarted = try { + withTimeout(3000L) { + systemBridgeConnectionManager.isConnected.first { it } } + } catch (_: TimeoutCancellationException) { + false + } - if (isStarted) { - showNotification( - getString(R.string.pro_mode_setup_notification_started_success_title), - getString(R.string.pro_mode_setup_notification_started_success_text) - ) + if (isStarted) { + showNotification( + getString(R.string.pro_mode_setup_notification_started_success_title), + getString(R.string.pro_mode_setup_notification_started_success_text) + ) - getKeyMapperAppTask()?.moveToFront() + getKeyMapperAppTask()?.moveToFront() - delay(5000) + delay(5000) - dismissNotification() - } else { - // TODO Show notification - Timber.w("Failed to start system bridge after pairing.") - } - }.onFailure { - Timber.e("Failed to pair with wireless ADB: $it") - // TODO show notification + dismissNotification() + } else { + Timber.e("Failed to start system bridge after pairing.") + showNotification( + getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_title), + getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_text), + ) } } @@ -223,7 +249,11 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( ?.performAction(AccessibilityNodeInfo.ACTION_CLICK) } - private fun showNotification(title: String, text: String) { + private fun showNotification( + title: String, + text: String, + actions: List> = emptyList() + ) { val notification = NotificationModel( // Use the same notification id for all so they overwrite each other. id = NotificationController.Companion.ID_SETUP_ASSISTANT, @@ -236,7 +266,8 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( autoCancel = true, bigTextStyle = true, // Must not be silent so it is shown as a heads up notification - silent = false + silent = false, + actions = actions ) manageNotifications.show(notification) } @@ -251,16 +282,8 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( }?.text?.toString() } - private fun findPortText(rootNode: AccessibilityNodeInfo): String? { - return rootNode.findNodeRecursively { - it.text != null && PORT_REGEX.matches(it.text) - }?.text?.toString() - } - private fun startSetupStep(step: SystemBridgeSetupStep) { Timber.i("Starting setup assistant step: $step") - startInteractionTimeoutJob() - when (step) { SystemBridgeSetupStep.DEVELOPER_OPTIONS -> { showNotification( @@ -278,18 +301,32 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( interactionStep = InteractionStep.PAIR_DEVICE } - else -> {} // Do nothing + else -> return // Do not start interaction timeout job } - - // TODO if finding pairing node does not work, show a notification asking for the pairing code. - // TODO do this in the timeout job too + startInteractionTimeoutJob() } private fun startInteractionTimeoutJob() { interactionTimeoutJob?.cancel() interactionTimeoutJob = coroutineScope.launch { delay(INTERACTION_TIMEOUT) + + if (interactionStep == InteractionStep.PAIR_DEVICE) { + Timber.i("Interaction timed out. Asking user to input pairing code manually.") + + showNotification( + title = getString(R.string.pro_mode_setup_notification_pairing_button_not_found_title), + text = getString(R.string.pro_mode_setup_notification_pairing_button_not_found_text), + actions = listOf(KMNotificationAction.RemoteInput.PairingCode to getString(R.string.pro_mode_setup_notification_action_input_pairing_code)) + ) + + // Give the user 30 seconds to input the pairing code and then dismiss the notification. + delay(30000) + } + + dismissNotification() + interactionStep = null } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 6683453555..48660d2f28 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -273,7 +273,7 @@ class SettingsViewModel @Inject constructor( } fun onPauseResumeNotificationClick() { - onNotificationSettingsClick(NotificationController.CHANNEL_TOGGLE_KEYMAPS) + onNotificationSettingsClick(NotificationController.CHANNEL_TOGGLE_KEY_MAPS) } fun onDefaultOptionsClick() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt index fb9ed752f1..d94ed01369 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt @@ -1,41 +1,87 @@ package io.github.sds100.keymapper.base.system.notifications +import android.Manifest import android.app.NotificationChannel import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager import android.os.Build import android.provider.Settings +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput +import androidx.core.content.ContextCompat import com.google.android.material.color.DynamicColors import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.utils.ui.color import io.github.sds100.keymapper.common.KeyMapperClassProvider +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.system.notifications.NotificationAdapter import io.github.sds100.keymapper.system.notifications.NotificationChannelModel -import io.github.sds100.keymapper.system.notifications.NotificationIntentType import io.github.sds100.keymapper.system.notifications.NotificationModel +import io.github.sds100.keymapper.system.notifications.NotificationRemoteInput import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class AndroidNotificationAdapter @Inject constructor( - @ApplicationContext private val context: Context, + @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, private val classProvider: KeyMapperClassProvider, ) : NotificationAdapter { - private val ctx = context.applicationContext private val manager: NotificationManagerCompat = NotificationManagerCompat.from(ctx) - override val onNotificationActionClick = MutableSharedFlow() + override val onNotificationActionClick = MutableSharedFlow() + override val onNotificationRemoteInput = MutableSharedFlow() + + private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + context ?: return + intent ?: return + + onReceiveNotificationActionIntent(intent) + + // dismiss the notification drawer after tapping on the notification. This is deprecated on S+ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS).apply { + context.sendBroadcast(this) + } + } + } + + } + + init { + val intentFilter = IntentFilter().apply { + for (entry in KMNotificationAction.IntentAction.entries) { + addAction(entry.name) + } + } + ContextCompat.registerReceiver( + ctx, + broadcastReceiver, + intentFilter, + ContextCompat.RECEIVER_EXPORTED + ) + } override fun showNotification(notification: NotificationModel) { + if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED + ) { + return + } + val builder = NotificationCompat.Builder(ctx, notification.channel).apply { if (!DynamicColors.isDynamicColorAvailable()) { color = ctx.color(R.color.md_theme_secondary) @@ -66,14 +112,24 @@ class AndroidNotificationAdapter @Inject constructor( setVisibility(NotificationCompat.VISIBILITY_SECRET) } - for (action in notification.actions) { - addAction( - NotificationCompat.Action( - 0, - action.text, - createActionIntent(action.intentType), - ), + for ((action, label) in notification.actions) { + val pendingIntent = createActionIntent(action) + + val notificationAction = NotificationCompat.Action.Builder( + 0, + label, + pendingIntent, ) + + if (action is KMNotificationAction.RemoteInput) { + val remoteInput = RemoteInput.Builder(action.key) + .setLabel(label) + .build() + + notificationAction.addRemoteInput(remoteInput) + } + + addAction(notificationAction.build()) } setSilent(notification.silent) @@ -87,15 +143,13 @@ class AndroidNotificationAdapter @Inject constructor( } override fun createChannel(channel: NotificationChannelModel) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val androidChannel = NotificationChannel( - channel.id, - channel.name, - channel.importance, - ) + val androidChannel = NotificationChannel( + channel.id, + channel.name, + channel.importance, + ) - manager.createNotificationChannel(androidChannel) - } + manager.createNotificationChannel(androidChannel) } override fun deleteChannel(channelId: String) { @@ -114,36 +168,98 @@ class AndroidNotificationAdapter @Inject constructor( } fun onReceiveNotificationActionIntent(intent: Intent) { - val actionId = intent.action ?: return + val intentAction = + KMNotificationAction.IntentAction.entries.single { it.name == intent.action } + + Timber.d("Received notification click: actionId=$intentAction") + + // Check if there's RemoteInput data + val remoteInputBundle = RemoteInput.getResultsFromIntent(intent) + + if (remoteInputBundle != null) { + for (key in remoteInputBundle.keySet()) { + val text = remoteInputBundle.getCharSequence(key)?.toString() + if (!text.isNullOrEmpty()) { + coroutineScope.launch { + onNotificationRemoteInput.emit(NotificationRemoteInput(intentAction, text)) + } + + return + } + } + } + + // No text input, treat as regular action click coroutineScope.launch { - onNotificationActionClick.emit(actionId) + onNotificationActionClick.emit(intentAction) } } - private fun createActionIntent(intentType: NotificationIntentType): PendingIntent { - when (intentType) { - is NotificationIntentType.Broadcast -> { - val intent = Intent(ctx, NotificationClickReceiver::class.java).apply { - action = intentType.action - } + private fun createActionIntent( + notificationAction: KMNotificationAction + ): PendingIntent { + return when (notificationAction) { + KMNotificationAction.Activity.AccessibilitySettings -> createActivityPendingIntent( + Settings.ACTION_ACCESSIBILITY_SETTINGS + ) - return PendingIntent.getBroadcast(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) - } + is KMNotificationAction.Activity.MainActivity -> createMainActivityPendingIntent( + notificationAction.action + ) - is NotificationIntentType.MainActivity -> { - val intent = Intent(ctx, classProvider.getMainActivity()).apply { - action = intentType.customIntentAction ?: Intent.ACTION_MAIN - } + is KMNotificationAction.Broadcast -> createBroadcastPendingIntent(notificationAction.intentAction.name) + is KMNotificationAction.RemoteInput -> createRemoteInputPendingIntent(notificationAction.intentAction.name) + } + } - return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) - } + private fun createRemoteInputPendingIntent(action: String): PendingIntent { + val intent = Intent(action).apply { + setPackage(ctx.packageName) + } + + return PendingIntent.getBroadcast( + ctx, + 0, + intent, + PendingIntent.FLAG_MUTABLE + ) + } - is NotificationIntentType.Activity -> { - val intent = Intent(intentType.action) + private fun createBroadcastPendingIntent(action: String): PendingIntent { + val intent = Intent(action).apply { + setPackage(ctx.packageName) + } - return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) - } + return PendingIntent.getBroadcast( + ctx, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + } + + private fun createActivityPendingIntent(action: String): PendingIntent { + val intent = Intent(action) + + return PendingIntent.getActivity( + ctx, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + } + + private fun createMainActivityPendingIntent(action: String?): PendingIntent { + val intent = Intent(ctx, classProvider.getMainActivity()).apply { + this.action = action ?: Intent.ACTION_MAIN } + + return PendingIntent.getActivity( + ctx, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt index f4d0d2c1ad..9874927eed 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt @@ -1,8 +1,10 @@ package io.github.sds100.keymapper.base.system.notifications +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.system.notifications.NotificationAdapter import io.github.sds100.keymapper.system.notifications.NotificationChannelModel import io.github.sds100.keymapper.system.notifications.NotificationModel +import io.github.sds100.keymapper.system.notifications.NotificationRemoteInput import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import kotlinx.coroutines.flow.Flow @@ -13,7 +15,10 @@ class ManageNotificationsUseCaseImpl @Inject constructor( private val permissionAdapter: PermissionAdapter, ) : ManageNotificationsUseCase { - override val onActionClick: Flow = notificationAdapter.onNotificationActionClick + override val onNotificationTextInput: Flow = + notificationAdapter.onNotificationRemoteInput + override val onActionClick: Flow = + notificationAdapter.onNotificationActionClick override fun show(notification: NotificationModel) { notificationAdapter.showNotification(notification) @@ -37,10 +42,13 @@ class ManageNotificationsUseCaseImpl @Inject constructor( } interface ManageNotificationsUseCase { + /** - * The string is the ID of the action. + * Emits text input from notification actions that support RemoteInput. */ - val onActionClick: Flow + val onNotificationTextInput: Flow + + val onActionClick: Flow fun isPermissionGranted(): Boolean fun show(notification: NotificationModel) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationClickReceiver.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationClickReceiver.kt deleted file mode 100644 index 0558a8ad5f..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationClickReceiver.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.sds100.keymapper.base.system.notifications - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Build -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class NotificationClickReceiver : BroadcastReceiver() { - - @Inject - lateinit var notificationAdapter: AndroidNotificationAdapter - - override fun onReceive(context: Context, intent: Intent?) { - intent ?: return - - notificationAdapter.onReceiveNotificationActionIntent(intent) - - // dismiss the notification drawer after tapping on the notification. This is deprecated on S+ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS).apply { - context.sendBroadcast(this) - } - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 92a99cca3c..2ccb913fa5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.system.notifications -import android.provider.Settings import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import io.github.sds100.keymapper.base.BaseMainActivity @@ -9,18 +8,16 @@ import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.base.system.inputmethod.ShowHideInputMethodUseCase -import io.github.sds100.keymapper.base.system.inputmethod.ShowInputMethodPickerUseCase import io.github.sds100.keymapper.base.system.inputmethod.ToggleCompatibleImeUseCase import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.common.utils.DefaultDispatcherProvider import io.github.sds100.keymapper.common.utils.DispatcherProvider import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceState import io.github.sds100.keymapper.system.notifications.NotificationChannelModel -import io.github.sds100.keymapper.system.notifications.NotificationIntentType import io.github.sds100.keymapper.system.notifications.NotificationModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -40,14 +37,12 @@ class NotificationController @Inject constructor( private val coroutineScope: CoroutineScope, private val manageNotifications: ManageNotificationsUseCase, private val pauseMappings: PauseKeyMapsUseCase, - private val showImePicker: ShowInputMethodPickerUseCase, private val controlAccessibilityService: ControlAccessibilityServiceUseCase, private val toggleCompatibleIme: ToggleCompatibleImeUseCase, private val hideInputMethod: ShowHideInputMethodUseCase, private val onboardingUseCase: OnboardingUseCase, private val resourceProvider: ResourceProvider, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), - private val buildConfigProvider: BuildConfigProvider, ) : ResourceProvider by resourceProvider { companion object { @@ -60,7 +55,7 @@ class NotificationController @Inject constructor( // private const val ID_FEATURE_ASSISTANT_TRIGGER = 900 private const val ID_FEATURE_FLOATING_BUTTONS = 901 - const val CHANNEL_TOGGLE_KEYMAPS = "channel_toggle_remaps" + const val CHANNEL_TOGGLE_KEY_MAPS = "channel_toggle_remaps" // TODO delete all ime picker notifications and auto showing logic. @Deprecated("Removed in 4.0.0") @@ -78,31 +73,6 @@ class NotificationController @Inject constructor( private const val CHANNEL_ID_PERSISTENT = "channel_persistent" } - private val actionResumeMappings = - "${buildConfigProvider.packageName}.ACTION_RESUME_MAPPINGS" - - private val actionPauseMappings = "${buildConfigProvider.packageName}.ACTION_PAUSE_MAPPINGS" - - private val actionStartService = - "${buildConfigProvider.packageName}.ACTION_START_ACCESSIBILITY_SERVICE" - - private val actionRestartService = - "${buildConfigProvider.packageName}.ACTION_RESTART_ACCESSIBILITY_SERVICE" - - private val actionStopService = - "${buildConfigProvider.packageName}.ACTION_STOP_ACCESSIBILITY_SERVICE" - - private val actionDismissToggleMappings = - "${buildConfigProvider.packageName}.ACTION_DISMISS_TOGGLE_MAPPINGS" - - private val actionShowImePicker = - "${buildConfigProvider.packageName}.ACTION_SHOW_IME_PICKER" - - private val actionShowKeyboard = "${buildConfigProvider.packageName}.ACTION_SHOW_KEYBOARD" - - private val actionToggleKeyboard = - "${buildConfigProvider.packageName}.ACTION_TOGGLE_KEYBOARD" - /** * Open the app and use the String as the Intent action. */ @@ -177,26 +147,30 @@ class NotificationController @Inject constructor( } else { manageNotifications.dismiss(ID_KEYBOARD_HIDDEN) } - }.flowOn(dispatchers.default()).launchIn(coroutineScope) + }.launchIn(coroutineScope) manageNotifications.onActionClick.onEach { actionId -> when (actionId) { - actionResumeMappings -> pauseMappings.resume() - actionPauseMappings -> pauseMappings.pause() - actionStartService -> attemptStartAccessibilityService() - actionRestartService -> attemptRestartAccessibilityService() - actionStopService -> controlAccessibilityService.stopService() - - actionDismissToggleMappings -> manageNotifications.dismiss(ID_TOGGLE_MAPPINGS) - actionShowImePicker -> showImePicker.show(fromForeground = false) - actionShowKeyboard -> hideInputMethod.show() - actionToggleKeyboard -> toggleCompatibleIme.toggle().onSuccess { - _showToast.emit(getString(R.string.toast_chose_keyboard, it.label)) - }.onFailure { - _showToast.emit(it.getFullMessage(this)) - } + KMNotificationAction.IntentAction.RESUME_KEY_MAPS -> pauseMappings.resume() + KMNotificationAction.IntentAction.PAUSE_KEY_MAPS -> pauseMappings.pause() + KMNotificationAction.IntentAction.DISMISS_TOGGLE_KEY_MAPS_NOTIFICATION -> manageNotifications.dismiss( + ID_TOGGLE_MAPPINGS + ) + + KMNotificationAction.IntentAction.STOP_ACCESSIBILITY_SERVICE -> controlAccessibilityService.stopService() + KMNotificationAction.IntentAction.START_ACCESSIBILITY_SERVICE -> attemptStartAccessibilityService() + KMNotificationAction.IntentAction.RESTART_ACCESSIBILITY_SERVICE -> attemptRestartAccessibilityService() + KMNotificationAction.IntentAction.TOGGLE_KEY_MAPPER_IME -> toggleCompatibleIme.toggle() + .onSuccess { + _showToast.emit(getString(R.string.toast_chose_keyboard, it.label)) + }.onFailure { + _showToast.emit(it.getFullMessage(this)) + } + + KMNotificationAction.IntentAction.SHOW_KEYBOARD -> hideInputMethod.show() + else -> Unit // Ignore other notification actions } - }.flowOn(dispatchers.default()).launchIn(coroutineScope) + }.launchIn(coroutineScope) } fun onOpenApp() { @@ -232,7 +206,7 @@ class NotificationController @Inject constructor( ) { manageNotifications.createChannel( NotificationChannelModel( - id = CHANNEL_TOGGLE_KEYMAPS, + id = CHANNEL_TOGGLE_KEY_MAPS, name = getString(R.string.notification_channel_toggle_mappings), NotificationManagerCompat.IMPORTANCE_MIN, ), @@ -256,35 +230,29 @@ class NotificationController @Inject constructor( } private fun mappingsPausedNotification(): NotificationModel { + // Since Notification trampolines are no longer allowed, the notification + // must directly launch the accessibility settings instead of relaying the request + // through a broadcast receiver that eventually calls the ServiceAdapter. val stopServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) { - NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS) + KMNotificationAction.Activity.AccessibilitySettings } else { - NotificationIntentType.Broadcast(actionStopService) + KMNotificationAction.Broadcast.StopAccessibilityService } return NotificationModel( id = ID_TOGGLE_MAPPINGS, - channel = CHANNEL_TOGGLE_KEYMAPS, + channel = CHANNEL_TOGGLE_KEY_MAPS, title = getString(R.string.notification_keymaps_paused_title), text = getString(R.string.notification_keymaps_paused_text), icon = R.drawable.ic_notification_play, - onClickAction = NotificationIntentType.MainActivity(), + onClickAction = KMNotificationAction.Activity.MainActivity(), showOnLockscreen = true, onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_action_resume), - NotificationIntentType.Broadcast(actionResumeMappings), - ), - NotificationModel.Action( - getString(R.string.notification_action_dismiss), - NotificationIntentType.Broadcast(actionDismissToggleMappings), - ), - NotificationModel.Action( - getString(R.string.notification_action_stop_acc_service), - stopServiceAction, - ), + KMNotificationAction.Broadcast.ResumeKeyMaps to getString(R.string.notification_action_resume), + KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), + stopServiceAction to getString(R.string.notification_action_stop_acc_service) ), ) } @@ -294,34 +262,25 @@ class NotificationController @Inject constructor( // must directly launch the accessibility settings instead of relaying the request // through a broadcast receiver that eventually calls the ServiceAdapter. val stopServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) { - NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS) + KMNotificationAction.Activity.AccessibilitySettings } else { - NotificationIntentType.Broadcast(actionStopService) + KMNotificationAction.Broadcast.StopAccessibilityService } return NotificationModel( id = ID_TOGGLE_MAPPINGS, - channel = CHANNEL_TOGGLE_KEYMAPS, + channel = CHANNEL_TOGGLE_KEY_MAPS, title = getString(R.string.notification_keymaps_resumed_title), text = getString(R.string.notification_keymaps_resumed_text), icon = R.drawable.ic_notification_pause, - onClickAction = NotificationIntentType.MainActivity(), + onClickAction = KMNotificationAction.Activity.MainActivity(), showOnLockscreen = true, onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_action_pause), - NotificationIntentType.Broadcast(actionPauseMappings), - ), - NotificationModel.Action( - getString(R.string.notification_action_dismiss), - NotificationIntentType.Broadcast(actionDismissToggleMappings), - ), - NotificationModel.Action( - getString(R.string.notification_action_stop_acc_service), - stopServiceAction, - ), + KMNotificationAction.Broadcast.PauseKeyMaps to getString(R.string.notification_action_pause), + KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), + stopServiceAction to getString(R.string.notification_action_stop_acc_service) ), ) } @@ -330,28 +289,26 @@ class NotificationController @Inject constructor( // Since Notification trampolines are no longer allowed, the notification // must directly launch the accessibility settings instead of relaying the request // through a broadcast receiver that eventually calls the ServiceAdapter. - val onClickAction = if (controlAccessibilityService.isUserInteractionRequired()) { - NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS) + val startServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) { + KMNotificationAction.Activity.AccessibilitySettings } else { - NotificationIntentType.Broadcast(actionStartService) + KMNotificationAction.Broadcast.StartAccessibilityService } return NotificationModel( id = ID_TOGGLE_MAPPINGS, - channel = CHANNEL_TOGGLE_KEYMAPS, + channel = CHANNEL_TOGGLE_KEY_MAPS, title = getString(R.string.notification_accessibility_service_disabled_title), text = getString(R.string.notification_accessibility_service_disabled_text), icon = R.drawable.ic_notification_pause, - onClickAction = onClickAction, + onClickAction = startServiceAction, showOnLockscreen = true, onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_action_dismiss), - NotificationIntentType.Broadcast(actionDismissToggleMappings), + KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), + ), - ), ) } @@ -359,28 +316,25 @@ class NotificationController @Inject constructor( // Since Notification trampolines are no longer allowed, the notification // must directly launch the accessibility settings instead of relaying the request // through a broadcast receiver that eventually calls the ServiceAdapter. - val onClickAction = if (controlAccessibilityService.isUserInteractionRequired()) { - NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS) + val restartServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) { + KMNotificationAction.Activity.AccessibilitySettings } else { - NotificationIntentType.Broadcast(actionRestartService) + KMNotificationAction.Broadcast.RestartAccessibilityService } return NotificationModel( id = ID_TOGGLE_MAPPINGS, - channel = CHANNEL_TOGGLE_KEYMAPS, + channel = CHANNEL_TOGGLE_KEY_MAPS, title = getString(R.string.notification_accessibility_service_crashed_title), text = getString(R.string.notification_accessibility_service_crashed_text), icon = R.drawable.ic_notification_pause, - onClickAction = onClickAction, + onClickAction = restartServiceAction, showOnLockscreen = true, onGoing = true, priority = NotificationCompat.PRIORITY_MIN, bigTextStyle = true, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_action_restart_accessibility_service), - onClickAction, - ), + restartServiceAction to getString(R.string.notification_action_restart_accessibility_service) ), ) } @@ -395,10 +349,7 @@ class NotificationController @Inject constructor( onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_toggle_keyboard_action), - intentType = NotificationIntentType.Broadcast(actionToggleKeyboard), - ), + KMNotificationAction.Broadcast.TogglerKeyMapperIme to getString(R.string.notification_toggle_keyboard_action) ), ) @@ -408,7 +359,7 @@ class NotificationController @Inject constructor( title = getString(R.string.notification_keyboard_hidden_title), text = getString(R.string.notification_keyboard_hidden_text), icon = R.drawable.ic_notification_keyboard_hide, - onClickAction = NotificationIntentType.Broadcast(actionShowKeyboard), + onClickAction = KMNotificationAction.Broadcast.ShowKeyboard, showOnLockscreen = false, onGoing = true, priority = NotificationCompat.PRIORITY_LOW, @@ -420,7 +371,7 @@ class NotificationController @Inject constructor( title = getString(R.string.notification_floating_buttons_feature_title), text = getString(R.string.notification_floating_buttons_feature_text), icon = R.drawable.outline_bubble_chart_24, - onClickAction = NotificationIntentType.MainActivity(BaseMainActivity.ACTION_USE_FLOATING_BUTTONS), + onClickAction = KMNotificationAction.Activity.MainActivity(BaseMainActivity.ACTION_USE_FLOATING_BUTTONS), priority = NotificationCompat.PRIORITY_LOW, autoCancel = true, onGoing = false, diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index abd0a544a3..f4bc1e4b29 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1637,7 +1637,14 @@ Searching for pairing code and port… Unable to find pairing port and code - Tap on the button to pair with pairing code and type it in here + Tap on the button to pair with pairing code and type the code in here + + Starting PRO Mode failed + Please try again + + Pairing failed + Keep the pairing code popup on-screen when submitting the pairing code + Input pairing code What can I do with PRO mode? 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: Wi-Fi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device. diff --git a/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt b/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt new file mode 100644 index 0000000000..07b03f5908 --- /dev/null +++ b/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt @@ -0,0 +1,45 @@ +package io.github.sds100.keymapper.common.notifications + +sealed class KMNotificationAction { + enum class IntentAction { + RESUME_KEY_MAPS, + PAUSE_KEY_MAPS, + PAIRING_CODE_REPLY, + DISMISS_TOGGLE_KEY_MAPS_NOTIFICATION, + STOP_ACCESSIBILITY_SERVICE, + START_ACCESSIBILITY_SERVICE, + RESTART_ACCESSIBILITY_SERVICE, + TOGGLE_KEY_MAPPER_IME, + SHOW_KEYBOARD + } + + sealed class Broadcast(val intentAction: IntentAction) : KMNotificationAction() { + data object ResumeKeyMaps : Broadcast(IntentAction.RESUME_KEY_MAPS) + data object PauseKeyMaps : Broadcast(IntentAction.PAUSE_KEY_MAPS) + data object DismissToggleKeyMapsNotification : + Broadcast(IntentAction.DISMISS_TOGGLE_KEY_MAPS_NOTIFICATION) + + data object StopAccessibilityService : Broadcast(IntentAction.STOP_ACCESSIBILITY_SERVICE) + data object StartAccessibilityService : Broadcast(IntentAction.START_ACCESSIBILITY_SERVICE) + data object RestartAccessibilityService : + Broadcast(IntentAction.RESTART_ACCESSIBILITY_SERVICE) + + data object TogglerKeyMapperIme : Broadcast(IntentAction.TOGGLE_KEY_MAPPER_IME) + data object ShowKeyboard : Broadcast(IntentAction.SHOW_KEYBOARD) + } + + sealed class RemoteInput( + val key: String, val intentAction: IntentAction + ) : KMNotificationAction() { + + data object PairingCode : RemoteInput( + key = "pairing_code", + intentAction = IntentAction.PAIRING_CODE_REPLY + ) + } + + sealed class Activity() : KMNotificationAction() { + data object AccessibilitySettings : Activity() + data class MainActivity(val action: String? = null) : Activity() + } +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt index 8d4c156586..cf932ac10e 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt @@ -11,6 +11,7 @@ import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.success import io.github.sds100.keymapper.common.utils.then import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -42,8 +43,11 @@ class AdbManagerImpl @Inject constructor( val result = withContext(Dispatchers.IO) { return@withContext commandMutex.withLock { adbConnectMdns.start() - - val port = withTimeout(1000L) { adbConnectMdns.port.first { it != null } } + val port = try { + withTimeout(1000L) { adbConnectMdns.port.first { it != null } } + } catch (_: TimeoutCancellationException) { + null + } adbConnectMdns.stop() if (port == null) { @@ -77,10 +81,14 @@ class AdbManagerImpl @Inject constructor( return result } - override suspend fun pair(code: Int): KMResult { + override suspend fun pair(code: String): KMResult { return pairMutex.withLock { adbPairMdns.start() - val port = withTimeout(1000L) { adbPairMdns.port.first { it != null } } + val port = try { + withTimeout(1000L) { adbPairMdns.port.first { it != null } } + } catch (_: TimeoutCancellationException) { + null + } adbPairMdns.stop() if (port == null) { @@ -128,5 +136,5 @@ interface AdbManager { suspend fun executeCommand(command: String): KMResult @RequiresApi(Build.VERSION_CODES.R) - suspend fun pair(code: Int): KMResult + suspend fun pair(code: String): KMResult } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt index 06477219b1..d5c9de3c7c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt @@ -70,6 +70,9 @@ internal class AdbMdns( } fun start() { + // Reset the port so searching starts again. + _port.update { null } + if (running) { return } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index a7cd4a891d..a6d41a580a 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -21,8 +21,10 @@ import io.github.sds100.keymapper.sysbridge.adb.AdbManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -50,8 +52,10 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override val isWirelessDebuggingEnabled: MutableStateFlow = MutableStateFlow(getWirelessDebuggingEnabled()) - override val setupAssistantStep: MutableStateFlow = - MutableStateFlow(null) + // Use a SharedFlow so that the same value can be emitted repeatedly. + override val setupAssistantStep: MutableSharedFlow = MutableSharedFlow() + private val setupAssistantStepState = + setupAssistantStep.stateIn(coroutineScope, SharingStarted.Eagerly, null) init { // Automatically go back to the Key Mapper app when turning on wireless debugging @@ -63,7 +67,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // Only go back if the user is currently setting up the wireless debugging step. // This stops Key Mapper going back if they are turning on wireless debugging // for another reason. - if (isEnabled && setupAssistantStep.value == SystemBridgeSetupStep.WIRELESS_DEBUGGING) { + if (isEnabled && setupAssistantStepState.value == SystemBridgeSetupStep.WIRELESS_DEBUGGING) { getKeyMapperAppTask()?.moveToFront() } } @@ -74,7 +78,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( SettingsUtils.settingsCallbackFlow(ctx, uri).collect { val isEnabled = getDeveloperOptionsEnabled() - if (isEnabled && setupAssistantStep.value == SystemBridgeSetupStep.DEVELOPER_OPTIONS) { + if (isEnabled && setupAssistantStepState.value == SystemBridgeSetupStep.DEVELOPER_OPTIONS) { getKeyMapperAppTask()?.moveToFront() } } @@ -106,14 +110,11 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } @RequiresApi(Build.VERSION_CODES.R) - override suspend fun pairWirelessAdb(code: Int): KMResult { + override suspend fun pairWirelessAdb(code: String): KMResult { return adbManager.pair(code).onSuccess { - setupAssistantStep.update { value -> - if (value == SystemBridgeSetupStep.ADB_PAIRING) { - null - } else { - value - } + // Clear the step if still at the pairing step. + if (setupAssistantStepState.value == SystemBridgeSetupStep.ADB_PAIRING) { + setupAssistantStep.emit(null) } } } @@ -213,7 +214,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @SuppressLint("ObsoleteSdkInt") @RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeSetupController { - val setupAssistantStep: StateFlow + val setupAssistantStep: Flow val isDeveloperOptionsEnabled: Flow fun enableDeveloperOptions() @@ -227,7 +228,7 @@ interface SystemBridgeSetupController { suspend fun isAdbPaired(): Boolean @RequiresApi(Build.VERSION_CODES.R) - suspend fun pairWirelessAdb(code: Int): KMResult + suspend fun pairWirelessAdb(code: String): KMResult fun startWithRoot() fun startWithShizuku() diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index b8747aca85..b5eefd679a 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -130,7 +130,7 @@ class SystemBridgeStarter @Inject constructor( } // TODO enable usb debugging and disable authorization timeout - // Get the file that contains the external files + // TODO disable wireless debugging when started return startSystemBridge(executeCommand = adbManager::executeCommand).onFailure { error -> Timber.w("Failed to start system bridge with ADB: $error") } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index d86a2b318c..bfc6162436 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -236,17 +236,13 @@ class AndroidNetworkAdapter @Inject constructor( } } - private fun getIsWifiConnected(): Boolean { // Add this to your NetworkAdapter interface too - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val network = connectivityManager.activeNetwork ?: return false - val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false - return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) - } else { - @Suppress("DEPRECATION") - val networkInfo = connectivityManager.activeNetworkInfo ?: return false - @Suppress("DEPRECATION") - return networkInfo.isConnected && networkInfo.type == ConnectivityManager.TYPE_WIFI - } + // TODO this does not return true if the device is connected to a wifi network but there is no internet connection on it. + // Perhaps use connectivityManager.allNetworks and check them all for a transport. + // .activeNetwork gets the current one used to connect to the internet i think + private fun getIsWifiConnected(): Boolean { + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } fun invalidateState() { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt index d59ce18d0b..f62834cdac 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt @@ -1,13 +1,18 @@ package io.github.sds100.keymapper.system.notifications +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import kotlinx.coroutines.flow.Flow - interface NotificationAdapter { /** * The string is the ID of the action. */ - val onNotificationActionClick: Flow + val onNotificationActionClick: Flow + + /** + * Emits text input from notification actions that support RemoteInput. + */ + val onNotificationRemoteInput: Flow fun showNotification(notification: NotificationModel) fun dismissNotification(notificationId: Int) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt index e49dadbb2d..0d42285c30 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.system.notifications import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat +import io.github.sds100.keymapper.common.notifications.KMNotificationAction data class NotificationModel( @@ -13,14 +14,18 @@ data class NotificationModel( /** * Null if nothing should happen when the notification is tapped. */ - val onClickAction: NotificationIntentType? = null, + val onClickAction: KMNotificationAction? = null, val showOnLockscreen: Boolean, val onGoing: Boolean, /** * On Android Oreo and newer this does nothing because the channel priority is used. */ val priority: Int = NotificationCompat.PRIORITY_DEFAULT, - val actions: List = emptyList(), + + /** + * Maps the action intent to the label string. + */ + val actions: List> = emptyList(), /** * Clicking on the notification will automatically dismiss it. @@ -28,25 +33,5 @@ data class NotificationModel( val autoCancel: Boolean = false, val bigTextStyle: Boolean = false, val silent: Boolean = false -) { - data class Action(val text: String, val intentType: NotificationIntentType) -} - -/** - * Due to restrictions on notification trampolines in Android 12+ you can't launch - * activities from a broadcast receiver in response to a notification action. - */ -sealed class NotificationIntentType { - /** - * Broadcast an intent to the NotificationReceiver. - */ - data class Broadcast(val action: String) : NotificationIntentType() - - /** - * Launch the main activity with the specified action in the intent. If it is null - * then it will just launch the activity without a custom action. - */ - data class MainActivity(val customIntentAction: String? = null) : NotificationIntentType() +) - data class Activity(val action: String) : NotificationIntentType() -} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt new file mode 100644 index 0000000000..9e536e8de4 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt @@ -0,0 +1,13 @@ +package io.github.sds100.keymapper.system.notifications + +import io.github.sds100.keymapper.common.notifications.KMNotificationAction + +/** + * Represents text input from a notification action with RemoteInput. + * @param intentAction The intent action that triggered the text input + * @param text The text that was inputted by the user + */ +data class NotificationRemoteInput( + val intentAction: KMNotificationAction.IntentAction, + val text: String +) \ No newline at end of file From 694749234eb165f8de39bb0c6a9f58c7165ad74c Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 27 Aug 2025 23:53:56 +0200 Subject: [PATCH 171/215] #1394 enable USB debugging and disable ADB authorization timeout when system bridge is started with ADB --- .../keymapper/sysbridge/ISystemBridge.aidl | 3 +- .../manager/SystemBridgeConnectionManager.kt | 33 ++++++++++++- .../sysbridge/service/SystemBridge.kt | 46 +++++++++++++++++++ .../sysbridge/starter/SystemBridgeStarter.kt | 9 ++-- 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index f1d44011e7..d69186f529 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -8,7 +8,6 @@ interface ISystemBridge { // Destroy method defined by Shizuku server. This is required // for Shizuku user services. // See demo/service/UserService.java in the Shizuku-API repository. - // TODO use this from Key Mapper to kill the system bridge void destroy() = 16777114; boolean grabEvdevDevice(String devicePath) = 1; @@ -25,4 +24,6 @@ interface ISystemBridge { EvdevDeviceHandle[] getEvdevInputDevices() = 9; boolean setWifiEnabled(boolean enable) = 10; + + void putGlobalSetting(String name, String value) = 11; } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 28b78edc52..1603da2eb7 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -1,23 +1,29 @@ package io.github.sds100.keymapper.sysbridge.manager import android.annotation.SuppressLint +import android.content.Context import android.os.Build import android.os.IBinder import android.os.IBinder.DeathRecipient import android.os.RemoteException import androidx.annotation.RequiresApi +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.sysbridge.ISystemBridge import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import javax.inject.Inject import javax.inject.Singleton @@ -26,6 +32,7 @@ import javax.inject.Singleton */ @Singleton class SystemBridgeConnectionManagerImpl @Inject constructor( + @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, private val starter: SystemBridgeStarter, ) : SystemBridgeConnectionManager { @@ -36,7 +43,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( override val isConnected: Flow = systemBridgeFlow.map { it != null } private val deathRecipient: DeathRecipient = DeathRecipient { - // TODO show notification when pro mode is stopped for an unexpected reason. Do not show it if the user stopped it + // TODO show notification when pro mode is stopped for an unexpected reason. Do not show it if the user stopped it. Add action to open the set up screen through MainActivity action. synchronized(systemBridgeLock) { systemBridgeFlow.update { null } } @@ -83,7 +90,29 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) override fun startWithAdb() { coroutineScope.launch { - starter.startWithAdb() + starter.startWithAdb().onSuccess { + // Wait for the system bridge to connect + try { + withTimeout(3000) { isConnected.first { it } } + } catch (_: TimeoutCancellationException) { + return@launch + } + + this@SystemBridgeConnectionManagerImpl.run { bridge -> + // Disable automatic revoking of ADB pairings + bridge.putGlobalSetting( + "adb_allowed_connection_time", + "0" + ) + + // Enable USB debugging so the Shell user can keep running in the background + // even when disconnected from the WiFi network + bridge.putGlobalSetting( + "adb_enabled", + "1" + ) + } + } } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 2e76025944..c2b4c5cb3c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -10,9 +10,13 @@ import android.os.Bundle import android.os.Handler import android.os.IBinder import android.os.Looper +import android.os.Process import android.os.ServiceManager +import android.os.UserHandle +import android.provider.Settings import android.util.Log import android.view.InputEvent +import androidx.core.os.bundleOf import io.github.sds100.keymapper.common.models.EvdevDeviceHandle import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.ISystemBridge @@ -250,6 +254,48 @@ internal class SystemBridge : ISystemBridge.Stub() { return writeEvdevEventNative(devicePath, type, code, value) } + override fun putGlobalSetting(name: String, value: String) { + val providerName = "settings" + + val token: IBinder? = null + val userId: Int = UserHandle::class.java.getMethod("getCallingUserId").invoke(null) as Int + + Log.d(TAG, "Putting global setting $name = $value for user $userId") + + val settingsProvider = ActivityManagerApis.getContentProviderExternal( + providerName, + userId, + token, + providerName + ) + + if (settingsProvider == null) { + Log.w(TAG, "Failed to get settings provider") + return + } + + val bundle = bundleOf( + Settings.NameValueTable.VALUE to value + ) + + val packageName = if (Process.myUid() == Process.ROOT_UID) { + "root" + } else { + "com.android.shell" + } + + IContentProviderUtils.callCompat( + settingsProvider, + packageName, + providerName, + "PUT_global", + name, + bundle + ) + + Log.i(TAG, "Put global setting $name = $value") + } + private fun sendBinderToApp(): Boolean { // Only support Key Mapper running in a single Android user for now so just send // it to the first user that accepts the binder. diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index b5eefd679a..6133e648af 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -129,11 +129,10 @@ class SystemBridgeStarter @Inject constructor( return KMError.Exception(IllegalStateException("User is locked")) } - // TODO enable usb debugging and disable authorization timeout - // TODO disable wireless debugging when started - return startSystemBridge(executeCommand = adbManager::executeCommand).onFailure { error -> - Timber.w("Failed to start system bridge with ADB: $error") - } + return startSystemBridge(executeCommand = adbManager::executeCommand) + .onFailure { error -> + Timber.w("Failed to start system bridge with ADB: $error") + } } suspend fun startWithRoot() { From 3e98ea05d9c2bd20dc3e3e000c8573d90defdf78 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 Aug 2025 12:20:45 +0200 Subject: [PATCH 172/215] #1394 show notification when system bridge is killed --- .../keymapper/base/ActivityViewModel.kt | 13 ++++-- .../sds100/keymapper/base/BaseMainActivity.kt | 44 ++++++++++++------- .../notifications/NotificationController.kt | 25 +++++++++++ .../utils/navigation/NavigationProvider.kt | 4 +- base/src/main/res/values/strings.xml | 3 ++ .../manager/SystemBridgeConnectionManager.kt | 23 +++++++--- 6 files changed, 86 insertions(+), 26 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt index a8b48bb2b9..db6ccc08af 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt @@ -3,8 +3,9 @@ package io.github.sds100.keymapper.base import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider -import io.github.sds100.keymapper.base.utils.navigation.NavigationProviderImpl +import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.ViewModelHelper @@ -15,12 +16,12 @@ import javax.inject.Inject class ActivityViewModel @Inject constructor( resourceProvider: ResourceProvider, dialogProvider: DialogProvider, + navigationProvider: NavigationProvider ) : ViewModel(), ResourceProvider by resourceProvider, DialogProvider by dialogProvider, - NavigationProvider by NavigationProviderImpl() { + NavigationProvider by navigationProvider { - var handledActivityLaunchIntent: Boolean = false var previousNightMode: Int? = null fun onCantFindAccessibilitySettings() { @@ -31,4 +32,10 @@ class ActivityViewModel @Inject constructor( ) } } + + fun launchProModeSetup() { + viewModelScope.launch { + navigate("pro_mode_setup", NavDestination.ProModeSetup) + } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index af493bfc97..d7cbf67ec0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -31,7 +31,6 @@ import io.github.sds100.keymapper.base.system.accessibility.AccessibilityService import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter @@ -60,6 +59,9 @@ abstract class BaseMainActivity : AppCompatActivity() { const val ACTION_SAVE_FILE = "${BuildConfig.LIBRARY_PACKAGE_NAME}.ACTION_SAVE_FILE" const val EXTRA_FILE_URI = "${BuildConfig.LIBRARY_PACKAGE_NAME}.EXTRA_FILE_URI" + + const val ACTION_START_SYSTEM_BRIDGE = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.ACTION_START_SYSTEM_BRIDGE" } @Inject @@ -171,21 +173,6 @@ abstract class BaseMainActivity : AppCompatActivity() { } .launchIn(lifecycleScope) - // Must launch when the activity is resumed - // so the nav controller can be found - launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - if (viewModel.handledActivityLaunchIntent) { - return@launchRepeatOnLifecycle - } - - when (intent?.action) { - ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG -> { - viewModel.onCantFindAccessibilitySettings() - viewModel.handledActivityLaunchIntent = true - } - } - } - IntentFilter().apply { addAction(ACTION_SAVE_FILE) @@ -196,6 +183,8 @@ abstract class BaseMainActivity : AppCompatActivity() { ContextCompat.RECEIVER_EXPORTED, ) } + + handleIntent(intent) } override fun onResume() { @@ -237,6 +226,29 @@ abstract class BaseMainActivity : AppCompatActivity() { } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + handleIntent(intent) + } + + private fun handleIntent(intent: Intent?) { + when (intent?.action) { + ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG -> { + viewModel.onCantFindAccessibilitySettings() + // Only clear the intent if it is handled in case it is used elsewhere + this.intent = null + } + + ACTION_START_SYSTEM_BRIDGE -> { + viewModel.launchProModeSetup() + + // Only clear the intent if it is handled in case it is used elsewhere + this.intent = null + } + } + } + private fun saveFile(originalFile: Uri, targetFile: Uri) { lifecycleScope.launch(Dispatchers.IO) { targetFile.openOutputStream(this@BaseMainActivity)?.use { output -> diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 2ccb913fa5..157f51c146 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.system.notifications +import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import io.github.sds100.keymapper.base.BaseMainActivity @@ -16,10 +17,12 @@ import io.github.sds100.keymapper.common.utils.DefaultDispatcherProvider import io.github.sds100.keymapper.common.utils.DispatcherProvider import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceState import io.github.sds100.keymapper.system.notifications.NotificationChannelModel import io.github.sds100.keymapper.system.notifications.NotificationModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -42,6 +45,7 @@ class NotificationController @Inject constructor( private val hideInputMethod: ShowHideInputMethodUseCase, private val onboardingUseCase: OnboardingUseCase, private val resourceProvider: ResourceProvider, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), ) : ResourceProvider by resourceProvider { @@ -51,6 +55,7 @@ class NotificationController @Inject constructor( private const val ID_TOGGLE_MAPPINGS = 231 private const val ID_TOGGLE_KEYBOARD = 143 const val ID_SETUP_ASSISTANT = 144 + const val ID_SYSTEM_BRIDGE_DIED = 145 // private const val ID_FEATURE_ASSISTANT_TRIGGER = 900 private const val ID_FEATURE_FLOATING_BUTTONS = 901 @@ -171,6 +176,26 @@ class NotificationController @Inject constructor( else -> Unit // Ignore other notification actions } }.launchIn(coroutineScope) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + coroutineScope.launch { + systemBridgeConnectionManager.onUnexpectedDeath.consumeEach { + val model = NotificationModel( + id = ID_SYSTEM_BRIDGE_DIED, + channel = CHANNEL_SETUP_ASSISTANT, + title = getString(R.string.system_bridge_died_notification_title), + text = getString(R.string.system_bridge_died_notification_text), + icon = R.drawable.pro_mode, + onClickAction = KMNotificationAction.Activity.MainActivity(BaseMainActivity.ACTION_START_SYSTEM_BRIDGE), + showOnLockscreen = true, + onGoing = false, + priority = NotificationCompat.PRIORITY_HIGH, + autoCancel = true + ) + manageNotifications.show(model) + } + } + } } fun onOpenApp() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt index 5b3bf43a13..00f811d795 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.utils.navigation +import android.annotation.SuppressLint import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -172,8 +173,9 @@ fun SetupNavigation( navigationProvider: NavigationProviderImpl, navController: NavHostController, ) { + @SuppressLint("StateFlowValueCalledInComposition") val navEvent: NavigateEvent? by navigationProvider.onNavigate - .collectAsStateWithLifecycle(null) + .collectAsStateWithLifecycle(navigationProvider.onNavigate.value) val returnResult: String? by navigationProvider.onReturnResult .collectAsStateWithLifecycle(null) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index f4bc1e4b29..87adeb4009 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1651,4 +1651,7 @@ Show PRO mode info Dismiss + PRO Mode stopped unexpectedly + Tap to restart PRO Mode + diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 1603da2eb7..293b95acd2 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -1,13 +1,11 @@ package io.github.sds100.keymapper.sysbridge.manager import android.annotation.SuppressLint -import android.content.Context import android.os.Build import android.os.IBinder import android.os.IBinder.DeathRecipient import android.os.RemoteException import androidx.annotation.RequiresApi -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success @@ -17,6 +15,7 @@ import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first @@ -32,7 +31,6 @@ import javax.inject.Singleton */ @Singleton class SystemBridgeConnectionManagerImpl @Inject constructor( - @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, private val starter: SystemBridgeStarter, ) : SystemBridgeConnectionManager { @@ -41,11 +39,20 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( private var systemBridgeFlow: MutableStateFlow = MutableStateFlow(null) override val isConnected: Flow = systemBridgeFlow.map { it != null } + override val onUnexpectedDeath: Channel = Channel() + private var isExpectedDeath: Boolean = false private val deathRecipient: DeathRecipient = DeathRecipient { - // TODO show notification when pro mode is stopped for an unexpected reason. Do not show it if the user stopped it. Add action to open the set up screen through MainActivity action. synchronized(systemBridgeLock) { systemBridgeFlow.update { null } + + if (!isExpectedDeath) { + coroutineScope.launch { + onUnexpectedDeath.send(Unit) + } + } + + isExpectedDeath = false } } @@ -79,10 +86,13 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( override fun stopSystemBridge() { synchronized(systemBridgeLock) { + isExpectedDeath = true + try { systemBridgeFlow.value?.destroy() - } catch (_: RemoteException) { - deathRecipient.binderDied() + } catch (e: RemoteException) { + // This is expected to throw an exception because the destroy() method kills + // the process. } } } @@ -131,6 +141,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( @RequiresApi(Build.VERSION_CODES.Q) interface SystemBridgeConnectionManager { val isConnected: Flow + val onUnexpectedDeath: Channel fun run(block: (ISystemBridge) -> T): KMResult fun stopSystemBridge() From fcf2202f82b53545d2185eb40335e65d4a434d48 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 Aug 2025 12:24:44 +0200 Subject: [PATCH 173/215] #1394 use version catalogs for sysbridge and systemstubs build.gradle --- gradle/libs.versions.toml | 14 ++++++++++++++ sysbridge/build.gradle.kts | 13 ++++++------- systemstubs/build.gradle.kts | 7 +++---- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index be0a6ee1b2..4992c424ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ target-sdk = "36" android-gradle-plugin = "8.9.1" androidx-activity = "1.10.1" +androidx-annotation = "1.9.1" androidx-appcompat = "1.7.0" androidx-arch-core-testing = "2.2.0" androidx-constraintlayout = "2.2.1" @@ -38,7 +39,13 @@ epoxy = "4.6.2" flexbox = "3.0.0" google-accompanist-drawablepainter = "0.35.0-alpha" hiddenapibypass = "4.3" +hiddenapibypass-lsposed = "6.1" introshowcaseview = "2.0.2" +conscrypt-android = "2.5.3" +boringssl-ndk = "20250114" +bouncycastle-bcpkix = "1.70" +appiconloader = "1.5.0" +rikkax-core = "1.4.1" junit = "4.13.2" junit-params = "1.1.1" @@ -85,6 +92,7 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- # AndroidX androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidx-activity" } +androidx-annotation-jvm = { group = "androidx.annotation", name = "annotation-jvm", version.ref = "androidx-annotation" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidx-arch-core-testing" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } @@ -173,8 +181,14 @@ github-mflisar-dragselectrecyclerview = { group = "com.github.MFlisar", name = " jakewharton-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } kotson = { group = "com.github.salomonbrys.kotson", name = "kotson", version.ref = "kotson" } lsposed-hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" } +lsposed-hiddenapibypass-updated = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass-lsposed" } net-lingala-zip4j = { group = "net.lingala.zip4j", name = "zip4j", version.ref = "lingala-zip4j" } github-topjohnwu-libsu = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu-core" } +conscrypt-android = { group = "org.conscrypt", name = "conscrypt-android", version.ref = "conscrypt-android" } +vvb2060-ndk-boringssl = { group = "io.github.vvb2060.ndk", name = "boringssl", version.ref = "boringssl-ndk" } +bouncycastle-bcpkix = { group = "org.bouncycastle", name = "bcpkix-jdk15on", version.ref = "bouncycastle-bcpkix" } +zhanghai-appiconloader = { group = "me.zhanghai.android.appiconloader", name = "appiconloader", version.ref = "appiconloader" } +rikka-rikkax-core = { group = "dev.rikka.rikkax.core", name = "core-ktx", version.ref = "rikkax-core" } # Gradle Plugins - Aliases for buildscript dependencies / plugins block diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index ad955d24ac..fd99d1ed85 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -85,8 +85,7 @@ dependencies { implementation(libs.jakewharton.timber) - // TODO use version catalog - implementation("org.conscrypt:conscrypt-android:2.5.3") + implementation(libs.conscrypt.android) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.dagger.hilt.android) @@ -98,11 +97,11 @@ dependencies { compileOnly(libs.rikka.hidden.stub) // From Shizuku :manager module build.gradle file. - implementation("io.github.vvb2060.ndk:boringssl:20250114") - implementation("org.lsposed.hiddenapibypass:hiddenapibypass:6.1") - 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(libs.vvb2060.ndk.boringssl) + implementation(libs.lsposed.hiddenapibypass.updated) + implementation(libs.bouncycastle.bcpkix) + implementation(libs.zhanghai.appiconloader) + implementation(libs.rikka.rikkax.core) } tasks.named("preBuild") { diff --git a/systemstubs/build.gradle.kts b/systemstubs/build.gradle.kts index 081785188e..9c067452a1 100644 --- a/systemstubs/build.gradle.kts +++ b/systemstubs/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } android { @@ -36,6 +36,5 @@ android { } dependencies { - // TODO use version catalogs - implementation("androidx.annotation:annotation-jvm:1.9.1") + implementation(libs.androidx.annotation.jvm) } From 6a8a4cde49c50f373c144352f22921698d1c5083 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 Aug 2025 12:35:18 +0200 Subject: [PATCH 174/215] #1394 NetworkAdapter: check all networks for a WiFi transport --- .../io/github/sds100/keymapper/sysbridge/ktx/Log.kt | 2 -- .../system/network/AndroidNetworkAdapter.kt | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt index 7f6a358f6a..b951be5e1c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt @@ -4,8 +4,6 @@ package io.github.sds100.keymapper.sysbridge.ktx import android.util.Log -// TODO replace with Timber usage. - inline val T.TAG: String get() = T::class.java.simpleName.let { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index bfc6162436..3469bf0ef5 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -125,6 +125,8 @@ class AndroidNetworkAdapter @Inject constructor( .build() connectivityManager.registerNetworkCallback(networkRequest, networkCallback) + + Timber.e("Is wifi connected: ${isWifiConnected.value}") } override fun isWifiEnabled(): Boolean = wifiManager.isWifiEnabled @@ -236,13 +238,11 @@ class AndroidNetworkAdapter @Inject constructor( } } - // TODO this does not return true if the device is connected to a wifi network but there is no internet connection on it. - // Perhaps use connectivityManager.allNetworks and check them all for a transport. - // .activeNetwork gets the current one used to connect to the internet i think private fun getIsWifiConnected(): Boolean { - val network = connectivityManager.activeNetwork ?: return false - val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false - return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + return connectivityManager.allNetworks.any { network -> + connectivityManager.getNetworkCapabilities(network) + ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ?: false + } } fun invalidateState() { From d41270fa98751cf2dda8c45cc471aed63a50ad24 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 Aug 2025 15:16:16 +0200 Subject: [PATCH 175/215] #1394 check every minute if Key Mapper is uninstalled when it unbinds from the system bridge --- .../sysbridge/service/SystemBridge.kt | 133 ++++++++++++------ .../system/network/AndroidNetworkAdapter.kt | 2 - 2 files changed, 89 insertions(+), 46 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index c2b4c5cb3c..5e63c4e951 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint import android.content.Context import android.content.IContentProvider +import android.content.pm.ApplicationInfo import android.ddm.DdmHandleAppName import android.hardware.input.IInputManager import android.net.wifi.IWifiManager @@ -25,7 +26,9 @@ import io.github.sds100.keymapper.sysbridge.provider.SystemBridgeBinderProvider import io.github.sds100.keymapper.sysbridge.utils.IContentProviderUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import rikka.hidden.compat.ActivityManagerApis import rikka.hidden.compat.DeviceIdleControllerApis @@ -38,12 +41,6 @@ import kotlin.system.exitProcess @SuppressLint("LogNotTimber") internal class SystemBridge : ISystemBridge.Stub() { - // TODO observe if Key Mapper is uninstalled and stop the process. Look at ApkChangedObservers in Shizuku code. - // TODO every minute ping key mapper and if no response then stop the process. - // TODO if no response when sending to the callback, stop the process. - - // TODO return error code and map this to a SystemBridgeError in key mapper - external fun grabEvdevDeviceNative(devicePath: String): Boolean external fun ungrabEvdevDeviceNative(devicePath: String): Boolean @@ -66,9 +63,10 @@ internal class SystemBridge : ISystemBridge.Stub() { System.getProperty("keymapper_sysbridge.package") private const val SHELL_PACKAGE = "com.android.shell" + private const val KEYMAPPER_CHECK_INTERVAL_MS = 60 * 1000L // 1 minute + @JvmStatic fun main(args: Array) { - Log.i(TAG, "Sysbridge package name = $systemBridgePackageName") DdmHandleAppName.setAppName("keymapper_sysbridge", 0) @Suppress("DEPRECATION") Looper.prepareMainLooper() @@ -79,7 +77,6 @@ internal class SystemBridge : ISystemBridge.Stub() { private fun waitSystemService(name: String?) { while (ServiceManager.getService(name) == null) { try { - Log.i(TAG, "service $name is not started, wait 1s.") Thread.sleep(1000) } catch (e: InterruptedException) { Log.w(TAG, e.message, e) @@ -96,26 +93,25 @@ internal class SystemBridge : ISystemBridge.Stub() { uid: Int, foregroundActivities: Boolean ) { - // Do not send the binder if the binder is already sent to the user or - // the app is not in the foreground. - if (isBinderSent || !foregroundActivities) { + // Do not send the binder if the app is not in the foreground. + if (!foregroundActivities) { return } - val packages: List = - PackageManagerApis.getPackagesForUidNoThrow(uid).filterNotNull() - - if (packages.contains(systemBridgePackageName)) { + if (getKeyMapperPackageInfo() == null) { + Log.i(TAG, "Key Mapper app not installed - exiting") + destroy() + } else { synchronized(sendBinderLock) { - Log.i(TAG, "Key Mapper process started, send binder to app") - - sendBinderToApp() + if (!isBinderSent) { + Log.i(TAG, "Key Mapper process started, send binder to app") + mainHandler.post { + sendBinderToApp() + } + } } } } - - override fun onProcessDied(pid: Int, uid: Int) { - } } private val sendBinderLock: Any = Any() @@ -124,10 +120,14 @@ internal class SystemBridge : ISystemBridge.Stub() { private val coroutineScope: CoroutineScope = MainScope() private val mainHandler = Handler(Looper.myLooper()!!) + private val keyMapperCheckLock: Any = Any() + private var keyMapperCheckJob: Job? = null + private val evdevCallbackLock: Any = Any() private var evdevCallback: IEvdevCallback? = null private val evdevCallbackDeathRecipient: IBinder.DeathRecipient = IBinder.DeathRecipient { Log.i(TAG, "EvdevCallback binder died") + synchronized(sendBinderLock) { isBinderSent = false } @@ -135,11 +135,20 @@ internal class SystemBridge : ISystemBridge.Stub() { coroutineScope.launch(Dispatchers.Default) { stopEvdevEventLoop() } + + // Start periodic check for Key Mapper installation + startKeyMapperPeriodicCheck() } private val inputManager: IInputManager private val wifiManager: IWifiManager + private val processPackageName = if (Process.myUid() == Process.ROOT_UID) { + "root" + } else { + "com.android.shell" + } + init { val libraryPath = System.getProperty("keymapper_sysbridge.library.path") @SuppressLint("UnsafeDynamicallyLoadedCode") @@ -160,19 +169,11 @@ internal class SystemBridge : ISystemBridge.Stub() { wifiManager = IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) - // TODO check that the key mapper app is installed, otherwise end the process. -// val ai: ApplicationInfo? = rikka.shizuku.server.ShizukuService.getManagerApplicationInfo() -// if (ai == null) { -// System.exit(ServerConstants.MANAGER_APP_NOT_FOUND) -// } + val applicationInfo = getKeyMapperPackageInfo() - // TODO listen for key mapper being uninstalled, and stop the process -// ApkChangedObservers.start(ai.sourceDir, { -// if (rikka.shizuku.server.ShizukuService.getManagerApplicationInfo() == null) { -// LOGGER.w("manager app is uninstalled in user 0, exiting...") -// System.exit(ServerConstants.MANAGER_APP_NOT_FOUND) -// } -// }) + if (applicationInfo == null) { + destroy() + } ActivityManagerApis.registerProcessObserver(processObserver) @@ -182,9 +183,54 @@ internal class SystemBridge : ISystemBridge.Stub() { } } + private fun getKeyMapperPackageInfo(): ApplicationInfo? = + PackageManagerApis.getApplicationInfoNoThrow(systemBridgePackageName, 0, 0) + + private fun startKeyMapperPeriodicCheck() { + synchronized(keyMapperCheckLock) { + keyMapperCheckJob?.cancel() + + Log.i(TAG, "Starting periodic Key Mapper installation check") + + keyMapperCheckJob = coroutineScope.launch(Dispatchers.Default) { + try { + while (true) { + if (getKeyMapperPackageInfo() == null) { + Log.i(TAG, "Key Mapper not installed - exiting") + destroy() + break + } else { + // While Key Mapper is still installed but not bound, then periodically + // check if it has uninstalled + delay(KEYMAPPER_CHECK_INTERVAL_MS) + } + } + } finally { + // Clear the job reference when the coroutine completes + synchronized(keyMapperCheckLock) { + if (keyMapperCheckJob?.isCompleted == true) { + keyMapperCheckJob = null + } + } + } + } + } + } + + private fun stopKeyMapperPeriodicCheck() { + synchronized(keyMapperCheckLock) { + keyMapperCheckJob?.cancel() + keyMapperCheckJob = null + Log.i(TAG, "Stopped periodic Key Mapper installation check") + } + } + override fun destroy() { Log.i(TAG, "SystemBridge destroyed") + // Clean up periodic check job + stopKeyMapperPeriodicCheck() + // Must be last line in this method because it halts the JVM. exitProcess(0) } @@ -194,6 +240,9 @@ internal class SystemBridge : ISystemBridge.Stub() { Log.i(TAG, "Register evdev callback") + // Stop periodic check since Key Mapper has reconnected + stopKeyMapperPeriodicCheck() + val binder = callback.asBinder() if (this.evdevCallback != null) { @@ -254,6 +303,10 @@ internal class SystemBridge : ISystemBridge.Stub() { return writeEvdevEventNative(devicePath, type, code, value) } + // TODO If Key Mapper has WRITE_SECURE_SETTINGS permission it can write to Global settings itself. + // Replace with a method to request permissions for Key Mapper. If Key Mapper does not have WRITE_SECURE_SETTINGS permission + // then the action will show an error that it needs WRITE_SECURE_SETTINGS. The action will explain that Key Mapper can grant itself if they start PRO Mode one-time. + // Everytime PRO mode is started, Key Mapper should grant itself WRITE_SECURE_SETTINGS override fun putGlobalSetting(name: String, value: String) { val providerName = "settings" @@ -278,15 +331,9 @@ internal class SystemBridge : ISystemBridge.Stub() { Settings.NameValueTable.VALUE to value ) - val packageName = if (Process.myUid() == Process.ROOT_UID) { - "root" - } else { - "com.android.shell" - } - IContentProviderUtils.callCompat( settingsProvider, - packageName, + processPackageName, providerName, "PUT_global", name, @@ -319,10 +366,6 @@ internal class SystemBridge : ISystemBridge.Stub() { userId, 316, /* PowerExemptionManager#REASON_SHELL */"shell" ) - Log.d( - TAG, - "Add $userId:$systemBridgePackageName to power save temp whitelist for 30s" - ) } catch (tr: Throwable) { Log.e(TAG, tr.toString()) } @@ -366,6 +409,8 @@ internal class SystemBridge : ISystemBridge.Stub() { if (reply != null) { Log.i(TAG, "Send binder to user app $systemBridgePackageName in user $userId") isBinderSent = true + // Stop periodic check since connection is successful + stopKeyMapperPeriodicCheck() return true } else { Log.w( diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 3469bf0ef5..33137e69f0 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -125,8 +125,6 @@ class AndroidNetworkAdapter @Inject constructor( .build() connectivityManager.registerNetworkCallback(networkRequest, networkCallback) - - Timber.e("Is wifi connected: ${isWifiConnected.value}") } override fun isWifiEnabled(): Boolean = wifiManager.isWifiEnabled From ec587ba5817a7f9c6d9c450cdc40691a8bd7ea73 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 Aug 2025 16:15:56 +0200 Subject: [PATCH 176/215] #1394 grant WRITE_SECURE_SETTINGS permission when the System Bridge starts --- .../base/promode/SystemBridgeSetupUseCase.kt | 2 +- .../AutoGrantPermissionController.kt | 43 +++---- .../keymapper/common/utils/SettingsUtils.kt | 19 +++ .../keymapper/common/utils/UserHandleUtils.kt | 10 +- sysbridge/src/main/AndroidManifest.xml | 7 +- .../keymapper/sysbridge/ISystemBridge.aidl | 2 +- .../manager/SystemBridgeConnectionManager.kt | 73 ++++++++--- .../sysbridge/service/SystemBridge.kt | 55 +++----- .../service/SystemBridgeSetupController.kt | 56 ++++++--- .../sysbridge/starter/SystemBridgeStarter.kt | 61 ++++----- .../permissions/AndroidPermissionAdapter.kt | 118 ++++++------------ .../permission/PermissionManagerApis.kt | 54 ++++++++ 12 files changed, 295 insertions(+), 205 deletions(-) create mode 100644 systemstubs/src/main/java/android/permission/PermissionManagerApis.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 4041a22a3f..0e30ab7beb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -127,7 +127,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } override fun enableWirelessDebugging() { - systemBridgeSetupController.launchEnableWirelessDebuggingAssistant() + systemBridgeSetupController.enableWirelessDebugging() } @RequiresApi(Build.VERSION_CODES.R) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/AutoGrantPermissionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/AutoGrantPermissionController.kt index e00c42515d..566e81521d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/AutoGrantPermissionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/AutoGrantPermissionController.kt @@ -1,43 +1,44 @@ package io.github.sds100.keymapper.base.system.permissions import android.Manifest -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.popup.ToastAdapter +import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn +import timber.log.Timber import javax.inject.Inject +import javax.inject.Singleton +@Singleton class AutoGrantPermissionController @Inject constructor( private val coroutineScope: CoroutineScope, private val permissionAdapter: PermissionAdapter, - private val popupAdapter: ToastAdapter, - private val resourceProvider: ResourceProvider, + private val shizukuAdapter: ShizukuAdapter, ) { fun start() { // automatically grant WRITE_SECURE_SETTINGS if Key Mapper has root or shizuku permission - combine( - permissionAdapter.isGrantedFlow(Permission.ROOT), - permissionAdapter.isGrantedFlow(Permission.SHIZUKU), - permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), - ) { isRootGranted, isShizukuGranted, isWriteSecureSettingsGranted -> + permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS) + .flatMapLatest { isGranted -> + if (isGranted) { + emptyFlow() + } else { + combine( + permissionAdapter.isGrantedFlow(Permission.ROOT), + permissionAdapter.isGrantedFlow(Permission.SHIZUKU), + shizukuAdapter.isStarted, + ) { isRootGranted, isShizukuGranted, isShizukuStarted -> - if (!isWriteSecureSettingsGranted && (isRootGranted || isShizukuGranted)) { - permissionAdapter.grant(Manifest.permission.WRITE_SECURE_SETTINGS).onSuccess { - val stringRes = if (isRootGranted) { - R.string.toast_granted_itself_write_secure_settings_with_root - } else { - R.string.toast_granted_itself_write_secure_settings_with_shizuku + if (isRootGranted || (isShizukuGranted && isShizukuStarted)) { + Timber.i("Auto-granting WRITE_SECURE_SETTINGS permission") + permissionAdapter.grant(Manifest.permission.WRITE_SECURE_SETTINGS) + } } - - popupAdapter.show(resourceProvider.getString(stringRes)) } - } - }.launchIn(coroutineScope) + }.launchIn(coroutineScope) } } diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt index d80fdcdbb3..b5f21c0871 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt @@ -122,6 +122,25 @@ object SettingsUtils { } } + /** + * @return whether the setting was changed successfully + */ + @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) + inline fun putGlobalSetting(ctx: Context, name: String, value: T): Boolean { + val contentResolver = ctx.contentResolver + + return when (T::class) { + Int::class -> Settings.Global.putInt(contentResolver, name, value as Int) + String::class -> Settings.Global.putString(contentResolver, name, value as String) + Float::class -> Settings.Global.putFloat(contentResolver, name, value as Float) + Long::class -> Settings.Global.putLong(contentResolver, name, value as Long) + + else -> { + throw Exception("Setting type ${T::class} is not supported") + } + } + } + fun launchSettingsScreen(ctx: Context, action: String, fragmentArg: String? = null) { val intent = Intent(action).apply { if (fragmentArg != null) { diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/UserHandleUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/UserHandleUtils.kt index 43880737a5..59032ad5fa 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/UserHandleUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/UserHandleUtils.kt @@ -2,8 +2,12 @@ package io.github.sds100.keymapper.common.utils import android.os.UserHandle -fun UserHandle.getIdentifier(): Int { - val getIdentifierMethod = UserHandle::class.java.getMethod("getIdentifier") +object UserHandleUtils { + fun getCallingUserId(): Int { + return UserHandle::class.java.getMethod("getCallingUserId").invoke(null) as Int + } +} - return getIdentifierMethod.invoke(this) as Int +fun UserHandle.getIdentifier(): Int { + return UserHandle::class.java.getMethod("getIdentifier").invoke(this) as Int } diff --git a/sysbridge/src/main/AndroidManifest.xml b/sysbridge/src/main/AndroidManifest.xml index dad1614a20..77951bd794 100644 --- a/sysbridge/src/main/AndroidManifest.xml +++ b/sysbridge/src/main/AndroidManifest.xml @@ -1,5 +1,10 @@ - + + + - Keyboard picker Pause/Resume key maps Keyboard is hidden warning Toggle Key Mapper keyboard diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 849b2f6910..c7f4c7c307 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -121,4 +121,7 @@ object Keys { val isProModeAutoStartBootEnabled = booleanPreferencesKey("key_is_pro_mode_auto_start_boot_enabled") + + val isSystemBridgeEmergencyKilled = + booleanPreferencesKey("key_is_system_bridge_emergency_killed") } diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl index b1f375bd73..f6b4039a6d 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl @@ -3,4 +3,5 @@ package io.github.sds100.keymapper.sysbridge; interface IEvdevCallback { oneway void onEvdevEventLoopStarted(); boolean onEvdevEvent(String devicePath, long timeSec, long timeUsec, int type, int code, int value, int androidCode); + void onEmergencyKillSystemBridge(); } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index 2ceb22294c..96068dffaa 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -40,6 +40,10 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override { return _impl->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); } + + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override { + return _impl->onEmergencyKillSystemBridge(); + } protected: private: std::shared_ptr _impl; diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index 6bab1bbf8b..3213b50062 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -21,6 +21,8 @@ class BpEvdevCallback : public ::ndk::BpCInterface { ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; + + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override; }; } // namespace sysbridge } // namespace keymapper diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index 9cfa20e678..3602375324 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -66,6 +66,16 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_out, _aidl_return); if (_aidl_ret_status != STATUS_OK) break; + break; + } + case (FIRST_CALL_TRANSACTION + 2 /*onEmergencyKillSystemBridge*/): { + + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEmergencyKillSystemBridge(); + _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); + if (_aidl_ret_status != STATUS_OK) break; + + if (!AStatus_isOk(_aidl_status.get())) break; + break; } } @@ -160,6 +170,41 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(const std::string& in_deviceP _aidl_ret_status = ::ndk::AParcel_readData(_aidl_out.get(), _aidl_return); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; +} + + ::ndk::ScopedAStatus BpEvdevCallback::onEmergencyKillSystemBridge() { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 2 /*onEmergencyKillSystemBridge*/), + _aidl_in.getR(), + _aidl_out.getR(), + 0 +#ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL +#endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEmergencyKillSystemBridge(); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; _aidl_error: _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); _aidl_status_return: @@ -234,6 +279,12 @@ ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(const std::string& /*in _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; } + + ::ndk::ScopedAStatus IEvdevCallbackDefault::onEmergencyKillSystemBridge() { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; +} ::ndk::SpAIBinder IEvdevCallbackDefault::asBinder() { return ::ndk::SpAIBinder(); } diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index 007e2afbd2..1630128b68 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -31,6 +31,7 @@ class IEvdevCallback : public ::ndk::ICInterface { static constexpr uint32_t TRANSACTION_onEvdevEventLoopStarted = FIRST_CALL_TRANSACTION + 0; static constexpr uint32_t TRANSACTION_onEvdevEvent = FIRST_CALL_TRANSACTION + 1; + static constexpr uint32_t TRANSACTION_onEmergencyKillSystemBridge = FIRST_CALL_TRANSACTION + 2; static std::shared_ptr fromBinder(const ::ndk::SpAIBinder& binder); static binder_status_t writeToParcel(AParcel* parcel, const std::shared_ptr& instance); @@ -39,6 +40,8 @@ class IEvdevCallback : public ::ndk::ICInterface { static const std::shared_ptr& getDefaultImpl(); virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; virtual ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) = 0; + + virtual ::ndk::ScopedAStatus onEmergencyKillSystemBridge() = 0; private: static std::shared_ptr default_impl; }; @@ -46,6 +49,8 @@ class IEvdevCallbackDefault : public IEvdevCallback { public: ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; + + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; }; diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp index 617dd2cea1..e51847aa9d 100644 --- a/sysbridge/src/main/cpp/libevdev_jni.cpp +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -198,6 +198,8 @@ Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNa return result; } +struct timeval powerButtonDownTime = {0, 0}; + /** * @return Whether the events were all handled by the callback. If the callback dies then this * returns false. @@ -218,9 +220,24 @@ bool onEpollEvdevEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { if (rc == LIBEVDEV_READ_STATUS_SUCCESS) { // rc == 0 int32_t outKeycode = -1; uint32_t outFlags = -1; + deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); - // TODO if power button (matching scancode OR key code) is pressed for more than 10 seconds, stop the systembridge process. Call kill from here + // 26 = KEYCODE_POWER + if (inputEvent.code == KEY_POWER || outKeycode == 26) { + if (inputEvent.value == 1) { + // Down click + powerButtonDownTime = inputEvent.time; + } else if (inputEvent.value == 0) { + // Up click + + // If held down for 10 seconds or more, kill system bridge. + if (inputEvent.time.tv_sec - powerButtonDownTime.tv_sec >= 10) { + callback->onEmergencyKillSystemBridge(); + exit(0); + } + } + } bool returnValue; ndk::ScopedAStatus callbackResult = callback->onEvdevEvent(deviceContext->devicePath, From 7e7ce92935b3e9afeeb4a3b007287f3a6b1fcc56 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 00:21:06 +0200 Subject: [PATCH 193/215] undo style reformatting --- .../keymapper/sysbridge/BnEvdevCallback.h | 7 +- .../keymapper/sysbridge/BpEvdevCallback.h | 3 +- .../keymapper/sysbridge/IEvdevCallback.cpp | 86 +++++++++---------- .../keymapper/sysbridge/IEvdevCallback.h | 8 +- 4 files changed, 49 insertions(+), 55 deletions(-) diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h index 96068dffaa..d07bb0aae2 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -40,10 +40,9 @@ class IEvdevCallbackDelegator : public BnEvdevCallback { ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override { return _impl->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); } - - ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override { - return _impl->onEmergencyKillSystemBridge(); - } + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override { + return _impl->onEmergencyKillSystemBridge(); + } protected: private: std::shared_ptr _impl; diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h index 3213b50062..33c5c4a1ff 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -21,8 +21,7 @@ class BpEvdevCallback : public ::ndk::BpCInterface { ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; - - ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override; + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override; }; } // namespace sysbridge } // namespace keymapper diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp index 3602375324..65943c8ecf 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -66,15 +66,15 @@ static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_out, _aidl_return); if (_aidl_ret_status != STATUS_OK) break; - break; + break; } - case (FIRST_CALL_TRANSACTION + 2 /*onEmergencyKillSystemBridge*/): { + case (FIRST_CALL_TRANSACTION + 2 /*onEmergencyKillSystemBridge*/): { - ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEmergencyKillSystemBridge(); - _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); - if (_aidl_ret_status != STATUS_OK) break; + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEmergencyKillSystemBridge(); + _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); + if (_aidl_ret_status != STATUS_OK) break; - if (!AStatus_isOk(_aidl_status.get())) break; + if (!AStatus_isOk(_aidl_status.get())) break; break; } @@ -170,41 +170,40 @@ ::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(const std::string& in_deviceP _aidl_ret_status = ::ndk::AParcel_readData(_aidl_out.get(), _aidl_return); if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - _aidl_error: - _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); - _aidl_status_return: - return _aidl_status; + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; } +::ndk::ScopedAStatus BpEvdevCallback::onEmergencyKillSystemBridge() { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - ::ndk::ScopedAStatus BpEvdevCallback::onEmergencyKillSystemBridge() { - binder_status_t _aidl_ret_status = STATUS_OK; - ::ndk::ScopedAStatus _aidl_status; - ::ndk::ScopedAParcel _aidl_in; - ::ndk::ScopedAParcel _aidl_out; - - _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = AIBinder_transact( - asBinder().get(), - (FIRST_CALL_TRANSACTION + 2 /*onEmergencyKillSystemBridge*/), - _aidl_in.getR(), - _aidl_out.getR(), - 0 -#ifdef BINDER_STABILITY_SUPPORT - | FLAG_PRIVATE_LOCAL -#endif // BINDER_STABILITY_SUPPORT - ); - if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { - _aidl_status = IEvdevCallback::getDefaultImpl()->onEmergencyKillSystemBridge(); - goto _aidl_status_return; - } - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); - if (_aidl_ret_status != STATUS_OK) goto _aidl_error; - - if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 2 /*onEmergencyKillSystemBridge*/), + _aidl_in.getR(), + _aidl_out.getR(), + 0 + #ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL + #endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEmergencyKillSystemBridge(); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; _aidl_error: _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); _aidl_status_return: @@ -279,11 +278,10 @@ ::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(const std::string& /*in _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); return _aidl_status; } - - ::ndk::ScopedAStatus IEvdevCallbackDefault::onEmergencyKillSystemBridge() { - ::ndk::ScopedAStatus _aidl_status; - _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); - return _aidl_status; +::ndk::ScopedAStatus IEvdevCallbackDefault::onEmergencyKillSystemBridge() { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; } ::ndk::SpAIBinder IEvdevCallbackDefault::asBinder() { return ::ndk::SpAIBinder(); diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h index 1630128b68..b81eafec61 100644 --- a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -31,7 +31,7 @@ class IEvdevCallback : public ::ndk::ICInterface { static constexpr uint32_t TRANSACTION_onEvdevEventLoopStarted = FIRST_CALL_TRANSACTION + 0; static constexpr uint32_t TRANSACTION_onEvdevEvent = FIRST_CALL_TRANSACTION + 1; - static constexpr uint32_t TRANSACTION_onEmergencyKillSystemBridge = FIRST_CALL_TRANSACTION + 2; + static constexpr uint32_t TRANSACTION_onEmergencyKillSystemBridge = FIRST_CALL_TRANSACTION + 2; static std::shared_ptr fromBinder(const ::ndk::SpAIBinder& binder); static binder_status_t writeToParcel(AParcel* parcel, const std::shared_ptr& instance); @@ -40,8 +40,7 @@ class IEvdevCallback : public ::ndk::ICInterface { static const std::shared_ptr& getDefaultImpl(); virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; virtual ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) = 0; - - virtual ::ndk::ScopedAStatus onEmergencyKillSystemBridge() = 0; + virtual ::ndk::ScopedAStatus onEmergencyKillSystemBridge() = 0; private: static std::shared_ptr default_impl; }; @@ -49,8 +48,7 @@ class IEvdevCallbackDefault : public IEvdevCallback { public: ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; - - ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override; + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override; ::ndk::SpAIBinder asBinder() override; bool isRemote() override; }; From 35abae3d8ae852a49ee70540f649e94b80c474b9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 00:29:01 +0200 Subject: [PATCH 194/215] #1394 add option to turn off auto start at boot and show emergency tip --- .../keymapper/base/promode/ProModeScreen.kt | 138 ++++++++++++++++-- .../base/promode/ProModeViewModel.kt | 8 + .../base/promode/SystemBridgeSetupUseCase.kt | 13 ++ base/src/main/res/values/strings.xml | 6 + .../io/github/sds100/keymapper/data/Keys.kt | 8 +- 5 files changed, 156 insertions(+), 17 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index c18ee9fa8e..6fe2dbca52 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -29,8 +29,11 @@ import androidx.compose.material.icons.automirrored.rounded.HelpOutline import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Checklist import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Notifications import androidx.compose.material.icons.rounded.Numbers +import androidx.compose.material.icons.rounded.RestartAlt +import androidx.compose.material.icons.rounded.Tune import androidx.compose.material.icons.rounded.WarningAmber import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ButtonDefaults @@ -62,6 +65,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose import io.github.sds100.keymapper.base.utils.ui.compose.icons.FakeShizuku import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcon import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons @@ -74,6 +78,7 @@ fun ProModeScreen( ) { val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle() + val autoStartBootEnabled by viewModel.autoStartBootEnabled.collectAsStateWithLifecycle() ProModeScreen( modifier = modifier, @@ -92,6 +97,8 @@ fun ProModeScreen( onRootButtonClick = viewModel::onRootButtonClick, onSetupWithKeyMapperClick = viewModel::onSetupWithKeyMapperClick, onRequestNotificationPermissionClick = viewModel::onRequestNotificationPermissionClick, + autoStartAtBoot = autoStartBootEnabled, + onAutoStartAtBootToggled = { viewModel.onAutoStartBootToggled() }, ) } } @@ -169,6 +176,8 @@ private fun Content( onRootButtonClick: () -> Unit = {}, onSetupWithKeyMapperClick: () -> Unit = {}, onRequestNotificationPermissionClick: () -> Unit = {}, + autoStartAtBoot: Boolean, + onAutoStartAtBootToggled: (Boolean) -> Unit = {}, ) { Column(modifier = modifier.verticalScroll(rememberScrollState())) { AnimatedVisibility( @@ -213,6 +222,8 @@ private fun Content( onRootButtonClick = onRootButtonClick, onSetupWithKeyMapperClick = onSetupWithKeyMapperClick, onRequestNotificationPermissionClick = onRequestNotificationPermissionClick, + autoStartAtBoot = autoStartAtBoot, + onAutoStartAtBootToggled = onAutoStartAtBootToggled, ) } } @@ -238,7 +249,9 @@ private fun SetupSection( onShizukuButtonClick: () -> Unit, onStopServiceClick: () -> Unit, onSetupWithKeyMapperClick: () -> Unit, - onRequestNotificationPermissionClick: () -> Unit = {} + onRequestNotificationPermissionClick: () -> Unit = {}, + autoStartAtBoot: Boolean, + onAutoStartAtBootToggled: (Boolean) -> Unit = {} ) { Column(modifier) { OptionsHeaderRow( @@ -277,12 +290,22 @@ private fun SetupSection( } when (state) { - ProModeState.Started -> ProModeStartedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - onStopClick = onStopServiceClick - ) + ProModeState.Started -> { + EmergencyTipCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + ProModeStartedCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + onStopClick = onStopServiceClick + ) + } is ProModeState.Stopped -> { if (state.isRootGranted) { @@ -372,6 +395,26 @@ private fun SetupSection( ) } } + + // Options section + Spacer(modifier = Modifier.height(16.dp)) + + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.Tune, + text = stringResource(R.string.pro_mode_options_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + modifier = Modifier.padding(horizontal = 8.dp), + title = stringResource(R.string.title_pref_pro_mode_auto_start_at_boot), + text = stringResource(R.string.summary_pref_pro_mode_auto_start_at_boot), + icon = Icons.Rounded.RestartAlt, + isChecked = autoStartAtBoot, + onCheckedChange = onAutoStartAtBootToggled + ) } } @@ -381,9 +424,15 @@ private fun WarningCard( state: ProModeWarningState, onButtonClick: () -> Unit = {}, ) { + val borderStroke = if (state is ProModeWarningState.Understood) { + CardDefaults.outlinedCardBorder() + } else { + BorderStroke(1.dp, MaterialTheme.colorScheme.error) + } + OutlinedCard( modifier = modifier, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), + border = borderStroke, elevation = CardDefaults.elevatedCardElevation() ) { Spacer(modifier = Modifier.height(16.dp)) @@ -543,6 +592,44 @@ private fun SetupCard( } } +@Composable +private fun EmergencyTipCard( + modifier: Modifier = Modifier +) { + OutlinedCard( + modifier = modifier, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary), + elevation = CardDefaults.elevatedCardElevation() + ) { + Spacer(modifier = Modifier.height(16.dp)) + Row(modifier = Modifier.padding(horizontal = 16.dp)) { + Icon( + imageVector = Icons.Rounded.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(R.string.pro_mode_emergency_tip_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.pro_mode_emergency_tip_text), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(16.dp)) + } +} + @Composable private fun ProModeInfoCard( modifier: Modifier = Modifier, @@ -615,7 +702,9 @@ private fun Preview() { ) ), showInfoCard = true, - onInfoCardDismiss = {} + onInfoCardDismiss = {}, + autoStartAtBoot = false, + onAutoStartAtBootToggled = {} ) } } @@ -630,7 +719,9 @@ private fun PreviewDark() { warningState = ProModeWarningState.Understood, setupState = State.Data(ProModeState.Started), showInfoCard = false, - onInfoCardDismiss = {} + onInfoCardDismiss = {}, + autoStartAtBoot = true, + onAutoStartAtBootToggled = {} ) } } @@ -647,13 +738,32 @@ private fun PreviewCountingDown() { ), setupState = State.Loading, showInfoCard = true, - onInfoCardDismiss = {} + onInfoCardDismiss = {}, + autoStartAtBoot = false, + onAutoStartAtBootToggled = {} + ) + } + } +} + +@Preview +@Composable +private fun PreviewStarted() { + KeyMapperTheme { + ProModeScreen { + Content( + warningState = ProModeWarningState.Understood, + setupState = State.Data(ProModeState.Started), + showInfoCard = false, + onInfoCardDismiss = {}, + autoStartAtBoot = false, + onAutoStartAtBootToggled = {} ) } } } -@Preview(name = "Notification Permission Not Granted") +@Preview @Composable private fun PreviewNotificationPermissionNotGranted() { KeyMapperTheme { @@ -668,7 +778,9 @@ private fun PreviewNotificationPermissionNotGranted() { ) ), showInfoCard = false, - onInfoCardDismiss = {} + onInfoCardDismiss = {}, + autoStartAtBoot = false, + onAutoStartAtBootToggled = {} ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index 38be14caf1..72dfb38980 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -62,6 +62,10 @@ class ProModeViewModel @Inject constructor( ::buildSetupState ).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + val autoStartBootEnabled: StateFlow = + useCase.isAutoStartBootEnabled + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + var showInfoCard by mutableStateOf(!useCase.isInfoDismissed()) private set @@ -141,6 +145,10 @@ class ProModeViewModel @Inject constructor( useCase.requestNotificationPermission() } + fun onAutoStartBootToggled() { + useCase.toggleAutoStartBoot() + } + private fun buildSetupState( isSystemBridgeConnected: Boolean, isRootGranted: Boolean, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index a669695893..16ddd87581 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -186,6 +186,16 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( preferences.set(Keys.isProModeInfoDismissed, true) } + override val isAutoStartBootEnabled: Flow = + preferences.get(Keys.isProModeAutoStartBootEnabled) + .map { it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT } + + override fun toggleAutoStartBoot() { + preferences.update(Keys.isProModeAutoStartBootEnabled) { + !(it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT) + } + } + @RequiresApi(Build.VERSION_CODES.R) private fun getNextStep( accessibilityServiceState: AccessibilityServiceState, @@ -214,6 +224,9 @@ interface SystemBridgeSetupUseCase { fun isInfoDismissed(): Boolean fun dismissInfo() + val isAutoStartBootEnabled: Flow + fun toggleAutoStartBoot() + val isSetupAssistantEnabled: Flow fun toggleSetupAssistant() diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index b354985db3..4ba39347ac 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1590,6 +1590,12 @@ PRO mode service is running Stop + Automatically start at boot + PRO Mode will start itself whenever you turn on or restart your device + + Emergency tip + If your buttons stop working, hold down the power button for 10 seconds and release to disable PRO Mode. + Setup wizard Step %d of %d Use interactive setup assistant diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index c7f4c7c307..0ee6383581 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -9,13 +9,13 @@ 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 hasRootPermission = booleanPreferencesKey("pref_allow_root_features") val shownAppIntro = booleanPreferencesKey("pref_first_time") - val showToggleKeyMapsNotification = booleanPreferencesKey("pref_show_remappings_notification") - val showToggleKeyboardNotification = - booleanPreferencesKey("pref_toggle_key_mapper_keyboard_notification") +// val showToggleKeyMapsNotification = booleanPreferencesKey("pref_show_remappings_notification") +// val showToggleKeyboardNotification = +// booleanPreferencesKey("pref_toggle_key_mapper_keyboard_notification") val devicesThatChangeIme = stringSetPreferencesKey("pref_devices_that_change_ime") val changeImeOnDeviceConnect = From b85a767a99779acbf5e46c05039af7f8e0b5503c Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 00:30:38 +0200 Subject: [PATCH 195/215] #1394 remove unused PRO mode switch in trigger screen --- .../base/trigger/RecordTriggerButtonRow.kt | 68 +++++-------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt index e84bf2cdb5..92c28666d4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt @@ -18,12 +18,10 @@ 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.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface -import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -41,9 +39,6 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperTapTarget -import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons -import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon -import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIconDisabled import io.github.sds100.keymapper.base.utils.ui.compose.keyMapperShowcaseStyle @Composable @@ -57,17 +52,8 @@ fun RecordTriggerButtonRow( onSkipTapTarget: () -> Unit = {}, showAdvancedTriggerTapTarget: Boolean = false, onAdvancedTriggerTapTargetCompleted: () -> Unit = {}, - isProModeSelected: Boolean = false // TODO ) { Column { -// Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { -// Text( -// text = stringResource(R.string.trigger_record_with_pro_mode), -// overflow = TextOverflow.Ellipsis -// ) -// Spacer(modifier = Modifier.width(16.dp)) -// -// } Row(modifier, verticalAlignment = Alignment.CenterVertically) { IntroShowcase( showIntroShowCase = showRecordTriggerTapTarget, @@ -90,39 +76,24 @@ fun RecordTriggerButtonRow( Spacer(modifier = Modifier.width(8.dp)) - Switch( - checked = isProModeSelected, - onCheckedChange = {}, - enabled = recordTriggerState !is RecordTriggerState.CountingDown, - thumbContent = { - if (isProModeSelected) { - Icon(imageVector = KeyMapperIcons.ProModeIcon, contentDescription = null) - } else { - Icon( - imageVector = KeyMapperIcons.ProModeIconDisabled, - contentDescription = null - ) - - } - }) -// 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, -// ) -// } + 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, + ) + } } } } @@ -229,7 +200,6 @@ private fun PreviewCountingDown() { RecordTriggerButtonRow( modifier = Modifier.fillMaxWidth(), recordTriggerState = RecordTriggerState.CountingDown(3), - isProModeSelected = true ) } } @@ -243,7 +213,6 @@ private fun PreviewStopped() { RecordTriggerButtonRow( modifier = Modifier.fillMaxWidth(), recordTriggerState = RecordTriggerState.Idle, - isProModeSelected = false ) } } @@ -257,7 +226,6 @@ private fun PreviewStoppedCompact() { RecordTriggerButtonRow( modifier = Modifier.fillMaxWidth(), recordTriggerState = RecordTriggerState.Idle, - isProModeSelected = true ) } } From b96c2fd0712b2e8f8a9febff44ce2f5b4c8f91c8 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 22:01:12 +0200 Subject: [PATCH 196/215] #1394 automatically restart the system bridge if it has a different version code --- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 6 +- .../base/promode/SystemBridgeAutoStarter.kt | 8 +-- .../keymapper/sysbridge/ISystemBridge.aidl | 4 +- sysbridge/src/main/cpp/starter.cpp | 17 +++-- .../manager/SystemBridgeConnectionManager.kt | 64 +++++++++++++++---- .../provider/SystemBridgeBinderProvider.kt | 2 - .../sysbridge/service/SystemBridge.kt | 54 +++++++++++----- .../sysbridge/starter/SystemBridgeStarter.kt | 6 +- sysbridge/src/main/res/raw/start.sh | 4 +- 9 files changed, 118 insertions(+), 47 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index b19d08b070..fe649afd41 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -24,6 +24,7 @@ import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl import io.github.sds100.keymapper.system.apps.AndroidPackageManagerAdapter import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl @@ -87,6 +88,9 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { @Inject lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter + @Inject + lateinit var systemBridgeConnectionManager: SystemBridgeConnectionManagerImpl + private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() } private val userManager: UserManager? by lazy { getSystemService() } @@ -142,8 +146,6 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { private fun init() { Log.i(tag, "KeyMapperApp: Init") - // TODO if autostart for PRO mode is turned on then start it here from boot. - settingsRepository.get(Keys.darkTheme) .map { it?.toIntOrNull() } .map { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt index dcc4d8d9be..5ca2f7d3c7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt @@ -127,10 +127,11 @@ class SystemBridgeAutoStarter @Inject constructor( .first() // Wait 5 seconds for the system bridge to potentially connect itself to Key Mapper - // before starting it + // before starting it. val isConnected = withTimeoutOrNull(5000L) { - connectionManager.connectionState.value is SystemBridgeConnectionState.Connected + connectionManager.connectionState.first { it is SystemBridgeConnectionState.Connected } + true } ?: false if (isBootAutoStartEnabled && !isConnected) { @@ -157,8 +158,6 @@ class SystemBridgeAutoStarter @Inject constructor( return } - showAutoStartNotification(getString(R.string.system_bridge_died_notification_restarting_text)) - lastAutoStartTime = SystemClock.elapsedRealtime() when (type) { @@ -222,6 +221,7 @@ class SystemBridgeAutoStarter @Inject constructor( icon = R.drawable.pro_mode, priority = NotificationCompat.PRIORITY_MAX, onGoing = true, + showIndeterminateProgress = true, showOnLockscreen = false ) diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 500599748c..8829018878 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -6,6 +6,9 @@ import android.view.InputEvent; interface ISystemBridge { void destroy() = 16777114; + int getProcessUid() = 16777113; + int getVersionCode() = 16777112; + String executeCommand(String command) = 16777111; boolean grabEvdevDevice(String devicePath) = 1; boolean grabEvdevDeviceArray(in String[] devicePath) = 2; @@ -24,5 +27,4 @@ interface ISystemBridge { boolean setWifiEnabled(boolean enable) = 10; void grantPermission(String permission, int deviceId) = 12; - int getProcessUid() = 13; } \ No newline at end of file diff --git a/sysbridge/src/main/cpp/starter.cpp b/sysbridge/src/main/cpp/starter.cpp index 7bbad5f747..5b5b0c4411 100644 --- a/sysbridge/src/main/cpp/starter.cpp +++ b/sysbridge/src/main/cpp/starter.cpp @@ -44,7 +44,8 @@ #endif static void run_server(const char *apk_path, const char *lib_path, const char *main_class, - const char *process_name, const char *package_name) { + const char *process_name, const char *package_name, + const char *version_code) { if (setenv("CLASSPATH", apk_path, true)) { LOGE("can't set CLASSPATH\n"); exit(EXIT_FATAL_SET_CLASSPATH); @@ -96,6 +97,7 @@ v_current = (uintptr_t) v + v_size - sizeof(char *); \ ARG_PUSH_FMT(argv, "-Djava.class.path=%s", apk_path) ARG_PUSH_FMT(argv, "-Dkeymapper_sysbridge.library.path=%s", lib_path) ARG_PUSH_FMT(argv, "-Dkeymapper_sysbridge.package=%s", package_name) + ARG_PUSH_FMT(argv, "-Dkeymapper_sysbridge.version_code=%s", version_code) ARG_PUSH_DEBUG_VM_PARAMS(argv) ARG_PUSH(argv, "/system/bin") ARG_PUSH_FMT(argv, "--nice-name=%s", process_name) @@ -111,11 +113,12 @@ v_current = (uintptr_t) v + v_size - sizeof(char *); \ } static void start_server(const char *apk_path, const char *lib_path, const char *main_class, - const char *process_name, const char *package_name) { + const char *process_name, const char *package_name, + const char *version_code) { if (daemon(false, false) == 0) { LOGD("child"); - run_server(apk_path, lib_path, main_class, process_name, package_name); + run_server(apk_path, lib_path, main_class, process_name, package_name, version_code); } else { perrorf("fatal: can't fork\n"); exit(EXIT_FATAL_FORK); @@ -171,6 +174,7 @@ int starter_main(int argc, char *argv[]) { char *apk_path = nullptr; char *lib_path = nullptr; char *package_name = nullptr; + char *version_code = 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. @@ -181,12 +185,15 @@ int starter_main(int argc, char *argv[]) { lib_path = argv[i] + 6; } else if (strncmp(argv[i], "--package=", 10) == 0) { package_name = argv[i] + 10; + } else if (strncmp(argv[i], "--version_code=", 15) == 0) { + version_code = argv[i] + 15; } } printf("info: apk path = %s\n", apk_path); printf("info: lib path = %s\n", lib_path); printf("info: package name = %s\n", package_name); + printf("info: version code = %s\n", version_code); int uid = getuid(); if (uid != 0 && uid != 2000) { @@ -199,7 +206,7 @@ int starter_main(int argc, char *argv[]) { if (uid == 0) { chown("/data/local/tmp/keymapper_sysbridge_starter", 2000, 2000); se::setfilecon("/data/local/tmp/keymapper_sysbridge_starter", - "u:object_r:shell_data_file:s0"); + "u:object_r:shell_data_file:s0"); switch_cgroup(); int sdkLevel = 0; @@ -292,7 +299,7 @@ int starter_main(int argc, char *argv[]) { printf("info: starting server...\n"); fflush(stdout); LOGD("start_server"); - start_server(apk_path, lib_path, SERVER_CLASS_PATH, SERVER_NAME, package_name); + start_server(apk_path, lib_path, SERVER_CLASS_PATH, SERVER_NAME, package_name, version_code); exit(EXIT_SUCCESS); } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 30881c9d7e..656df5855b 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -5,22 +5,29 @@ import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build +import android.os.DeadObjectException import android.os.IBinder import android.os.IBinder.DeathRecipient import android.os.Process import android.os.RemoteException import android.os.SystemClock +import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.onFailure +import io.github.sds100.keymapper.common.utils.success import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.ktx.TAG import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -38,6 +45,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, private val starter: SystemBridgeStarter, + private val buildConfigProvider: BuildConfigProvider ) : SystemBridgeConnectionManager { private val systemBridgeLock: Any = Any() @@ -79,24 +87,58 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( /** * This is called by the SystemBridgeBinderProvider content provider. */ + @SuppressLint("LogNotTimber") fun onBinderReceived(binder: IBinder) { val systemBridge = ISystemBridge.Stub.asInterface(binder) synchronized(systemBridgeLock) { - systemBridge.asBinder().linkToDeath(deathRecipient, 0) - this.systemBridgeFlow.update { systemBridge } - - connectionState.update { - SystemBridgeConnectionState.Connected( - time = SystemClock.elapsedRealtime(), - ) + if (systemBridge.versionCode == buildConfigProvider.versionCode) { + // Only link to death if it is the same version code so restarting it + // doesn't send a death message + systemBridge.asBinder().linkToDeath(deathRecipient, 0) + + this.systemBridgeFlow.update { systemBridge } + + // Only turn on the ADB options to prevent killing if it is running under + // the ADB shell user + if (systemBridge.processUid == Process.SHELL_UID) { + preventSystemBridgeKilling(systemBridge) + } + + connectionState.update { + SystemBridgeConnectionState.Connected( + time = SystemClock.elapsedRealtime(), + ) + } + } else { + coroutineScope.launch(Dispatchers.IO) { + // Can not use Timber because the content provider is called before the application's + // onCreate where the Timber Tree is installed. The content provider then + // calls this message. + Log.w( + TAG, + "System Bridge version mismatch! Restarting it. App: ${buildConfigProvider.versionCode}, System Bridge: ${systemBridge.versionCode}" + ) + + restartSystemBridge(systemBridge) + } } + } + } - // Only turn on the ADB options to prevent killing if it is running under - // the ADB shell user - if (systemBridge.processUid == Process.SHELL_UID) { - preventSystemBridgeKilling(systemBridge) + @SuppressLint("LogNotTimber") + private suspend fun restartSystemBridge(systemBridge: ISystemBridge) { + starter.startSystemBridge(executeCommand = { command -> + try { + systemBridge.executeCommand(command)!!.success() + } catch (_: DeadObjectException) { + // This exception is expected since it is killing the system bridge + Success("") + } catch (e: Exception) { + KMError.Exception(e) } + }).onFailure { error -> + Log.e(TAG, "Failed to restart System Bridge: $error") } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt index 5183ab3640..05fbad5610 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt @@ -11,7 +11,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl -import timber.log.Timber /** * Taken from the ShizukuProvider class. @@ -62,7 +61,6 @@ internal class SystemBridgeBinderProvider : ContentProvider() { private fun handleSendBinder(extras: Bundle) { if (systemBridgeManager.pingBinder()) { - Timber.d("sendBinder is called when there is already a Binder from the system bridge.") return } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index e6d32e241d..5b6c7b9d0d 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -60,6 +60,10 @@ internal class SystemBridge : ISystemBridge.Stub() { private const val TAG: String = "KeyMapperSystemBridge" private val systemBridgePackageName: String? = System.getProperty("keymapper_sysbridge.package") + + private val systemBridgeVersionCode: Int = + System.getProperty("keymapper_sysbridge.version_code")!!.toInt() + private const val SHELL_PACKAGE = "com.android.shell" private const val KEYMAPPER_CHECK_INTERVAL_MS = 60 * 1000L // 1 minute @@ -86,11 +90,17 @@ internal class SystemBridge : ISystemBridge.Stub() { private val processObserver = object : ProcessObserverAdapter() { // This is used as a proxy for detecting the Key Mapper process has started. + // It is called when ANY foreground activities check so don't execute anything + // long running. override fun onForegroundActivitiesChanged( pid: Int, uid: Int, foregroundActivities: Boolean ) { + if (evdevCallback?.asBinder()?.pingBinder() != true) { + evdevCallbackDeathRecipient.binderDied() + } + // Do not send the binder if the app is not in the foreground. if (!foregroundActivities) { return @@ -101,7 +111,7 @@ internal class SystemBridge : ISystemBridge.Stub() { destroy() } else { synchronized(sendBinderLock) { - if (!isBinderSent) { + if (evdevCallback == null) { Log.i(TAG, "Key Mapper process started, send binder to app") mainHandler.post { sendBinderToApp() @@ -113,7 +123,6 @@ internal class SystemBridge : ISystemBridge.Stub() { } private val sendBinderLock: Any = Any() - private var isBinderSent: Boolean = false private val coroutineScope: CoroutineScope = MainScope() private val mainHandler = Handler(Looper.myLooper()!!) @@ -125,10 +134,7 @@ internal class SystemBridge : ISystemBridge.Stub() { private var evdevCallback: IEvdevCallback? = null private val evdevCallbackDeathRecipient: IBinder.DeathRecipient = IBinder.DeathRecipient { Log.i(TAG, "EvdevCallback binder died") - - synchronized(sendBinderLock) { - isBinderSent = false - } + evdevCallback = null coroutineScope.launch(Dispatchers.Default) { stopEvdevEventLoop() @@ -147,7 +153,7 @@ internal class SystemBridge : ISystemBridge.Stub() { @SuppressLint("UnsafeDynamicallyLoadedCode") System.load("$libraryPath/libevdev.so") - Log.i(TAG, "SystemBridge started") + Log.i(TAG, "SystemBridge started. Version code $versionCode") waitSystemService("package") waitSystemService(Context.ACTIVITY_SERVICE) @@ -202,12 +208,6 @@ internal class SystemBridge : ISystemBridge.Stub() { } } } finally { - // Clear the job reference when the coroutine completes - synchronized(keyMapperCheckLock) { - if (keyMapperCheckJob?.isCompleted == true) { - keyMapperCheckJob = null - } - } } } } @@ -224,9 +224,6 @@ internal class SystemBridge : ISystemBridge.Stub() { override fun destroy() { Log.i(TAG, "SystemBridge destroyed") - // Clean up periodic check job - stopKeyMapperPeriodicCheck() - // Must be last line in this method because it halts the JVM. exitProcess(0) } @@ -391,7 +388,6 @@ internal class SystemBridge : ISystemBridge.Stub() { ) if (reply != null) { Log.i(TAG, "Send binder to user app $systemBridgePackageName in user $userId") - isBinderSent = true // Stop periodic check since connection is successful stopKeyMapperPeriodicCheck() return true @@ -419,4 +415,28 @@ internal class SystemBridge : ISystemBridge.Stub() { return false } + + override fun executeCommand(command: String?): String { + command ?: throw IllegalArgumentException("command is null") + + Log.i(TAG, "Executing command: $command") + + val process = Runtime.getRuntime().exec(command) + + val out = with(process.inputStream.bufferedReader()) { + readText() + } + + val err = with(process.errorStream.bufferedReader()) { + readText() + } + + process.waitFor() + + return "$out\n$err" + } + + override fun getVersionCode(): Int { + return systemBridgeVersionCode + } } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index fd7d596bde..4d7abbcda7 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -158,7 +158,7 @@ class SystemBridgeStarter @Inject constructor( }) } - private suspend fun startSystemBridge(executeCommand: suspend (String) -> KMResult): KMResult { + suspend fun startSystemBridge(executeCommand: suspend (String) -> KMResult): KMResult { startMutex.withLock { val externalFilesParent = try { ctx.getExternalFilesDir(null)?.parentFile @@ -179,7 +179,7 @@ class SystemBridgeStarter @Inject constructor( } val startCommand = - "sh ${outputStarterScript.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName" + "sh ${outputStarterScript.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName --version_code=${buildConfigProvider.versionCode}" return executeCommand(startCommand).then { output -> @@ -223,7 +223,7 @@ class SystemBridgeStarter @Inject constructor( } val startCommand = - "sh ${outputStarterScript.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName" + "sh ${outputStarterScript.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName --version_code=${buildConfigProvider.versionCode}" // Make starter binary executable try { diff --git a/sysbridge/src/main/res/raw/start.sh b/sysbridge/src/main/res/raw/start.sh index 0f70844b0a..49cc606183 100644 --- a/sysbridge/src/main/res/raw/start.sh +++ b/sysbridge/src/main/res/raw/start.sh @@ -39,8 +39,8 @@ fi if [ -f $STARTER_PATH ]; then echo "info: exec $STARTER_PATH" - # Pass apk path, library path, and package name - $STARTER_PATH "$1" "$2" "$3" + # Pass apk path, library path, package name, version code + $STARTER_PATH "$1" "$2" "$3" "$4" result=$? if [ ${result} -ne 0 ]; then echo "info: keymapper_sysbridge_starter exit with non-zero value $result" From 5a253ee01175c52bd5a89f7d6b3a627ce6696d41 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 22:20:03 +0200 Subject: [PATCH 197/215] #1394 random fixes in ADB Mdns --- .../SystemBridgeSetupAssistantController.kt | 1 + .../keymapper/sysbridge/adb/AdbClient.kt | 3 + .../sds100/keymapper/sysbridge/adb/AdbMdns.kt | 58 ++++++++++++------- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index 420840e94d..09b66a1956 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -288,6 +288,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( showOnLockscreen = false, autoCancel = true, onClickAction = onClickAction, + bigTextStyle = true, // Must not be silent so it is shown as a heads up notification silent = false, actions = actions diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt index 99f0e49737..0c0fc936a6 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt @@ -21,6 +21,7 @@ import java.io.DataInputStream import java.io.DataOutputStream import java.net.ConnectException import java.net.Socket +import java.net.SocketException import java.nio.ByteBuffer import java.nio.ByteOrder import javax.net.ssl.SSLProtocolException @@ -102,6 +103,8 @@ internal class AdbClient(private val host: String, private val port: Int, privat } catch (e: SSLProtocolException) { // This can be thrown if the encryption keys mismatch return AdbError.SslHandshakeError + } catch (e: SocketException) { + return AdbError.ConnectionError } return Success(Unit) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt index 6c1979a54e..5a6b46c5f3 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt @@ -9,6 +9,7 @@ import androidx.annotation.RequiresApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update @@ -47,12 +48,24 @@ internal class AdbMdns( private val resolveListener: NsdManager.ResolveListener = object : NsdManager.ResolveListener { override fun onResolveFailed(nsdServiceInfo: NsdServiceInfo, i: Int) { - serviceResolvedChannel.trySend(null) + serviceResolvedChannel.trySendBlocking(null) } override fun onServiceResolved(nsdServiceInfo: NsdServiceInfo) { Timber.d("onServiceResolved: ${nsdServiceInfo.serviceName} ${nsdServiceInfo.host} ${nsdServiceInfo.port} ${nsdServiceInfo.serviceType}") - serviceResolvedChannel.trySend(nsdServiceInfo) + serviceResolvedChannel.trySendBlocking(nsdServiceInfo) + } + + override fun onResolutionStopped(serviceInfo: NsdServiceInfo) { + super.onResolutionStopped(serviceInfo) + + serviceResolvedChannel.trySendBlocking(null) + } + + override fun onStopResolutionFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + super.onStopResolutionFailed(serviceInfo, errorCode) + + serviceResolvedChannel.trySendBlocking(null) } } @@ -112,9 +125,7 @@ internal class AdbMdns( var port: Int? = null if (isDiscovering.value) { - runCatching { - nsdManager.stopServiceDiscovery(discoveryListener) - } + cleanup() } // Wait for it to stop discovering @@ -156,28 +167,33 @@ internal class AdbMdns( } catch (e: Exception) { Timber.e(e, "Failed to discover ADB port") } finally { - runCatching { - if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.TIRAMISU) >= 7) { - nsdManager.stopServiceResolution(resolveListener) - } - } + cleanup() + } - runCatching { - nsdManager.stopServiceDiscovery(discoveryListener) - } + return port + } - // Clear the resolve channel if there is anything left. - while (!serviceResolvedChannel.isEmpty) { - serviceResolvedChannel.tryReceive() + @OptIn(ExperimentalCoroutinesApi::class) + private fun cleanup() { + runCatching { + if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.TIRAMISU) >= 7) { + nsdManager.stopServiceResolution(resolveListener) } + } - // Clear the discovered channel if there is anything left. - while (!serviceDiscoveredChannel.isEmpty) { - serviceDiscoveredChannel.tryReceive() - } + runCatching { + nsdManager.stopServiceDiscovery(discoveryListener) } - return port + // Clear the resolve channel if there is anything left. + while (!serviceResolvedChannel.isEmpty) { + serviceResolvedChannel.tryReceive() + } + + // Clear the discovered channel if there is anything left. + while (!serviceDiscoveredChannel.isEmpty) { + serviceDiscoveredChannel.tryReceive() + } } private fun isPortAvailable(port: Int): Boolean { From e0b10e96fa65aa16294ef7fd0d1e70f440414d60 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 22:29:34 +0200 Subject: [PATCH 198/215] #1394 fix bugs when recording and do not record BTN_TOOL_FINGER --- .../base/trigger/RecordTriggerController.kt | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index f33b341fe1..6998120a59 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -15,6 +15,7 @@ import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.Scancode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -44,8 +45,8 @@ class RecordTriggerControllerImpl @Inject constructor( private const val INPUT_EVENT_HUB_ID = "record_trigger" private val SCAN_CODES_BLACKLIST = setOf( - // BTN_TOUCH - 330, + Scancode.BTN_TOUCH, + Scancode.BTN_TOOL_FINGER, ) } @@ -67,18 +68,10 @@ class RecordTriggerControllerImpl @Inject constructor( private val downEvdevEvents: MutableSet = mutableSetOf() private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() - // TODO update when system bridge is (dis)connected - override val isEvdevRecordingPermitted: MutableStateFlow = - MutableStateFlow(getIsEvdevRecordingPermitted()) - // TODO set to false by default and turn into a flow private var isEvdevRecordingEnabled: Boolean = true override fun setEvdevRecordingEnabled(enabled: Boolean) { - if (!isEvdevRecordingPermitted.value) { - return - } - if (state.value is RecordTriggerState.CountingDown) { return } @@ -86,11 +79,6 @@ class RecordTriggerControllerImpl @Inject constructor( isEvdevRecordingEnabled = enabled } - private fun getIsEvdevRecordingPermitted(): Boolean { - // TODO check if the preference enabling pro mode key maps is turned on - return inputEventHub.isSystemBridgeConnected() - } - override fun onInputEvent( event: KMInputEvent, detectionSource: InputEventDetectionSource, @@ -101,10 +89,9 @@ class RecordTriggerControllerImpl @Inject constructor( when (event) { is KMEvdevEvent -> { - // TODO check -// if (!isEvdevRecordingEnabled || !isEvdevRecordingPermitted.value) { -// return false -// } + if (!isEvdevRecordingEnabled) { + return false + } // Do not record evdev events that are not key events. if (event.type != KMEvdevEvent.TYPE_KEY_EVENT) { @@ -115,8 +102,6 @@ class RecordTriggerControllerImpl @Inject constructor( return false } - Timber.d("Recorded evdev event ${event.code} ${KeyEvent.keyCodeToString(event.androidCode)}") - // Must also remove old down events if a new down even is received. val matchingDownEvent: KMEvdevEvent? = downEvdevEvents.find { it == event.copy(value = KMEvdevEvent.VALUE_DOWN) @@ -130,13 +115,14 @@ class RecordTriggerControllerImpl @Inject constructor( downEvdevEvents.add(event) } else if (event.isUpEvent) { onRecordKey(createEvdevRecordedKey(event)) + Timber.d("Recorded evdev event ${event.code} ${KeyEvent.keyCodeToString(event.androidCode)}") } return true } is KMGamePadEvent -> { - if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { + if (isEvdevRecordingEnabled) { return false } @@ -144,20 +130,19 @@ class RecordTriggerControllerImpl @Inject constructor( for (keyEvent in dpadKeyEvents) { if (keyEvent.action == KeyEvent.ACTION_DOWN) { - Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") - val recordedKey = createKeyEventRecordedKey( keyEvent, detectionSource, ) onRecordKey(recordedKey) + Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") } } return true } is KMKeyEvent -> { - if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { + if (isEvdevRecordingEnabled) { return false } @@ -182,6 +167,7 @@ class RecordTriggerControllerImpl @Inject constructor( if (matchingDownEvent != null) { val recordedKey = createKeyEventRecordedKey(event, detectionSource) onRecordKey(recordedKey) + Timber.d("Recorded key event ${KeyEvent.keyCodeToString(event.keyCode)}") } } return true @@ -227,7 +213,7 @@ class RecordTriggerControllerImpl @Inject constructor( return false } - if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { + if (isEvdevRecordingEnabled) { return false } @@ -315,7 +301,6 @@ interface RecordTriggerController { val state: StateFlow val onRecordKey: Flow - val isEvdevRecordingPermitted: Flow fun setEvdevRecordingEnabled(enabled: Boolean) /** From 2f1549e5c36987a482d13e3fa66604c72542534f Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 23:17:11 +0200 Subject: [PATCH 199/215] #1394 fix bugs autostarting on boot --- .../base/promode/SystemBridgeAutoStarter.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt index 5ca2f7d3c7..38429d3244 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -38,7 +39,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout -import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -122,19 +122,22 @@ class SystemBridgeAutoStarter @Inject constructor( @OptIn(FlowPreview::class) fun init() { coroutineScope.launch { + // The Key Mapper process may not necessarily be started on boot due to the + // on boot receiver so assume if it is started within 30 seconds of boot that + // it should be auto started. + val isBoot = SystemClock.uptimeMillis() < 30000 + val isBootAutoStartEnabled = preferences.get(Keys.isProModeAutoStartBootEnabled) .map { it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT } .first() // Wait 5 seconds for the system bridge to potentially connect itself to Key Mapper // before starting it. - val isConnected = - withTimeoutOrNull(5000L) { - connectionManager.connectionState.first { it is SystemBridgeConnectionState.Connected } - true - } ?: false + delay(5000) + + val connectionState = connectionManager.connectionState.value - if (isBootAutoStartEnabled && !isConnected) { + if (isBoot && isBootAutoStartEnabled && connectionState !is SystemBridgeConnectionState.Connected) { val autoStartType = autoStartTypeFlow.first() if (autoStartType != null) { From c55c83e32191c1459d8e0bb283b89fe264461230 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 23:27:30 +0200 Subject: [PATCH 200/215] #1394 use input event hub for recording motion events --- .../sds100/keymapper/base/BaseMainActivity.kt | 16 +++++++-- .../base/trigger/RecordTriggerController.kt | 36 ------------------- 2 files changed, 14 insertions(+), 38 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index d7cbf67ec0..7164ec369e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -26,6 +26,8 @@ import com.anggrayudi.storage.extension.openInputStream import com.anggrayudi.storage.extension.openOutputStream import com.anggrayudi.storage.extension.toDocumentFile import io.github.sds100.keymapper.base.compose.ComposeColors +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHubImpl import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate @@ -100,6 +102,9 @@ abstract class BaseMainActivity : AppCompatActivity() { @Inject lateinit var networkAdapter: AndroidNetworkAdapter + @Inject + lateinit var inputEventHub: InputEventHubImpl + private lateinit var requestPermissionDelegate: RequestPermissionDelegate private val currentNightMode: Int @@ -211,12 +216,19 @@ abstract class BaseMainActivity : AppCompatActivity() { super.onDestroy() } + /** + * Process motion events from the activity so that DPAD buttons can be recorded + * even when the Key Mapper IME is not being used. DO NOT record the key events because + * these are sent from the joy sticks. + */ override fun onGenericMotionEvent(event: MotionEvent?): Boolean { event ?: return super.onGenericMotionEvent(event) - // TODO send this to inputeventhub val gamepadEvent = KMGamePadEvent.fromMotionEvent(event) ?: return false - val consume = recordTriggerController.onActivityMotionEvent(gamepadEvent) + val consume = inputEventHub.onInputEvent( + gamepadEvent, + detectionSource = InputEventDetectionSource.INPUT_METHOD + ) return if (consume) { true diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 6998120a59..63c3f87958 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -14,7 +14,6 @@ import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent import io.github.sds100.keymapper.system.inputevents.KMKeyEvent -import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import io.github.sds100.keymapper.system.inputevents.Scancode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -68,7 +67,6 @@ class RecordTriggerControllerImpl @Inject constructor( private val downEvdevEvents: MutableSet = mutableSetOf() private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() - // TODO set to false by default and turn into a flow private var isEvdevRecordingEnabled: Boolean = true override fun setEvdevRecordingEnabled(enabled: Boolean) { @@ -202,40 +200,6 @@ class RecordTriggerControllerImpl @Inject constructor( return Success(Unit) } - /** - * Process motion events from the activity so that DPAD buttons can be recorded - * even when the Key Mapper IME is not being used. DO NOT record the key events because - * these are sent from the joy sticks. - * @return Whether the motion event is consumed. - */ - fun onActivityMotionEvent(event: KMGamePadEvent): Boolean { - if (state.value !is RecordTriggerState.CountingDown) { - return false - } - - if (isEvdevRecordingEnabled) { - return false - } - - val keyEvent = - dpadMotionEventTracker.convertMotionEvent(event).firstOrNull() ?: return false - - if (!KeyEventUtils.isDpadKeyCode(keyEvent.keyCode)) { - return false - } - - if (keyEvent.action == KeyEvent.ACTION_UP) { - val recordedKey = createKeyEventRecordedKey( - keyEvent, - InputEventDetectionSource.INPUT_METHOD, - ) - - onRecordKey(recordedKey) - } - - return true - } - private fun onRecordKey(recordedKey: RecordedKey) { recordedKeys.add(recordedKey) runBlocking { onRecordKey.emit(recordedKey) } From afea48b99f6ddc181f982242082dd716688771df Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 31 Aug 2025 23:46:15 +0200 Subject: [PATCH 201/215] #1394 disable pro mode in settings on SDK < Q --- .../keymapper/base/settings/SettingsScreen.kt | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index ede4c2e65d..297aff6eb6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -70,6 +70,8 @@ import io.github.sds100.keymapper.base.utils.ui.compose.icons.WandStars import io.github.sds100.keymapper.system.files.FileUtils import kotlinx.coroutines.launch +private val isProModeSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + @Composable fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { val state by viewModel.mainScreenState.collectAsStateWithLifecycle() @@ -78,7 +80,6 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) val context = LocalContext.current val scope = rememberCoroutineScope() - val automaticBackupLocationChooser = rememberLauncherForActivityResult(CreateDocument(FileUtils.MIME_TYPE_ZIP)) { uri -> uri ?: return@rememberLauncherForActivityResult @@ -139,7 +140,20 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onThemeSelected = viewModel::onThemeSelected, onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick, onDefaultOptionsClick = viewModel::onDefaultOptionsClick, - onProModeClick = viewModel::onProModeClick, + onProModeClick = { + if (isProModeSupported) { + viewModel.onProModeClick() + } else { + scope.launch { + snackbarHostState.showSnackbar( + context.getString( + R.string.error_sdk_version_too_low, + "Android 10" + ) + ) + } + } + }, onAutomaticChangeImeClick = viewModel::onAutomaticChangeImeClick, onForceVibrateToggled = viewModel::onForceVibrateToggled, onLoggingToggled = viewModel::onLoggingToggled, @@ -265,24 +279,22 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - OptionPageButton( - title = stringResource(R.string.title_pref_show_toggle_keymaps_notification), - text = stringResource(R.string.summary_pref_show_toggle_keymaps_notification), - icon = Icons.Rounded.PlayCircleOutline, - onClick = onPauseResumeNotificationClick - ) + OptionPageButton( + title = stringResource(R.string.title_pref_show_toggle_keymaps_notification), + text = stringResource(R.string.summary_pref_show_toggle_keymaps_notification), + icon = Icons.Rounded.PlayCircleOutline, + onClick = onPauseResumeNotificationClick + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - SwitchPreferenceCompose( - title = stringResource(R.string.title_pref_hide_home_screen_alerts), - text = stringResource(R.string.summary_pref_hide_home_screen_alerts), - icon = Icons.Rounded.VisibilityOff, - isChecked = state.hideHomeScreenAlerts, - onCheckedChange = onHideHomeScreenAlertsToggled - ) - } + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_hide_home_screen_alerts), + text = stringResource(R.string.summary_pref_hide_home_screen_alerts), + icon = Icons.Rounded.VisibilityOff, + isChecked = state.hideHomeScreenAlerts, + onCheckedChange = onHideHomeScreenAlertsToggled + ) Spacer(modifier = Modifier.height(8.dp)) @@ -354,8 +366,16 @@ private fun Content( Spacer(modifier = Modifier.height(8.dp)) OptionPageButton( - title = stringResource(R.string.title_pref_pro_mode), - text = stringResource(R.string.summary_pref_pro_mode), + title = if (isProModeSupported) { + stringResource(R.string.title_pref_pro_mode) + } else { + stringResource(R.string.title_pref_pro_mode) + }, + text = if (isProModeSupported) { + stringResource(R.string.summary_pref_pro_mode) + } else { + stringResource(R.string.error_sdk_version_too_low, "Android 10") + }, icon = KeyMapperIcons.ProModeIcon, onClick = onProModeClick ) From cb5d90af0e35e746fbe386d34a0bf43ba927ab88 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 1 Sep 2025 00:14:47 +0200 Subject: [PATCH 202/215] #1394 show system bridge errors for actions --- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 9 ++ .../base/actions/ActionErrorSnapshot.kt | 50 ++++++--- .../keymapper/base/actions/ActionUtils.kt | 106 ++++++++++++------ .../base/actions/ConfigActionsViewModel.kt | 101 ----------------- .../base/actions/GetActionErrorUseCase.kt | 44 +++++--- .../base/actions/PerformActionsUseCase.kt | 1 + .../base/detection/KeyMapAlgorithm.kt | 1 - .../base/keymaps/DisplayKeyMapUseCase.kt | 16 ++- .../base/onboarding/OnboardingUseCase.kt | 12 -- .../keymapper/base/promode/ProModeScreen.kt | 8 +- .../permissions/RequestPermissionDelegate.kt | 68 +++++------ .../sds100/keymapper/base/utils/ErrorUtils.kt | 11 +- base/src/main/res/values/strings.xml | 1 + .../io/github/sds100/keymapper/data/Keys.kt | 7 +- .../manager/SystemBridgeConnectionManager.kt | 2 +- .../sysbridge/utils/SystemBridgeResult.kt | 1 - .../permissions/AndroidPermissionAdapter.kt | 14 +-- .../keymapper/system/shizuku/ShizukuUtils.kt | 2 - 18 files changed, 211 insertions(+), 243 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index fe649afd41..c4afd7d072 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -25,6 +25,7 @@ import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.system.apps.AndroidPackageManagerAdapter import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl @@ -200,6 +201,14 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { systemBridgeAutoStarter.init() + + appCoroutineScope.launch { + systemBridgeConnectionManager.connectionState.collect { state -> + if (state is SystemBridgeConnectionState.Connected) { + settingsRepository.set(Keys.isSystemBridgeUsed, true) + } + } + } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index fcfffc494e..07e8aad67e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -1,12 +1,19 @@ package io.github.sds100.keymapper.base.actions +import android.os.Build import io.github.sds100.keymapper.base.actions.sound.SoundsManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.common.utils.valueOrNull +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.apps.PackageManagerAdapter import io.github.sds100.keymapper.system.camera.CameraAdapter @@ -17,7 +24,6 @@ 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.system.shizuku.ShizukuAdapter class LazyActionErrorSnapshot( private val packageManager: PackageManagerAdapter, @@ -26,9 +32,10 @@ class LazyActionErrorSnapshot( systemFeatureAdapter: SystemFeatureAdapter, cameraAdapter: CameraAdapter, private val soundsManager: SoundsManager, - shizukuAdapter: ShizukuAdapter, private val ringtoneAdapter: RingtoneAdapter, private val buildConfigProvider: BuildConfigProvider, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val preferenceRepository: PreferenceRepository ) : ActionErrorSnapshot, IsActionSupportedUseCase by IsActionSupportedUseCaseImpl( systemFeatureAdapter, @@ -40,8 +47,6 @@ class LazyActionErrorSnapshot( private val isCompatibleImeEnabled by lazy { keyMapperImeHelper.isCompatibleImeEnabled() } private val isCompatibleImeChosen by lazy { keyMapperImeHelper.isCompatibleImeChosen() } - private val isShizukuInstalled by lazy { shizukuAdapter.isInstalled.value } - private val isShizukuStarted by lazy { shizukuAdapter.isStarted.value } private val isVoiceAssistantInstalled by lazy { packageManager.isVoiceAssistantInstalled() } private val grantedPermissions: MutableMap = mutableMapOf() private val flashLenses by lazy { @@ -56,7 +61,22 @@ class LazyActionErrorSnapshot( } } - // TODO return system bridge errors + private val isSystemBridgeConnected: Boolean by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeConnectionManager.connectionState.value is SystemBridgeConnectionState.Connected + } else { + false + } + } + + private val isSystemBridgeUsed: Boolean by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + preferenceRepository.get(Keys.isSystemBridgeUsed).firstBlocking() ?: false + } else { + false + } + } + override fun getErrors(actions: List): Map { // Fixes #797 and #1719 // Store which input method would be selected if the actions run successfully. @@ -97,15 +117,10 @@ class LazyActionErrorSnapshot( return isSupportedError } - if (action.canUseShizukuToPerform() && isShizukuInstalled) { - if (!(action.canUseImeToPerform() && isCompatibleImeChosen)) { - when { - !isShizukuStarted -> return KMError.ShizukuNotStarted - - !isPermissionGranted(Permission.SHIZUKU) -> return SystemError.PermissionDenied( - Permission.SHIZUKU, - ) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && action.canUseSystemBridgeToPerform() && isSystemBridgeUsed) { + // Only throw an error if they aren't using another compatible back up option + if (!(action.canUseImeToPerform() && isCompatibleImeChosen) && !isSystemBridgeConnected) { + return SystemBridgeError.Disconnected } } else if (action.canUseImeToPerform()) { if (!isCompatibleImeEnabled) { @@ -117,6 +132,13 @@ class LazyActionErrorSnapshot( } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + ActionUtils.isSystemBridgeRequired(action.id) && + !isSystemBridgeConnected + ) { + return SystemBridgeError.Disconnected + } + for (permission in ActionUtils.getRequiredPermissions(action.id)) { if (!isPermissionGranted(permission)) { return SystemError.PermissionDenied(permission) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index 5d502d4ef1..c7ed1e5a71 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.base.actions import android.content.pm.PackageManager import android.os.Build import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack @@ -86,6 +87,8 @@ import io.github.sds100.keymapper.system.permissions.Permission object ActionUtils { + val isSystemBridgeSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + @StringRes fun getCategoryLabel(category: ActionCategory): Int = when (category) { ActionCategory.NAVIGATION -> R.string.action_cat_navigation @@ -492,26 +495,26 @@ object ActionUtils { ActionId.TOGGLE_DND_MODE, ActionId.ENABLE_DND_MODE, ActionId.DISABLE_DND_MODE, - -> Build.VERSION_CODES.M + -> Build.VERSION_CODES.M ActionId.DISABLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.TOGGLE_FLASHLIGHT, - -> Build.VERSION_CODES.M + -> Build.VERSION_CODES.M ActionId.CHANGE_FLASHLIGHT_STRENGTH, - -> Build.VERSION_CODES.TIRAMISU + -> Build.VERSION_CODES.TIRAMISU ActionId.TOGGLE_KEYBOARD, ActionId.SHOW_KEYBOARD, ActionId.HIDE_KEYBOARD, - -> Build.VERSION_CODES.N + -> Build.VERSION_CODES.N ActionId.TEXT_CUT, ActionId.TEXT_COPY, ActionId.TEXT_PASTE, ActionId.SELECT_WORD_AT_CURSOR, - -> Build.VERSION_CODES.JELLY_BEAN_MR2 + -> Build.VERSION_CODES.JELLY_BEAN_MR2 ActionId.SHOW_POWER_MENU -> Build.VERSION_CODES.LOLLIPOP ActionId.DEVICE_CONTROLS -> Build.VERSION_CODES.S @@ -528,7 +531,7 @@ object ActionUtils { ActionId.ENABLE_BLUETOOTH, ActionId.DISABLE_BLUETOOTH, ActionId.TOGGLE_BLUETOOTH, - -> Build.VERSION_CODES.S_V2 + -> Build.VERSION_CODES.S_V2 // See https://issuetracker.google.com/issues/225186417. The global action // is not marked as deprecated even though it doesn't work. @@ -541,49 +544,73 @@ object ActionUtils { ActionId.END_PHONE_CALL, ActionId.ANSWER_PHONE_CALL, ActionId.PHONE_CALL, - -> listOf(PackageManager.FEATURE_TELEPHONY) + -> listOf(PackageManager.FEATURE_TELEPHONY) ActionId.SECURE_LOCK_DEVICE, - -> listOf(PackageManager.FEATURE_DEVICE_ADMIN) + -> listOf(PackageManager.FEATURE_DEVICE_ADMIN) ActionId.TOGGLE_WIFI, ActionId.ENABLE_WIFI, ActionId.DISABLE_WIFI, - -> listOf(PackageManager.FEATURE_WIFI) + -> listOf(PackageManager.FEATURE_WIFI) ActionId.TOGGLE_NFC, ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, - -> listOf(PackageManager.FEATURE_NFC) + -> listOf(PackageManager.FEATURE_NFC) ActionId.TOGGLE_BLUETOOTH, ActionId.ENABLE_BLUETOOTH, ActionId.DISABLE_BLUETOOTH, - -> listOf(PackageManager.FEATURE_BLUETOOTH) + -> listOf(PackageManager.FEATURE_BLUETOOTH) ActionId.TOGGLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.DISABLE_FLASHLIGHT, ActionId.CHANGE_FLASHLIGHT_STRENGTH, - -> listOf(PackageManager.FEATURE_CAMERA_FLASH) + -> listOf(PackageManager.FEATURE_CAMERA_FLASH) else -> emptyList() } + @RequiresApi(Build.VERSION_CODES.Q) + fun isSystemBridgeRequired(id: ActionId): Boolean { + return when (id) { + ActionId.ENABLE_WIFI, + ActionId.DISABLE_WIFI, + ActionId.TOGGLE_WIFI -> true + + ActionId.TOGGLE_MOBILE_DATA, + ActionId.ENABLE_MOBILE_DATA, + ActionId.DISABLE_MOBILE_DATA, + -> true + + ActionId.ENABLE_NFC, + ActionId.DISABLE_NFC, + ActionId.TOGGLE_NFC, + -> true + + ActionId.TOGGLE_AIRPLANE_MODE, + ActionId.ENABLE_AIRPLANE_MODE, + ActionId.DISABLE_AIRPLANE_MODE, + -> true + + ActionId.POWER_ON_OFF_DEVICE -> true + + else -> false + } + } + fun getRequiredPermissions(id: ActionId): List { when (id) { - // TODO show action error if pro mode is not started -// ActionId.TOGGLE_WIFI, -// ActionId.ENABLE_WIFI, -// ActionId.DISABLE_WIFI, -// -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -// return listOf(Permission.ROOT) -// } - ActionId.TOGGLE_MOBILE_DATA, ActionId.ENABLE_MOBILE_DATA, ActionId.DISABLE_MOBILE_DATA, - -> return listOf(Permission.ROOT) + -> return if (isSystemBridgeSupported) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.PLAY_PAUSE_MEDIA_PACKAGE, ActionId.PAUSE_MEDIA_PACKAGE, @@ -592,7 +619,7 @@ object ActionUtils { ActionId.PREVIOUS_TRACK_PACKAGE, ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, - -> return listOf(Permission.NOTIFICATION_LISTENER) + -> return listOf(Permission.NOTIFICATION_LISTENER) ActionId.VOLUME_UP, ActionId.VOLUME_DOWN, @@ -608,7 +635,7 @@ object ActionUtils { ActionId.TOGGLE_DND_MODE, ActionId.DISABLE_DND_MODE, ActionId.ENABLE_DND_MODE, - -> return listOf(Permission.ACCESS_NOTIFICATION_POLICY) + -> return listOf(Permission.ACCESS_NOTIFICATION_POLICY) ActionId.TOGGLE_AUTO_ROTATE, ActionId.ENABLE_AUTO_ROTATE, @@ -617,25 +644,29 @@ object ActionUtils { ActionId.LANDSCAPE_MODE, ActionId.SWITCH_ORIENTATION, ActionId.CYCLE_ROTATIONS, - -> return listOf(Permission.WRITE_SETTINGS) + -> return listOf(Permission.WRITE_SETTINGS) ActionId.TOGGLE_AUTO_BRIGHTNESS, ActionId.ENABLE_AUTO_BRIGHTNESS, ActionId.DISABLE_AUTO_BRIGHTNESS, ActionId.INCREASE_BRIGHTNESS, ActionId.DECREASE_BRIGHTNESS, - -> return listOf(Permission.WRITE_SETTINGS) + -> return listOf(Permission.WRITE_SETTINGS) ActionId.TOGGLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.DISABLE_FLASHLIGHT, ActionId.CHANGE_FLASHLIGHT_STRENGTH, - -> return listOf(Permission.CAMERA) + -> return listOf(Permission.CAMERA) ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, ActionId.TOGGLE_NFC, - -> return listOf(Permission.ROOT) + -> return if (isSystemBridgeSupported) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.SHOW_KEYBOARD_PICKER -> if (Build.VERSION.SDK_INT in Build.VERSION_CODES.O_MR1..Build.VERSION_CODES.P) { @@ -649,7 +680,11 @@ object ActionUtils { ActionId.TOGGLE_AIRPLANE_MODE, ActionId.ENABLE_AIRPLANE_MODE, ActionId.DISABLE_AIRPLANE_MODE, - -> return listOf(Permission.ROOT) + -> return if (isSystemBridgeSupported) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.SCREENSHOT -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { return listOf(Permission.ROOT) @@ -660,15 +695,19 @@ object ActionUtils { } ActionId.SECURE_LOCK_DEVICE -> return listOf(Permission.DEVICE_ADMIN) - ActionId.POWER_ON_OFF_DEVICE -> return listOf(Permission.ROOT) + ActionId.POWER_ON_OFF_DEVICE -> return if (isSystemBridgeSupported) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.DISMISS_ALL_NOTIFICATIONS, ActionId.DISMISS_MOST_RECENT_NOTIFICATION, - -> return listOf(Permission.NOTIFICATION_LISTENER) + -> return listOf(Permission.NOTIFICATION_LISTENER) ActionId.ANSWER_PHONE_CALL, ActionId.END_PHONE_CALL, - -> return listOf(Permission.ANSWER_PHONE_CALL) + -> return listOf(Permission.ANSWER_PHONE_CALL) ActionId.PHONE_CALL -> return listOf(Permission.CALL_PHONE) @@ -804,7 +843,6 @@ object ActionUtils { fun ActionData.canBeHeldDown(): Boolean = when (this) { is ActionData.InputKeyEvent -> !useShell - is ActionData.TapScreen -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.O else -> false } @@ -814,7 +852,7 @@ fun ActionData.canUseImeToPerform(): Boolean = when (this) { else -> false } -fun ActionData.canUseShizukuToPerform(): Boolean = when (this) { +fun ActionData.canUseSystemBridgeToPerform(): Boolean = when (this) { is ActionData.InputKeyEvent -> true else -> false } @@ -850,7 +888,7 @@ fun ActionData.isEditable(): Boolean = when (this) { is ActionData.HttpRequest, is ActionData.InteractUiElement, is ActionData.MoveCursor, - -> true + -> true else -> false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index d08ace4625..bd5c032166 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -139,14 +139,10 @@ class ConfigActionsViewModel @Inject constructor( viewModelScope.launch { val actionData = navigate("add_action", NavDestination.ChooseAction) ?: return@launch - val showInstallShizukuPrompt = onboarding.showInstallShizukuPrompt(actionData) val showInstallGuiKeyboardPrompt = onboarding.showInstallGuiKeyboardPrompt(actionData) when { - showInstallShizukuPrompt && showInstallGuiKeyboardPrompt -> - promptToInstallShizukuOrGuiKeyboard() - showInstallGuiKeyboardPrompt -> promptToInstallGuiKeyboard() } @@ -314,103 +310,6 @@ class ConfigActionsViewModel @Inject constructor( } } - // TODO stop advertising the GUI keyboard, and ask them to use PRO mode? - private suspend fun promptToInstallShizukuOrGuiKeyboard() { - if (onboarding.isTvDevice()) { - val chooseSolutionDialog = DialogModel.Alert( - title = getText(R.string.dialog_title_install_shizuku_or_leanback_keyboard), - message = getText(R.string.dialog_message_install_shizuku_or_leanback_keyboard), - positiveButtonText = getString(R.string.dialog_button_install_shizuku), - negativeButtonText = getString(R.string.dialog_button_install_leanback_keyboard), - neutralButtonText = getString(R.string.dialog_button_install_nothing), - ) - - val chooseSolutionResponse = - showDialog("choose_solution", chooseSolutionDialog) ?: return - - when (chooseSolutionResponse) { - // install shizuku - DialogResponse.POSITIVE -> { -// navigate("shizuku", NavDestination.ShizukuSettings) -// onboarding.neverShowGuiKeyboardPromptsAgain() - - return - } - // do nothing - DialogResponse.NEUTRAL -> { - onboarding.neverShowGuiKeyboardPromptsAgain() - return - } - - // download leanback keyboard - DialogResponse.NEGATIVE -> { - val chooseAppStoreDialog = DialogModel.ChooseAppStore( - title = getString(R.string.dialog_title_choose_download_leanback_keyboard), - message = getString(R.string.dialog_message_choose_download_leanback_keyboard), - model = ChooseAppStoreModel( - githubLink = getString(R.string.url_github_keymapper_leanback_keyboard), - ), - positiveButtonText = getString(R.string.pos_never_show_again), - negativeButtonText = getString(R.string.neg_cancel), - ) - - val response = showDialog("install_leanback_keyboard", chooseAppStoreDialog) - - if (response == DialogResponse.POSITIVE) { - onboarding.neverShowGuiKeyboardPromptsAgain() - } - } - } - } else { - val chooseSolutionDialog = DialogModel.Alert( - title = getText(R.string.dialog_title_install_shizuku_or_gui_keyboard), - message = getText(R.string.dialog_message_install_shizuku_or_gui_keyboard), - positiveButtonText = getString(R.string.dialog_button_install_shizuku), - negativeButtonText = getString(R.string.dialog_button_install_gui_keyboard), - neutralButtonText = getString(R.string.dialog_button_install_nothing), - ) - - val chooseSolutionResponse = - showDialog("choose_solution", chooseSolutionDialog) ?: return - - when (chooseSolutionResponse) { - // install shizuku - DialogResponse.POSITIVE -> { -// navigate("shizuku_error", NavDestination.ShizukuSettings) -// onboarding.neverShowGuiKeyboardPromptsAgain() - - return - } - // do nothing - DialogResponse.NEUTRAL -> { - onboarding.neverShowGuiKeyboardPromptsAgain() - return - } - - // download gui keyboard - DialogResponse.NEGATIVE -> { - val chooseAppStoreDialog = DialogModel.ChooseAppStore( - title = getString(R.string.dialog_title_choose_download_gui_keyboard), - message = getString(R.string.dialog_message_choose_download_gui_keyboard), - model = ChooseAppStoreModel( - playStoreLink = getString(R.string.url_play_store_keymapper_gui_keyboard), - fdroidLink = getString(R.string.url_fdroid_keymapper_gui_keyboard), - githubLink = getString(R.string.url_github_keymapper_gui_keyboard), - ), - positiveButtonText = getString(R.string.pos_never_show_again), - negativeButtonText = getString(R.string.neg_cancel), - ) - - val response = showDialog("install_gui_keyboard", chooseAppStoreDialog) - - if (response == DialogResponse.POSITIVE) { - onboarding.neverShowGuiKeyboardPromptsAgain() - } - } - } - } - } - private fun buildShortcut(action: ActionData): ShortcutModel { return ShortcutModel( icon = uiHelper.getIcon(action), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt index d165b5025b..889863bb1e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt @@ -1,18 +1,22 @@ package io.github.sds100.keymapper.base.actions +import android.os.Build import io.github.sds100.keymapper.base.actions.sound.SoundsManager import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.apps.PackageManagerAdapter 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 io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import javax.inject.Inject @@ -26,9 +30,10 @@ class GetActionErrorUseCaseImpl @Inject constructor( private val systemFeatureAdapter: SystemFeatureAdapter, private val cameraAdapter: CameraAdapter, private val soundsManager: SoundsManager, - private val shizukuAdapter: ShizukuAdapter, private val ringtoneAdapter: RingtoneAdapter, private val buildConfigProvider: BuildConfigProvider, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val preferenceRepository: PreferenceRepository ) : GetActionErrorUseCase { private val invalidateActionErrors = merge( @@ -37,9 +42,15 @@ class GetActionErrorUseCaseImpl @Inject constructor( inputMethodAdapter.inputMethods.drop(1).map { }, permissionAdapter.onPermissionsUpdate, soundsManager.soundFiles.drop(1).map { }, - shizukuAdapter.isStarted.drop(1).map { }, - shizukuAdapter.isInstalled.drop(1).map { }, packageManagerAdapter.onPackagesChanged, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + merge( + systemBridgeConnectionManager.connectionState.drop(1).map { }, + preferenceRepository.get(Keys.isSystemBridgeUsed) + ) + } else { + emptyFlow() + }, ) override val actionErrorSnapshot: Flow = channelFlow { @@ -50,17 +61,20 @@ class GetActionErrorUseCaseImpl @Inject constructor( } } - private fun createSnapshot(): ActionErrorSnapshot = LazyActionErrorSnapshot( - packageManagerAdapter, - inputMethodAdapter, - permissionAdapter, - systemFeatureAdapter, - cameraAdapter, - soundsManager, - shizukuAdapter, - ringtoneAdapter, - buildConfigProvider, - ) + private fun createSnapshot(): ActionErrorSnapshot { + return LazyActionErrorSnapshot( + packageManagerAdapter, + inputMethodAdapter, + permissionAdapter, + systemFeatureAdapter, + cameraAdapter, + soundsManager, + ringtoneAdapter, + buildConfigProvider, + systemBridgeConnectionManager, + preferenceRepository + ) + } } interface GetActionErrorUseCase { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index a0adcbf0de..1fb6b8ac9c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -118,6 +118,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( ) } + // TODO use system bridge where possible override suspend fun perform( action: ActionData, inputEventAction: InputEventAction, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index fe9f937e22..59a46876c5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -589,7 +589,6 @@ class KeyMapAlgorithm( return consume } - // TODO detect by scancode if the keycode is unknown. /** * @return whether to consume the [KeyEvent]. */ diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt index d4632d4e2b..4c8b1c91cc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt @@ -12,6 +12,9 @@ import io.github.sds100.keymapper.base.purchasing.PurchasingManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.base.trigger.TriggerError import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot +import io.github.sds100.keymapper.base.utils.navigation.NavDestination +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult @@ -23,6 +26,7 @@ import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.common.utils.valueIfFailure import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.apps.PackageManagerAdapter @@ -54,6 +58,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( private val getActionErrorUseCase: GetActionErrorUseCase, private val getConstraintErrorUseCase: GetConstraintErrorUseCase, private val buildConfigProvider: BuildConfigProvider, + private val navigationProvider: NavigationProvider ) : DisplayKeyMapUseCase, GetActionErrorUseCase by getActionErrorUseCase, GetConstraintErrorUseCase by getConstraintErrorUseCase { @@ -158,9 +163,11 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( } } - override fun getAppName(packageName: String): KMResult = packageManagerAdapter.getAppName(packageName) + override fun getAppName(packageName: String): KMResult = + packageManagerAdapter.getAppName(packageName) - override fun getAppIcon(packageName: String): KMResult = packageManagerAdapter.getAppIcon(packageName) + override fun getAppIcon(packageName: String): KMResult = + packageManagerAdapter.getAppIcon(packageName) override fun getInputMethodLabel(imeId: String): KMResult = inputMethodAdapter.getInfoById(imeId).then { Success(it.label) } @@ -191,6 +198,11 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( } } + is SystemBridgeError.Disconnected -> navigationProvider.navigate( + "fix_system_bridge", + NavDestination.ProMode + ) + else -> Unit } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt index b385ec5821..b0d670a105 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base.onboarding import androidx.datastore.preferences.core.Preferences import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.actions.canUseImeToPerform -import io.github.sds100.keymapper.base.actions.canUseShizukuToPerform import io.github.sds100.keymapper.base.purchasing.ProductId import io.github.sds100.keymapper.base.purchasing.PurchasingManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper @@ -23,7 +22,6 @@ 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.system.shizuku.ShizukuAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuUtils import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance @@ -61,10 +59,6 @@ class OnboardingUseCaseImpl @Inject constructor( action.canUseImeToPerform() } - override suspend fun showInstallShizukuPrompt(action: ActionData): Boolean = !shizukuAdapter.isInstalled.value && - ShizukuUtils.isRecommendedForSdkVersion() && - action.canUseShizukuToPerform() - override fun neverShowGuiKeyboardPromptsAgain() { settingsRepository.set(Keys.acknowledgedGuiKeyboard, true) } @@ -235,12 +229,6 @@ interface OnboardingUseCase { */ suspend fun showInstallGuiKeyboardPrompt(action: ActionData): Boolean - /** - * @return whether to prompt the user to install Shizuku after adding - * this action - */ - suspend fun showInstallShizukuPrompt(action: ActionData): Boolean - fun isTvDevice(): Boolean fun neverShowGuiKeyboardPromptsAgain() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 6fe2dbca52..7505551b5a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -214,7 +214,7 @@ private fun Content( } is State.Data -> { - SetupSection( + LoadedContent( modifier = Modifier.fillMaxWidth(), state = setupState.data, onShizukuButtonClick = onShizukuButtonClick, @@ -234,15 +234,11 @@ private fun Content( textAlign = TextAlign.Center, ) } - - Spacer(modifier = Modifier.height(16.dp)) - - // TODO show options for safety and autostart. Show different autostart text for root vs shizuku } } @Composable -private fun SetupSection( +private fun LoadedContent( modifier: Modifier, state: ProModeState, onRootButtonClick: () -> Unit = {}, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt index 5634a20398..c45bf06730 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt @@ -22,7 +22,6 @@ import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapt import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuUtils import io.github.sds100.keymapper.system.url.UrlUtils import splitties.alertdialog.appcompat.messageResource import splitties.alertdialog.appcompat.negativeButton @@ -77,14 +76,9 @@ class RequestPermissionDelegate( } Permission.IGNORE_BATTERY_OPTIMISATION -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestIgnoreBatteryOptimisations() - } + requestIgnoreBatteryOptimisations() - Permission.SHIZUKU -> - if (ShizukuUtils.isSupportedForSdkVersion()) { - shizukuAdapter.requestPermission() - } + Permission.SHIZUKU -> shizukuAdapter.requestPermission() Permission.ACCESS_FINE_LOCATION -> requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) @@ -112,10 +106,32 @@ class RequestPermissionDelegate( } private fun requestAccessNotificationPolicy() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS) + val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS) + + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_ACTIVITY_CLEAR_TASK + or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + // Add this flag so user only has to press back once. + or Intent.FLAG_ACTIVITY_NO_HISTORY, + ) + + try { + startActivityForResultLauncher.launch(intent) + } catch (e: Exception) { + Toast.makeText( + activity, + R.string.error_cant_find_dnd_access_settings, + Toast.LENGTH_SHORT, + ).show() + } + } - intent.addFlags( + private fun requestWriteSettings() { + Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply { + data = Uri.parse("package:${buildConfigProvider.packageName}") + + addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS @@ -124,43 +140,17 @@ class RequestPermissionDelegate( ) try { - startActivityForResultLauncher.launch(intent) + activity.startActivity(this) } catch (e: Exception) { Toast.makeText( activity, - R.string.error_cant_find_dnd_access_settings, + R.string.error_cant_find_write_settings_page, Toast.LENGTH_SHORT, ).show() } } } - private fun requestWriteSettings() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply { - data = Uri.parse("package:${buildConfigProvider.packageName}") - - addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - or Intent.FLAG_ACTIVITY_CLEAR_TASK - or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - // Add this flag so user only has to press back once. - or Intent.FLAG_ACTIVITY_NO_HISTORY, - ) - - try { - activity.startActivity(this) - } catch (e: Exception) { - Toast.makeText( - activity, - R.string.error_cant_find_write_settings_page, - Toast.LENGTH_SHORT, - ).show() - } - } - } - } - private fun requestWriteSecureSettings() { if (permissionAdapter.isGranted(Permission.SHIZUKU) || permissionAdapter.isGranted(Permission.ROOT) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt index 4c2dff014c..6ae66881b7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt @@ -6,6 +6,7 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.BuildUtils import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.data.DataError +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission @@ -83,7 +84,11 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { is KMError.Exception -> exception.toString() is KMError.EmptyJson -> resourceProvider.getString(R.string.error_empty_json) is KMError.InvalidNumber -> resourceProvider.getString(R.string.error_invalid_number) - is KMError.NumberTooSmall -> resourceProvider.getString(R.string.error_number_too_small, min) + is KMError.NumberTooSmall -> resourceProvider.getString( + R.string.error_number_too_small, + min + ) + is KMError.NumberTooBig -> resourceProvider.getString(R.string.error_number_too_big, max) is KMError.EmptyText -> resourceProvider.getString(R.string.error_cant_be_empty) KMError.BackupVersionTooNew -> resourceProvider.getString(R.string.error_backup_version_too_new) @@ -173,6 +178,7 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { KMError.InvalidBackup -> resourceProvider.getString(R.string.error_invalid_backup) KMError.MalformedUrl -> resourceProvider.getString(R.string.error_malformed_url) KMError.UiElementNotFound -> resourceProvider.getString(R.string.error_ui_element_not_found) + is SystemBridgeError.Disconnected -> resourceProvider.getString(R.string.error_system_bridge_disconnected) else -> this.toString() } @@ -190,8 +196,7 @@ val KMError.isFixable: Boolean is SystemError.PermissionDenied, is KMError.ShizukuNotStarted, is KMError.CantDetectKeyEventsInPhoneCall, - - -> true + is SystemBridgeError.Disconnected -> true else -> false } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 4ba39347ac..236dc21158 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -898,6 +898,7 @@ Must be %d or less! UI element not found! + PRO Mode needs starting diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 0ee6383581..705a8176f0 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -9,7 +9,7 @@ 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 hasRootPermission = booleanPreferencesKey("pref_allow_root_features") val shownAppIntro = booleanPreferencesKey("pref_first_time") @@ -124,4 +124,9 @@ object Keys { val isSystemBridgeEmergencyKilled = booleanPreferencesKey("key_is_system_bridge_emergency_killed") + + /** + * Whether the user has started the system bridge before. + */ + val isSystemBridgeUsed = booleanPreferencesKey("key_is_system_bridge_used") } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 656df5855b..39548be3a4 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -45,7 +45,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, private val starter: SystemBridgeStarter, - private val buildConfigProvider: BuildConfigProvider + private val buildConfigProvider: BuildConfigProvider, ) : SystemBridgeConnectionManager { private val systemBridgeLock: Any = Any() diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt index 10023629f6..54418fcb93 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt @@ -3,6 +3,5 @@ package io.github.sds100.keymapper.sysbridge.utils import io.github.sds100.keymapper.common.utils.KMError sealed class SystemBridgeError : KMError() { - data object NotStarted : SystemBridgeError() data object Disconnected : SystemBridgeError() } \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt index 8cbd8d35a1..df746e8621 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt @@ -34,7 +34,6 @@ import io.github.sds100.keymapper.system.DeviceAdmin import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -192,7 +191,7 @@ class AndroidPermissionAdapter @Inject constructor( } } else { // The system bridge should be the default way to grant permissions. - result = SystemBridgeError.NotStarted + result = SystemBridgeError.Disconnected } result.onSuccess { @@ -252,18 +251,11 @@ class AndroidPermissionAdapter @Inject constructor( Permission.ROOT -> suAdapter.isRootGranted.value Permission.IGNORE_BATTERY_OPTIMISATION -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val ignoringOptimisations = - powerManager?.isIgnoringBatteryOptimizations(buildConfigProvider.packageName) - - ignoringOptimisations ?: false - } else { - true - } + powerManager?.isIgnoringBatteryOptimizations(buildConfigProvider.packageName) ?: false // this check is super quick (~0ms) so this doesn't need to be cached. Permission.SHIZUKU -> { - if (ShizukuUtils.isSupportedForSdkVersion() && Shizuku.getBinder() != null) { + if (Shizuku.getBinder() != null) { Shizuku.checkSelfPermission() == PERMISSION_GRANTED } else { false diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt index df8a057936..856129b345 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt @@ -11,6 +11,4 @@ object ShizukuUtils { * Android 11 because a PC/mac isn't needed after every reboot to make it work. */ fun isRecommendedForSdkVersion(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - - fun isSupportedForSdkVersion(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M } From 04a25ed5d434a898505767010538a8101a75ee99 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 1 Sep 2025 18:54:21 +0200 Subject: [PATCH 203/215] #1394 support mobile data actions with system bridge --- .../keymapper/base/actions/ActionUtils.kt | 10 ++++++ .../keymapper/base/settings/SettingsScreen.kt | 8 +++-- .../sds100/keymapper/base/utils/ErrorUtils.kt | 5 ++- .../keymapper/sysbridge/ISystemBridge.aidl | 4 ++- .../sysbridge/service/SystemBridge.kt | 31 +++++++++++++++++-- .../system/network/AndroidNetworkAdapter.kt | 31 ++++++++++++++++--- .../internal/telephony/ITelephony.aidl | 9 ++++++ 7 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 systemstubs/src/main/aidl/com/android/internal/telephony/ITelephony.aidl diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index c7ed1e5a71..5831c9c05b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -528,6 +528,7 @@ object ActionUtils { // The global action still fails even though the API exists in SDK 34. ActionId.COLLAPSE_STATUS_BAR -> Build.VERSION_CODES.TIRAMISU + // TODO support system bridge ActionId.ENABLE_BLUETOOTH, ActionId.DISABLE_BLUETOOTH, ActionId.TOGGLE_BLUETOOTH, @@ -554,6 +555,15 @@ object ActionUtils { ActionId.DISABLE_WIFI, -> listOf(PackageManager.FEATURE_WIFI) + ActionId.TOGGLE_MOBILE_DATA, + ActionId.ENABLE_MOBILE_DATA, + ActionId.DISABLE_MOBILE_DATA, + -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + listOf(PackageManager.FEATURE_TELEPHONY_DATA) + } else { + listOf(PackageManager.FEATURE_TELEPHONY) + } + ActionId.TOGGLE_NFC, ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 297aff6eb6..3be4ee87ee 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -67,6 +67,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.icons.FolderManaged import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon import io.github.sds100.keymapper.base.utils.ui.compose.icons.WandStars +import io.github.sds100.keymapper.common.utils.BuildUtils import io.github.sds100.keymapper.system.files.FileUtils import kotlinx.coroutines.launch @@ -148,7 +149,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) snackbarHostState.showSnackbar( context.getString( R.string.error_sdk_version_too_low, - "Android 10" + BuildUtils.getSdkVersionName(Build.VERSION_CODES.Q) ) ) } @@ -374,7 +375,10 @@ private fun Content( text = if (isProModeSupported) { stringResource(R.string.summary_pref_pro_mode) } else { - stringResource(R.string.error_sdk_version_too_low, "Android 10") + stringResource( + R.string.error_sdk_version_too_low, + BuildUtils.getSdkVersionName(Build.VERSION_CODES.Q) + ) }, icon = KeyMapperIcons.ProModeIcon, onClick = onProModeClick diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt index 6ae66881b7..8912d09654 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt @@ -54,7 +54,10 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { PackageManager.FEATURE_BLUETOOTH -> resourceProvider.getString(R.string.error_system_feature_bluetooth_unsupported) PackageManager.FEATURE_DEVICE_ADMIN -> resourceProvider.getString(R.string.error_system_feature_device_admin_unsupported) PackageManager.FEATURE_CAMERA_FLASH -> resourceProvider.getString(R.string.error_system_feature_camera_flash_unsupported) - PackageManager.FEATURE_TELEPHONY -> resourceProvider.getString(R.string.error_system_feature_telephony_unsupported) + PackageManager.FEATURE_TELEPHONY, PackageManager.FEATURE_TELEPHONY_DATA -> resourceProvider.getString( + R.string.error_system_feature_telephony_unsupported + ) + else -> throw Exception("Don't know how to get error message for this system feature ${this.feature}") } diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 8829018878..969254edaa 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -26,5 +26,7 @@ interface ISystemBridge { boolean setWifiEnabled(boolean enable) = 10; - void grantPermission(String permission, int deviceId) = 12; + void grantPermission(String permission, int deviceId) = 11; + + void setDataEnabled(int subId, boolean enable) = 12; } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 5b6c7b9d0d..21ef30790c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -6,6 +6,7 @@ import android.content.IContentProvider import android.content.pm.ApplicationInfo import android.hardware.input.IInputManager import android.net.wifi.IWifiManager +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.IBinder @@ -16,6 +17,7 @@ import android.permission.IPermissionManager import android.permission.PermissionManagerApis import android.util.Log import android.view.InputEvent +import com.android.internal.telephony.ITelephony import io.github.sds100.keymapper.common.models.EvdevDeviceHandle import io.github.sds100.keymapper.common.utils.UserHandleUtils import io.github.sds100.keymapper.sysbridge.IEvdevCallback @@ -64,9 +66,8 @@ internal class SystemBridge : ISystemBridge.Stub() { private val systemBridgeVersionCode: Int = System.getProperty("keymapper_sysbridge.version_code")!!.toInt() - private const val SHELL_PACKAGE = "com.android.shell" - private const val KEYMAPPER_CHECK_INTERVAL_MS = 60 * 1000L // 1 minute + private const val DATA_ENABLED_REASON_USER: Int = 0 @JvmStatic fun main(args: Array) { @@ -147,6 +148,13 @@ internal class SystemBridge : ISystemBridge.Stub() { private val inputManager: IInputManager private val wifiManager: IWifiManager private val permissionManager: IPermissionManager + private val telephonyManager: ITelephony + + private val processPackageName: String = when (Process.myUid()) { + Process.ROOT_UID -> "root" + Process.SHELL_UID -> "com.android.shell" + else -> throw IllegalStateException("SystemBridge must run as root or shell user") + } init { val libraryPath = System.getProperty("keymapper_sysbridge.library.path") @@ -171,6 +179,10 @@ internal class SystemBridge : ISystemBridge.Stub() { wifiManager = IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) + waitSystemService(Context.TELEPHONY_SERVICE) + telephonyManager = + ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)) + val applicationInfo = getKeyMapperPackageInfo() if (applicationInfo == null) { @@ -299,7 +311,7 @@ internal class SystemBridge : ISystemBridge.Stub() { } override fun setWifiEnabled(enable: Boolean): Boolean { - return wifiManager.setWifiEnabled(SHELL_PACKAGE, enable) + return wifiManager.setWifiEnabled(processPackageName, enable) } override fun writeEvdevEvent(devicePath: String?, type: Int, code: Int, value: Int): Boolean { @@ -439,4 +451,17 @@ internal class SystemBridge : ISystemBridge.Stub() { override fun getVersionCode(): Int { return systemBridgeVersionCode } + + override fun setDataEnabled(subId: Int, enable: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + telephonyManager.setDataEnabledForReason( + subId, + DATA_ENABLED_REASON_USER, + enable, + processPackageName + ) + } else { + telephonyManager.setUserDataEnabled(subId, enable) + } + } } \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 33137e69f0..c174ba639c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -12,6 +12,7 @@ import android.net.NetworkRequest import android.net.wifi.WifiManager import android.os.Build import android.provider.Settings +import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import androidx.core.content.getSystemService @@ -150,16 +151,36 @@ class AndroidNetworkAdapter @Inject constructor( } override fun isMobileDataEnabled(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return telephonyManager.isDataEnabled + return telephonyManager.isDataEnabled + } + + override fun enableMobileData(): KMResult<*> { + val subId = SubscriptionManager.getDefaultSubscriptionId() + + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + throw IllegalStateException("No valid subscription ID") + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return systemBridgeConnManager.run { bridge -> bridge.setDataEnabled(subId, true) } } else { - return telephonyManager.dataState == TelephonyManager.DATA_CONNECTED + return suAdapter.execute("svc data enable") } } - override fun enableMobileData(): KMResult<*> = suAdapter.execute("svc data enable") + override fun disableMobileData(): KMResult<*> { + val subId = SubscriptionManager.getDefaultSubscriptionId() - override fun disableMobileData(): KMResult<*> = suAdapter.execute("svc data disable") + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + throw IllegalStateException("No valid subscription ID") + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return systemBridgeConnManager.run { bridge -> bridge.setDataEnabled(subId, false) } + } else { + return suAdapter.execute("svc data disable") + } + } /** * @return Null on Android 10+ because there is no API to do this anymore. diff --git a/systemstubs/src/main/aidl/com/android/internal/telephony/ITelephony.aidl b/systemstubs/src/main/aidl/com/android/internal/telephony/ITelephony.aidl new file mode 100644 index 0000000000..c26a955b6b --- /dev/null +++ b/systemstubs/src/main/aidl/com/android/internal/telephony/ITelephony.aidl @@ -0,0 +1,9 @@ +package com.android.internal.telephony; + +interface ITelephony { + // Requires Android 12+ + void setDataEnabledForReason(int subId, int reason, boolean enable, String callingPackage); + + // Max Android 11 + void setUserDataEnabled(int subId, boolean enable); +} \ No newline at end of file From 39f5c11ebde66869c97834b8bb2a2a375bb268e5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 1 Sep 2025 18:57:11 +0200 Subject: [PATCH 204/215] #1394 recording triggers with accessibility service works again --- .../keymapper/base/trigger/RecordTriggerController.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 63c3f87958..915db7fe44 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -120,10 +120,6 @@ class RecordTriggerControllerImpl @Inject constructor( } is KMGamePadEvent -> { - if (isEvdevRecordingEnabled) { - return false - } - val dpadKeyEvents = dpadMotionEventTracker.convertMotionEvent(event) for (keyEvent in dpadKeyEvents) { @@ -140,10 +136,6 @@ class RecordTriggerControllerImpl @Inject constructor( } is KMKeyEvent -> { - if (isEvdevRecordingEnabled) { - return false - } - val matchingDownEvent: KMKeyEvent? = downKeyEvents.find { it.keyCode == event.keyCode && it.scanCode == event.scanCode && From 93ec20fbf6457fa54c23bc6937dcbdb0d6095366 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 2 Sep 2025 22:22:17 +0200 Subject: [PATCH 205/215] #1394 WIP: use system bridge for Bluetooth actions --- .../keymapper/base/actions/ActionUtils.kt | 14 +++++------ .../base/constraints/ConstraintSnapshot.kt | 15 +++-------- .../sysbridge/service/SystemBridge.kt | 2 ++ .../system/network/AndroidNetworkAdapter.kt | 25 ++++++++++--------- .../system/network/NetworkAdapter.kt | 1 - 5 files changed, 25 insertions(+), 32 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index 5831c9c05b..1741a6e22b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -528,12 +528,6 @@ object ActionUtils { // The global action still fails even though the API exists in SDK 34. ActionId.COLLAPSE_STATUS_BAR -> Build.VERSION_CODES.TIRAMISU - // TODO support system bridge - ActionId.ENABLE_BLUETOOTH, - ActionId.DISABLE_BLUETOOTH, - ActionId.TOGGLE_BLUETOOTH, - -> Build.VERSION_CODES.S_V2 - // See https://issuetracker.google.com/issues/225186417. The global action // is not marked as deprecated even though it doesn't work. ActionId.TOGGLE_SPLIT_SCREEN -> Build.VERSION_CODES.S @@ -605,6 +599,11 @@ object ActionUtils { ActionId.DISABLE_AIRPLANE_MODE, -> true + ActionId.TOGGLE_BLUETOOTH, + ActionId.ENABLE_BLUETOOTH, + ActionId.DISABLE_BLUETOOTH, + -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 + ActionId.POWER_ON_OFF_DEVICE -> true else -> false @@ -722,7 +721,8 @@ object ActionUtils { ActionId.PHONE_CALL -> return listOf(Permission.CALL_PHONE) ActionId.ENABLE_BLUETOOTH, ActionId.DISABLE_BLUETOOTH, ActionId.TOGGLE_BLUETOOTH -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On S_V2 and newer, the system bridge is used which means no permissions are required + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) { return listOf(Permission.FIND_NEARBY_DEVICES) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt index e5803e9b0a..39fb58faf4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt @@ -1,7 +1,6 @@ package io.github.sds100.keymapper.base.constraints import android.media.AudioManager -import android.os.Build import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.firstBlocking @@ -41,25 +40,17 @@ class LazyConstraintSnapshot( private val appsPlayingMedia: List by lazy { mediaAdapter.getActiveMediaSessionPackages() } private val audioVolumeStreams: Set by lazy { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mediaAdapter.getActiveAudioVolumeStreams() - } else { - emptySet() - } + mediaAdapter.getActiveAudioVolumeStreams() } private val isWifiEnabled: Boolean by lazy { networkAdapter.isWifiEnabled() } - private val connectedWifiSSID: String? by lazy { networkAdapter.connectedWifiSSID } + private val connectedWifiSSID: String? by lazy { networkAdapter.connectedWifiSSIDFlow.firstBlocking() } private val chosenImeId: String? by lazy { inputMethodAdapter.chosenIme.value?.id } private val callState: CallState by lazy { phoneAdapter.getCallState() } private val isCharging: Boolean by lazy { powerAdapter.isCharging.value } private val isLocked: Boolean by lazy { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - lockScreenAdapter.isLocked() - } else { - false - } + lockScreenAdapter.isLocked() } private val isLockscreenShowing: Boolean by lazy { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 21ef30790c..f1d7479f05 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -175,10 +175,12 @@ internal class SystemBridge : ISystemBridge.Stub() { inputManager = IInputManager.Stub.asInterface(ServiceManager.getService(Context.INPUT_SERVICE)) + // TODO check that system supports wifi feature waitSystemService(Context.WIFI_SERVICE) wifiManager = IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) + // TODO check that the system supports telephony feature waitSystemService(Context.TELEPHONY_SERVICE) telephonyManager = ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index c174ba639c..c59cb2041b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -62,22 +62,13 @@ class AndroidNetworkAdapter @Inject constructor( } WifiManager.NETWORK_STATE_CHANGED_ACTION -> { - connectedWifiSSIDFlow.update { connectedWifiSSID } + connectedWifiSSIDFlow.update { getWifiSSID() } } } } } - override val connectedWifiSSID: String? - get() = wifiManager.connectionInfo?.ssid?.let { ssid -> - if (ssid == WifiManager.UNKNOWN_SSID) { - null - } else { - ssid.removeSurrounding("\"") - } - } - - override val connectedWifiSSIDFlow = MutableStateFlow(connectedWifiSSID) + override val connectedWifiSSIDFlow = MutableStateFlow(getWifiSSID()) override val isWifiConnected: MutableStateFlow = MutableStateFlow(getIsWifiConnected()) private val isWifiEnabled = MutableStateFlow(isWifiEnabled()) @@ -264,8 +255,18 @@ class AndroidNetworkAdapter @Inject constructor( } } + private fun getWifiSSID(): String? { + return wifiManager.connectionInfo?.ssid?.let { ssid -> + if (ssid == WifiManager.UNKNOWN_SSID) { + null + } else { + ssid.removeSurrounding("\"") + } + } + } + fun invalidateState() { - connectedWifiSSIDFlow.update { connectedWifiSSID } + connectedWifiSSIDFlow.update { getWifiSSID() } isWifiConnected.update { getIsWifiConnected() } } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt index 62480ffa9c..f7f8235510 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow interface NetworkAdapter { - val connectedWifiSSID: String? val connectedWifiSSIDFlow: Flow val isWifiConnected: Flow From fd69e5756041f47e1678d57a9c7cdb62add61eed Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 6 Sep 2025 13:47:16 +0200 Subject: [PATCH 206/215] #1394 use system bridge for Bluetooth actions on Android 13+ --- .../keymapper/sysbridge/ISystemBridge.aidl | 2 + .../sysbridge/service/SystemBridge.kt | 91 ++++++++++++++++--- .../bluetooth/AndroidBluetoothAdapter.kt | 22 +++-- .../android/bluetooth/IBluetoothManager.aidl | 9 ++ .../android/content/pm/IPackageManager.aidl | 1 + 5 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 systemstubs/src/main/aidl/android/bluetooth/IBluetoothManager.aidl diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 969254edaa..64d5fba755 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -29,4 +29,6 @@ interface ISystemBridge { void grantPermission(String permission, int deviceId) = 11; void setDataEnabled(int subId, boolean enable) = 12; + + void setBluetoothEnabled(boolean enable) = 13; } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index f1d7479f05..c69f73b058 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -1,9 +1,13 @@ package io.github.sds100.keymapper.sysbridge.service import android.annotation.SuppressLint +import android.bluetooth.IBluetoothManager +import android.content.AttributionSource import android.content.Context import android.content.IContentProvider import android.content.pm.ApplicationInfo +import android.content.pm.IPackageManager +import android.content.pm.PackageManager import android.hardware.input.IInputManager import android.net.wifi.IWifiManager import android.os.Build @@ -78,9 +82,16 @@ internal class SystemBridge : ISystemBridge.Stub() { } private fun waitSystemService(name: String?) { + var count = 0 + while (ServiceManager.getService(name) == null) { + if (count == 5) { + throw IllegalStateException("Failed to get $name system service") + } + try { Thread.sleep(1000) + count++ } catch (e: InterruptedException) { Log.w(TAG, e.message, e) } @@ -146,9 +157,11 @@ internal class SystemBridge : ISystemBridge.Stub() { } private val inputManager: IInputManager - private val wifiManager: IWifiManager + private val wifiManager: IWifiManager? private val permissionManager: IPermissionManager - private val telephonyManager: ITelephony + private val telephonyManager: ITelephony? + private val packageManager: IPackageManager + private val bluetoothManager: IBluetoothManager? private val processPackageName: String = when (Process.myUid()) { Process.ROOT_UID -> "root" @@ -161,9 +174,11 @@ internal class SystemBridge : ISystemBridge.Stub() { @SuppressLint("UnsafeDynamicallyLoadedCode") System.load("$libraryPath/libevdev.so") - Log.i(TAG, "SystemBridge started. Version code $versionCode") + Log.i(TAG, "SystemBridge starting... Version code $versionCode") waitSystemService("package") + packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) + waitSystemService(Context.ACTIVITY_SERVICE) waitSystemService(Context.USER_SERVICE) waitSystemService(Context.APP_OPS_SERVICE) @@ -175,15 +190,29 @@ internal class SystemBridge : ISystemBridge.Stub() { inputManager = IInputManager.Stub.asInterface(ServiceManager.getService(Context.INPUT_SERVICE)) - // TODO check that system supports wifi feature - waitSystemService(Context.WIFI_SERVICE) - wifiManager = - IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) + if (hasSystemFeature(PackageManager.FEATURE_WIFI)) { + waitSystemService(Context.WIFI_SERVICE) + wifiManager = + IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) + } else { + wifiManager = null + } - // TODO check that the system supports telephony feature - waitSystemService(Context.TELEPHONY_SERVICE) - telephonyManager = - ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)) + if (hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { + waitSystemService(Context.TELEPHONY_SERVICE) + telephonyManager = + ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)) + } else { + telephonyManager = null + } + + if (hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) { + waitSystemService("bluetooth_manager") + bluetoothManager = + IBluetoothManager.Stub.asInterface(ServiceManager.getService("bluetooth_manager")) + } else { + bluetoothManager = null + } val applicationInfo = getKeyMapperPackageInfo() @@ -197,6 +226,12 @@ internal class SystemBridge : ISystemBridge.Stub() { mainHandler.post { sendBinderToApp() } + + Log.i(TAG, "SystemBridge started complete. Version code $versionCode") + } + + private fun hasSystemFeature(name: String): Boolean { + return packageManager.hasSystemFeature(name, 0) } private fun getKeyMapperPackageInfo(): ApplicationInfo? = @@ -313,6 +348,10 @@ internal class SystemBridge : ISystemBridge.Stub() { } override fun setWifiEnabled(enable: Boolean): Boolean { + if (wifiManager == null) { + throw UnsupportedOperationException("WiFi not supported") + } + return wifiManager.setWifiEnabled(processPackageName, enable) } @@ -455,6 +494,10 @@ internal class SystemBridge : ISystemBridge.Stub() { } override fun setDataEnabled(subId: Int, enable: Boolean) { + if (telephonyManager == null) { + throw UnsupportedOperationException("Telephony not supported") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { telephonyManager.setDataEnabledForReason( subId, @@ -466,4 +509,30 @@ internal class SystemBridge : ISystemBridge.Stub() { telephonyManager.setUserDataEnabled(subId, enable) } } + + override fun setBluetoothEnabled(enable: Boolean) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + throw UnsupportedOperationException("Bluetooth enable/disable requires Android 12 or higher. Otherwise use the SDK's BluetoothAdapter which allows enable/disable.") + } + + if (bluetoothManager == null) { + throw UnsupportedOperationException("Bluetooth not supported") + } + + val attributionSourceBuilder = AttributionSource.Builder(Process.myUid()) + .setAttributionTag("KeyMapperSystemBridge") + .setPackageName(processPackageName) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + attributionSourceBuilder.setPid(Process.myPid()) + } + + val attributionSource = attributionSourceBuilder.build() + + if (enable) { + bluetoothManager.enable(attributionSource) + } else { + bluetoothManager.disable(attributionSource, true) + } + } } \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt index 060bc13655..838a455be7 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt @@ -8,12 +8,14 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.os.Build import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -26,6 +28,7 @@ import javax.inject.Singleton class AndroidBluetoothAdapter @Inject constructor( @ApplicationContext private val context: Context, private val coroutineScope: CoroutineScope, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager ) : io.github.sds100.keymapper.system.bluetooth.BluetoothAdapter { private val bluetoothManager: BluetoothManager? = context.getSystemService() @@ -45,6 +48,7 @@ class AndroidBluetoothAdapter @Inject constructor( onReceiveIntent(intent) } } + init { IntentFilter().apply { // these broadcasts can't be received from a manifest declared receiver on Android 8.0+ @@ -65,7 +69,6 @@ class AndroidBluetoothAdapter @Inject constructor( fun onReceiveIntent(intent: Intent) { when (intent.action) { BluetoothDevice.ACTION_ACL_CONNECTED -> { - val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) ?: return @@ -144,9 +147,13 @@ class AndroidBluetoothAdapter @Inject constructor( return KMError.SystemFeatureNotSupported(PackageManager.FEATURE_BLUETOOTH) } - adapter.enable() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) { + return systemBridgeConnectionManager.run { bridge -> bridge.setBluetoothEnabled(true) } + } else { + adapter.enable() + return Success(Unit) + } - return Success(Unit) } override fun disable(): KMResult<*> { @@ -154,8 +161,11 @@ class AndroidBluetoothAdapter @Inject constructor( return KMError.SystemFeatureNotSupported(PackageManager.FEATURE_BLUETOOTH) } - adapter.disable() - - return Success(Unit) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) { + return systemBridgeConnectionManager.run { bridge -> bridge.setBluetoothEnabled(false) } + } else { + adapter.disable() + return Success(Unit) + } } } diff --git a/systemstubs/src/main/aidl/android/bluetooth/IBluetoothManager.aidl b/systemstubs/src/main/aidl/android/bluetooth/IBluetoothManager.aidl new file mode 100644 index 0000000000..f4d1a4e530 --- /dev/null +++ b/systemstubs/src/main/aidl/android/bluetooth/IBluetoothManager.aidl @@ -0,0 +1,9 @@ +package android.bluetooth; +import android.content.AttributionSource; + +interface IBluetoothManager { + // Requires Android 13+ + boolean enable(in AttributionSource attributionSource); + // Requires Android 13+ + boolean disable(in AttributionSource attributionSource, boolean persist); +} \ No newline at end of file diff --git a/systemstubs/src/main/aidl/android/content/pm/IPackageManager.aidl b/systemstubs/src/main/aidl/android/content/pm/IPackageManager.aidl index 51f2382d8b..23f67f7321 100644 --- a/systemstubs/src/main/aidl/android/content/pm/IPackageManager.aidl +++ b/systemstubs/src/main/aidl/android/content/pm/IPackageManager.aidl @@ -2,4 +2,5 @@ package android.content.pm; interface IPackageManager { void grantRuntimePermission(String packageName, String permissionName, int userId); + boolean hasSystemFeature(String name, int version); } \ No newline at end of file From 1030ece454ebef63567a1f0e1604b60cdeecc271 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 6 Sep 2025 17:55:38 +0200 Subject: [PATCH 207/215] replace Wi-Fi with WiFi in strings so it is easier to search for the text in the UI --- base/src/main/res/values/strings.xml | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 236dc21158..e4728f4c70 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -151,7 +151,7 @@ Description for Key Mapper (required) Flags Sound file description - Wi-Fi network SSID + WiFi network SSID @@ -259,18 +259,18 @@ Flashlight is on Flashlight is off - Wi-Fi is on - Wi-Fi is off - Connected to a Wi-Fi network - Disconnected from a Wi-Fi network - You will have to type the SSID manually because apps aren\'t allowed to query the list of known Wi-Fi networks on Android 10 and newer. + WiFi is on + WiFi is off + Connected to a WiFi network + Disconnected from a WiFi network + You will have to type the SSID manually because apps aren\'t allowed to query the list of known WiFi networks on Android 10 and newer. - Leave it empty if any Wi-Fi network should be matched. + Leave it empty if any WiFi network should be matched. Any - Connected to %s Wi-Fi - Disconnected from %s Wi-Fi - Connected to any Wi-Fi - Disconnected to no Wi-Fi + Connected to %s WiFi + Disconnected from %s WiFi + Connected to any WiFi + Disconnected to no WiFi Input method is chosen %s is chosen @@ -804,7 +804,7 @@ Your device doesn\'t have a camera. Your device doesn\'t support NFC. Your device doesn\'t have a fingerprint reader. - Your device doesn\'t support Wi-Fi. + Your device doesn\'t support WiFi. Your device doesn\'t support Bluetooth. Your device doesn\'t support device policy enforcement. Your device doesn\'t have a camera flash. @@ -902,9 +902,9 @@ - Toggle Wi-Fi - Enable Wi-Fi - Disable Wi-Fi + Toggle WiFi + Enable WiFi + Disable WiFi Toggle Bluetooth Enable Bluetooth @@ -1613,8 +1613,8 @@ Enable developer options Key Mapper needs to use Android Debug Bridge to start PRO mode, and you need to enable developer options for that. - Connect to a Wi-Fi network - Key Mapper needs a Wi-Fi network to enable ADB. You do not need an internet connection.\n\nNo Wi-Fi network? Use a hotspot from someone else\'s phone. + Connect to a WiFi network + Key Mapper needs a WiFi network to enable ADB. You do not need an internet connection.\n\nNo WiFi network? Use a hotspot from someone else\'s phone. Enable wireless debugging Key Mapper uses wireless debugging to launch its remapping and input service. @@ -1651,7 +1651,7 @@ Auto starting PRO mode Using root Using shizuku - Using ADB over Wi-Fi + Using ADB over WiFi PRO mode started Have fun remapping! ❤️ @@ -1661,7 +1661,7 @@ Input pairing code What can I do with PRO mode? - 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: Wi-Fi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device. + 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: WiFi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device. Show PRO mode info Dismiss From 84e3cef1b6b8263d1dcd4d0cdfaa96b6bd6abd76 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 7 Sep 2025 12:27:19 +0200 Subject: [PATCH 208/215] #1394 use system bridge for NFC actions --- CHANGELOG.md | 1 + .../keymapper/sysbridge/ISystemBridge.aidl | 2 ++ .../sds100/keymapper/sysbridge/adb/AdbMdns.kt | 4 +-- .../sysbridge/service/SystemBridge.kt | 27 +++++++++++++++++++ .../keymapper/system/nfc/AndroidNfcAdapter.kt | 25 +++++++++++++---- .../main/java/android/nfc/INfcAdapter.java | 24 +++++++++++++++++ .../main/java/android/nfc/NfcAdapterApis.kt | 21 +++++++++++++++ 7 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 systemstubs/src/main/java/android/nfc/INfcAdapter.java create mode 100644 systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index c680a39a41..70a9a66c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - #761 Detect keys with scancodes. Key Mapper will do this automatically if the key code is unknown or you record different physical keys from the same device with the same key code. +- Redesign the Settings screen ## Removed diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index 64d5fba755..b6f3c4f603 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -31,4 +31,6 @@ interface ISystemBridge { void setDataEnabled(int subId, boolean enable) = 12; void setBluetoothEnabled(boolean enable) = 13; + + void setNfcEnabled(boolean enable) = 14; } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt index 5a6b46c5f3..3a3fbf868c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt @@ -124,9 +124,7 @@ internal class AdbMdns( private suspend fun discoverPortInternal(): Int? { var port: Int? = null - if (isDiscovering.value) { - cleanup() - } + cleanup() // Wait for it to stop discovering isDiscovering.first { !it } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index c69f73b058..91ea9ed34f 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -10,6 +10,8 @@ import android.content.pm.IPackageManager import android.content.pm.PackageManager import android.hardware.input.IInputManager import android.net.wifi.IWifiManager +import android.nfc.INfcAdapter +import android.nfc.NfcAdapterApis import android.os.Build import android.os.Bundle import android.os.Handler @@ -162,6 +164,7 @@ internal class SystemBridge : ISystemBridge.Stub() { private val telephonyManager: ITelephony? private val packageManager: IPackageManager private val bluetoothManager: IBluetoothManager? + private val nfcAdapter: INfcAdapter? private val processPackageName: String = when (Process.myUid()) { Process.ROOT_UID -> "root" @@ -214,6 +217,14 @@ internal class SystemBridge : ISystemBridge.Stub() { bluetoothManager = null } + if (hasSystemFeature(PackageManager.FEATURE_NFC)) { + waitSystemService(Context.NFC_SERVICE) + nfcAdapter = + INfcAdapter.Stub.asInterface(ServiceManager.getService(Context.NFC_SERVICE)) + } else { + nfcAdapter = null + } + val applicationInfo = getKeyMapperPackageInfo() if (applicationInfo == null) { @@ -535,4 +546,20 @@ internal class SystemBridge : ISystemBridge.Stub() { bluetoothManager.disable(attributionSource, true) } } + + override fun setNfcEnabled(enable: Boolean) { + if (nfcAdapter == null) { + throw UnsupportedOperationException("NFC not supported") + } + + if (enable) { + NfcAdapterApis.enable(nfcAdapter, processPackageName) + } else { + NfcAdapterApis.disable( + adapter = nfcAdapter, + saveState = true, + packageName = processPackageName + ) + } + } } \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt index 0f11120f17..4d43e26e4c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt @@ -2,10 +2,12 @@ package io.github.sds100.keymapper.system.nfc import android.content.Context import android.nfc.NfcManager +import android.os.Build import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -13,14 +15,27 @@ import javax.inject.Singleton class AndroidNfcAdapter @Inject constructor( @ApplicationContext private val context: Context, private val suAdapter: SuAdapter, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager ) : NfcAdapter { private val ctx = context.applicationContext - private val nfcManager: NfcManager by lazy { ctx.getSystemService()!! } + private val nfcManager: NfcManager? by lazy { ctx.getSystemService() } - override fun isEnabled(): Boolean = nfcManager.defaultAdapter.isEnabled + override fun isEnabled(): Boolean = nfcManager?.defaultAdapter?.isEnabled ?: false - override fun enable(): KMResult<*> = suAdapter.execute("svc nfc enable") + override fun enable(): KMResult<*> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return systemBridgeConnectionManager.run { bridge -> bridge.setNfcEnabled(true) } + } else { + return suAdapter.execute("svc nfc enable") + } + } - override fun disable(): KMResult<*> = suAdapter.execute("svc nfc disable") + override fun disable(): KMResult<*> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return systemBridgeConnectionManager.run { bridge -> bridge.setNfcEnabled(false) } + } else { + return suAdapter.execute("svc nfc disable") + } + } } diff --git a/systemstubs/src/main/java/android/nfc/INfcAdapter.java b/systemstubs/src/main/java/android/nfc/INfcAdapter.java new file mode 100644 index 0000000000..2e4b958915 --- /dev/null +++ b/systemstubs/src/main/java/android/nfc/INfcAdapter.java @@ -0,0 +1,24 @@ +package android.nfc; + +import android.os.Build; +import android.os.IBinder; + +import androidx.annotation.RequiresApi; + +public interface INfcAdapter extends android.os.IInterface { + boolean enable(); + + boolean disable(boolean saveState); + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + boolean enable(String pkg); + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + boolean disable(boolean saveState, String pkg); + + abstract class Stub extends android.os.Binder implements INfcAdapter { + public static INfcAdapter asInterface(IBinder obj) { + throw new RuntimeException("Stub!"); + } + } +} \ No newline at end of file diff --git a/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt b/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt new file mode 100644 index 0000000000..3f7dce07c9 --- /dev/null +++ b/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt @@ -0,0 +1,21 @@ +package android.nfc + +import android.os.Build + +object NfcAdapterApis { + fun enable(adapter: INfcAdapter, packageName: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + adapter.enable(packageName) + } else { + adapter.enable() + } + } + + fun disable(adapter: INfcAdapter, saveState: Boolean, packageName: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + adapter.disable(saveState, packageName) + } else { + adapter.disable(saveState) + } + } +} \ No newline at end of file From fd032c8b513df003ba50b69c434217321a71bd67 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 7 Sep 2025 13:04:26 +0200 Subject: [PATCH 209/215] #1394 use system bridge for airplane mode actions --- .../keymapper/sysbridge/ISystemBridge.aidl | 2 + .../sysbridge/service/SystemBridge.kt | 14 +++++++ .../airplanemode/AirplaneModeAdapter.kt | 4 +- .../AndroidAirplaneModeAdapter.kt | 41 ++++++++++++++----- .../android/net/IConnectivityManager.aidl | 5 +++ 5 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 systemstubs/src/main/aidl/android/net/IConnectivityManager.aidl diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index b6f3c4f603..d6a91ee54f 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -33,4 +33,6 @@ interface ISystemBridge { void setBluetoothEnabled(boolean enable) = 13; void setNfcEnabled(boolean enable) = 14; + + void setAirplaneMode(boolean enable) = 15; } \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 91ea9ed34f..7dc83038a1 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -9,6 +9,7 @@ import android.content.pm.ApplicationInfo import android.content.pm.IPackageManager import android.content.pm.PackageManager import android.hardware.input.IInputManager +import android.net.IConnectivityManager import android.net.wifi.IWifiManager import android.nfc.INfcAdapter import android.nfc.NfcAdapterApis @@ -165,6 +166,7 @@ internal class SystemBridge : ISystemBridge.Stub() { private val packageManager: IPackageManager private val bluetoothManager: IBluetoothManager? private val nfcAdapter: INfcAdapter? + private val connectivityManager: IConnectivityManager? private val processPackageName: String = when (Process.myUid()) { Process.ROOT_UID -> "root" @@ -225,6 +227,10 @@ internal class SystemBridge : ISystemBridge.Stub() { nfcAdapter = null } + waitSystemService(Context.CONNECTIVITY_SERVICE) + connectivityManager = + IConnectivityManager.Stub.asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)) + val applicationInfo = getKeyMapperPackageInfo() if (applicationInfo == null) { @@ -562,4 +568,12 @@ internal class SystemBridge : ISystemBridge.Stub() { ) } } + + override fun setAirplaneMode(enable: Boolean) { + if (connectivityManager == null) { + throw UnsupportedOperationException("ConnectivityManager not supported") + } + + connectivityManager.setAirplaneMode(enable) + } } \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AirplaneModeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AirplaneModeAdapter.kt index 6c38f9f3a4..bc0bfcfc11 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AirplaneModeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AirplaneModeAdapter.kt @@ -4,6 +4,6 @@ import io.github.sds100.keymapper.common.utils.KMResult interface AirplaneModeAdapter { fun isEnabled(): Boolean - fun enable(): KMResult<*> - fun disable(): KMResult<*> + suspend fun enable(): KMResult<*> + suspend fun disable(): KMResult<*> } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt index 4cba36171f..d44b2cde60 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt @@ -1,31 +1,52 @@ package io.github.sds100.keymapper.system.airplanemode import android.content.Context +import android.os.Build import android.provider.Settings import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils -import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter import javax.inject.Inject import javax.inject.Singleton @Singleton class AndroidAirplaneModeAdapter @Inject constructor( - @ApplicationContext private val context: Context, - val suAdapter: SuAdapter, + @ApplicationContext private val ctx: Context, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val suAdapter: SuAdapter ) : AirplaneModeAdapter { - private val ctx = context.applicationContext - override fun enable(): KMResult<*> = - suAdapter.execute("settings put global airplane_mode_on 1").onSuccess { - broadcastAirplaneModeChanged(false) + override suspend fun enable(): KMResult<*> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeConnectionManager.run { bridge -> bridge.setAirplaneMode(true) } + } else { + val success = SettingsUtils.putGlobalSetting(ctx, Settings.Global.AIRPLANE_MODE_ON, 1) + broadcastAirplaneModeChanged(true) + if (success) { + Success(Unit) + } else { + KMError.FailedToModifySystemSetting(Settings.Global.AIRPLANE_MODE_ON) + } } + } - override fun disable(): KMResult<*> = - suAdapter.execute("settings put global airplane_mode_on 0").onSuccess { - broadcastAirplaneModeChanged(false) + override suspend fun disable(): KMResult<*> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeConnectionManager.run { bridge -> bridge.setAirplaneMode(false) } + } else { + val success = SettingsUtils.putGlobalSetting(ctx, Settings.Global.AIRPLANE_MODE_ON, 0) + if (success) { + broadcastAirplaneModeChanged(false) + Success(Unit) + } else { + KMError.FailedToModifySystemSetting(Settings.Global.AIRPLANE_MODE_ON) + } } + } override fun isEnabled(): Boolean = SettingsUtils.getGlobalSetting(ctx, Settings.Global.AIRPLANE_MODE_ON) == 1 diff --git a/systemstubs/src/main/aidl/android/net/IConnectivityManager.aidl b/systemstubs/src/main/aidl/android/net/IConnectivityManager.aidl new file mode 100644 index 0000000000..6402a6e148 --- /dev/null +++ b/systemstubs/src/main/aidl/android/net/IConnectivityManager.aidl @@ -0,0 +1,5 @@ +package android.net; + +interface IConnectivityManager { + void setAirplaneMode(boolean enable); +} \ No newline at end of file From 522696f15677ce7f9bfc336d7894cc957e386ef9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 7 Sep 2025 13:11:00 +0200 Subject: [PATCH 210/215] #1394 use system bridge for power on/off action --- .../base/actions/PerformActionsUseCase.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 1fb6b8ac9c..ca975e24b1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -37,6 +37,8 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.system.airplanemode.AirplaneModeAdapter import io.github.sds100.keymapper.system.apps.AppShortcutAdapter import io.github.sds100.keymapper.system.apps.PackageManagerAdapter @@ -47,6 +49,7 @@ import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.files.FileAdapter import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.Scancode import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.intents.IntentAdapter import io.github.sds100.keymapper.system.intents.IntentTarget @@ -102,6 +105,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( private val ringtoneAdapter: RingtoneAdapter, private val settingsRepository: PreferenceRepository, private val inputEventHub: InputEventHub, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager ) : PerformActionsUseCase { @AssistedFactory @@ -785,7 +789,22 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.ScreenOnOff -> { - result = suAdapter.execute("input keyevent ${KeyEvent.KEYCODE_POWER}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + systemBridgeConnectionManager.connectionState.value is SystemBridgeConnectionState.Connected + ) { + val model = InjectKeyEventModel( + keyCode = KeyEvent.KEYCODE_POWER, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = -1, + scanCode = Scancode.KEY_POWER, + source = InputDevice.SOURCE_UNKNOWN + ) + result = inputEventHub.injectKeyEvent(model) + .then { inputEventHub.injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } + } else { + result = suAdapter.execute("input keyevent ${KeyEvent.KEYCODE_POWER}") + } } is ActionData.SecureLock -> { From 3cab7a1d634273bfcf5827127c06eb08d075f33b Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 7 Sep 2025 14:05:11 +0200 Subject: [PATCH 211/215] #1394 automatically set power trigger key to long press --- .../base/trigger/ConfigTriggerDelegate.kt | 65 ++++++-- .../base/actions/GetActionErrorUseCaseTest.kt | 3 +- .../base/actions/PerformActionsUseCaseTest.kt | 1 + .../base/trigger/ConfigTriggerDelegateTest.kt | 140 ++++++++++++++++++ 4 files changed, 196 insertions(+), 13 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index 3fe2237faf..65846ee9a4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -1,10 +1,12 @@ package io.github.sds100.keymapper.base.trigger +import android.view.KeyEvent import io.github.sds100.keymapper.base.floating.FloatingButtonData import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.Scancode /** * This extracts the core logic when configuring a trigger which makes it easier to write tests. @@ -56,6 +58,13 @@ class ConfigTriggerDelegate { return addTriggerKey(trigger, triggerKey) } + private fun isPowerButtonKey(keyCode: Int, scanCode: Int): Boolean { + return keyCode == KeyEvent.KEYCODE_POWER || + keyCode == KeyEvent.KEYCODE_TV_POWER || + scanCode == Scancode.KEY_POWER || + scanCode == Scancode.KEY_POWER2 + } + /** * @param otherTriggerKeys This needs to check the other triggers in the app so that it can * enable scancode detection by default in some situations. @@ -68,10 +77,16 @@ class ConfigTriggerDelegate { requiresIme: Boolean, otherTriggerKeys: List = emptyList() ): Trigger { - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS + val isPowerKey = isPowerButtonKey(keyCode, scanCode) + + val clickType = if (isPowerKey) { + ClickType.LONG_PRESS + } else { + when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } } var consumeKeyEvent = true @@ -101,9 +116,19 @@ class ConfigTriggerDelegate { detectWithScanCodeUserSetting = logicallyEqualKeys.isNotEmpty() ) - val newKeys = trigger.keys.filter { it !is EvdevTriggerKey } + var newKeys = trigger.keys.filter { it !is EvdevTriggerKey } + + if (isPowerKey && trigger.mode is TriggerMode.Parallel) { + newKeys = newKeys.map { it.setClickType(ClickType.LONG_PRESS) } + } + + val newMode = if (isPowerKey && trigger.mode is TriggerMode.Parallel) { + TriggerMode.Parallel(ClickType.LONG_PRESS) + } else { + trigger.mode + } - return addTriggerKey(trigger.copy(keys = newKeys), triggerKey) + return addTriggerKey(trigger.copy(mode = newMode, keys = newKeys), triggerKey) } fun addEvdevTriggerKey( @@ -113,10 +138,16 @@ class ConfigTriggerDelegate { device: EvdevDeviceInfo, otherTriggerKeys: List = emptyList() ): Trigger { - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS + val isPowerKey = isPowerButtonKey(keyCode, scanCode) + + val clickType = if (isPowerKey) { + ClickType.LONG_PRESS + } else { + when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } } // Scan code detection should be turned on by default if there are other @@ -138,9 +169,19 @@ class ConfigTriggerDelegate { detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty() ) - val newKeys = trigger.keys.filter { it !is KeyEventTriggerKey } + var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey } + + if (isPowerKey && trigger.mode is TriggerMode.Parallel) { + newKeys = newKeys.map { it.setClickType(ClickType.LONG_PRESS) } + } + + val newMode = if (isPowerKey && trigger.mode is TriggerMode.Parallel) { + TriggerMode.Parallel(ClickType.LONG_PRESS) + } else { + trigger.mode + } - return addTriggerKey(trigger.copy(keys = newKeys), triggerKey) + return addTriggerKey(trigger.copy(mode = newMode, keys = newKeys), triggerKey) } private fun addTriggerKey( diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt index b01a5f3724..4047760af2 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt @@ -69,9 +69,10 @@ class GetActionErrorUseCaseTest { systemFeatureAdapter = mock(), cameraAdapter = mock(), soundsManager = mock(), - shizukuAdapter = mockShizukuAdapter, ringtoneAdapter = mock(), buildConfigProvider = TestBuildConfigProvider(), + systemBridgeConnectionManager = mock(), + preferenceRepository = mock() ) } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index 8c22c9788e..e65a0ace00 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -80,6 +80,7 @@ class PerformActionsUseCaseTest { notificationReceiverAdapter = mock(), ringtoneAdapter = mock(), inputEventHub = mockInputEventHub, + systemBridgeConnectionManager = mock() ) } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt index d9f0627656..e5302b0b6e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt @@ -39,6 +39,146 @@ class ConfigTriggerDelegateTest { mockedKeyEvent.close() } + @Test + fun `set click type to long press when adding power button by key event to empty trigger`() { + val trigger = Trigger() + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding KEY_POWER by evdev event to empty trigger`() { + val trigger = Trigger() + val device = EvdevDeviceInfo( + name = "Power Button", + bus = 0, + vendor = 1, + product = 2, + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = device, + ) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding KEY_POWER2 by evdev event to empty trigger`() { + val trigger = Trigger() + val device = EvdevDeviceInfo( + name = "Power Button", + bus = 0, + vendor = 1, + product = 2, + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER2, + device = device, + ) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding TV power button by evdev event to empty trigger`() { + val trigger = Trigger() + val device = EvdevDeviceInfo( + name = "TV Remote", + bus = 0, + vendor = 1, + product = 2, + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_TV_POWER, + scanCode = Scancode.KEY_POWER, + device = device, + ) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding power button to parallel trigger`() { + val trigger = parallelTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEUP, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + assertThat(newTrigger.keys, hasSize(3)) + assertThat((newTrigger.mode as TriggerMode.Parallel).clickType, `is`(ClickType.LONG_PRESS)) + assertThat(newTrigger.keys[2].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding power button to sequence trigger`() { + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true + ), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ) + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + assertThat(newTrigger.keys, hasSize(3)) + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + assertThat(newTrigger.keys[2].clickType, `is`(ClickType.LONG_PRESS)) + } + @Test fun `Remove keys with the same scan code if scan code detection is enabled when switching to a parallel trigger`() { val key = KeyEventTriggerKey( From ac47d21abe81cc217559801dab6bd9a84f4568d1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 7 Sep 2025 15:16:37 +0200 Subject: [PATCH 212/215] #1394 remove detect screen off option and use shell for key event actions --- .../keymapper/base/actions/ActionData.kt | 2 - .../base/actions/ActionDataEntityMapper.kt | 15 ----- .../base/actions/ActionErrorSnapshot.kt | 7 --- .../keymapper/base/actions/ActionUiHelper.kt | 63 +++++++++---------- .../keymapper/base/actions/ActionUtils.kt | 4 +- .../base/actions/ChooseActionViewModel.kt | 1 - .../keyevent/ConfigKeyEventActionViewModel.kt | 14 +---- .../constraints/ChooseConstraintViewModel.kt | 35 +++-------- .../base/detection/DetectKeyMapsUseCase.kt | 11 ---- .../base/home/KeyMapListItemCreator.kt | 4 -- .../keymapper/base/home/KeyMapListScreen.kt | 1 - .../keymaps/ConfigKeyMapOptionsViewModel.kt | 10 --- .../base/keymaps/DisplayKeyMapUseCase.kt | 5 -- .../base/keymaps/KeyMapOptionsScreen.kt | 16 ----- .../comparators/KeyMapOptionsComparator.kt | 1 - .../base/trigger/ConfigTriggerDelegate.kt | 4 -- .../base/trigger/ConfigTriggerUseCase.kt | 7 --- .../sds100/keymapper/base/trigger/Trigger.kt | 16 ----- .../keymapper/base/trigger/TriggerError.kt | 1 - .../base/trigger/TriggerErrorSnapshot.kt | 10 +-- .../base/trigger/TriggerKeyListItem.kt | 1 - .../res/layout/fragment_config_key_event.xml | 18 +----- base/src/main/res/values/strings.xml | 6 -- .../keymapper/data/entities/ActionEntity.kt | 3 +- .../keymapper/data/entities/TriggerEntity.kt | 3 + .../service/SystemBridgeSetupController.kt | 1 - 26 files changed, 48 insertions(+), 211 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt index 514367a755..69ea62085f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt @@ -48,8 +48,6 @@ sealed class ActionData : Comparable { data class InputKeyEvent( val keyCode: Int, val metaState: Int = 0, - // TODO remove this option - val useShell: Boolean = false, val device: Device? = null, ) : ActionData() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt index d9c0dd80cc..8466da44d6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt @@ -79,11 +79,6 @@ object ActionDataEntityMapper { entity.extras.getData(ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME) .valueOrNull() ?: "" - val useShell = - entity.extras.getData(ActionEntity.EXTRA_KEY_EVENT_USE_SHELL).then { - (it == "true").success() - }.valueOrNull() ?: false - val device = if (deviceDescriptor != null) { ActionData.InputKeyEvent.Device(deviceDescriptor, deviceName) } else { @@ -93,7 +88,6 @@ object ActionDataEntityMapper { ActionData.InputKeyEvent( keyCode = entity.data.toInt(), metaState = metaState, - useShell = useShell, device = device, ) } @@ -724,15 +718,6 @@ object ActionDataEntityMapper { ) is ActionData.InputKeyEvent -> sequence { - if (data.useShell) { - val string = if (data.useShell) { - "true" - } else { - "false" - } - yield(EntityExtra(ActionEntity.EXTRA_KEY_EVENT_USE_SHELL, string)) - } - if (data.metaState != 0) { yield( EntityExtra( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index 07e8aad67e..be7125e2d3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -156,13 +156,6 @@ class LazyActionErrorSnapshot( return getAppError(action.packageName) } - is ActionData.InputKeyEvent -> - if ( - action.useShell && !isPermissionGranted(Permission.ROOT) - ) { - return SystemError.PermissionDenied(Permission.ROOT) - } - is ActionData.Sound.SoundFile -> { soundsManager.getSound(action.soundUid).onFailure { error -> return error diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt index 47ec8de250..caf5071d17 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt @@ -46,46 +46,43 @@ class ActionUiHelper( } // only a key code can be inputted through the shell - if (action.useShell) { - getString(R.string.description_keyevent_through_shell, keyCodeString) - } else { - val metaStateString = buildString { - for (label in KeyCodeStrings.MODIFIER_LABELS.entries) { - val modifier = label.key - val labelRes = label.value - if (action.metaState.hasFlag(modifier)) { - append("${getString(labelRes)} + ") - } - } - } + val metaStateString = buildString { + for (label in KeyCodeStrings.MODIFIER_LABELS.entries) { + val modifier = label.key + val labelRes = label.value - if (action.device != null) { - val name = if (action.device.name.isBlank()) { - getString(R.string.unknown_device_name) - } else { - action.device.name + if (action.metaState.hasFlag(modifier)) { + append("${getString(labelRes)} + ") } + } + } - val nameToShow = if (showDeviceDescriptors) { - InputDeviceUtils.appendDeviceDescriptorToName( - action.device.descriptor, - name, - ) - } else { - name - } + if (action.device != null) { + val name = if (action.device.name.isBlank()) { + getString(R.string.unknown_device_name) + } else { + action.device.name + } - getString( - R.string.description_keyevent_from_device, - arrayOf(metaStateString, keyCodeString, nameToShow), + val nameToShow = if (showDeviceDescriptors) { + InputDeviceUtils.appendDeviceDescriptorToName( + action.device.descriptor, + name, ) } else { - getString( - R.string.description_keyevent, - args = arrayOf(metaStateString, keyCodeString), - ) + name } + + getString( + R.string.description_keyevent_from_device, + arrayOf(metaStateString, keyCodeString, nameToShow), + ) + } else { + getString( + R.string.description_keyevent, + args = arrayOf(metaStateString, keyCodeString), + ) } } @@ -262,7 +259,7 @@ class ActionUiHelper( R.string.action_toggle_front_flashlight_with_strength, action.strengthPercent.toPercentString(), - ) + ) } else { getString( R.string.action_toggle_flashlight_with_strength, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index 1741a6e22b..e28e305dd8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -852,12 +852,12 @@ object ActionUtils { } fun ActionData.canBeHeldDown(): Boolean = when (this) { - is ActionData.InputKeyEvent -> !useShell + is ActionData.InputKeyEvent -> true else -> false } fun ActionData.canUseImeToPerform(): Boolean = when (this) { - is ActionData.InputKeyEvent -> !useShell + is ActionData.InputKeyEvent -> true is ActionData.Text -> true else -> false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionViewModel.kt index 5961652602..6fcc481422 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionViewModel.kt @@ -211,7 +211,6 @@ class ChooseActionViewModel @Inject constructor( -> R.string.action_toggle_keyboard_message ActionId.SECURE_LOCK_DEVICE -> R.string.action_secure_lock_device_message - ActionId.POWER_ON_OFF_DEVICE -> R.string.action_power_on_off_device_message else -> null } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt index a036b06f54..d8d1e89a70 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt @@ -105,7 +105,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( keyEventState.value = KeyEventState( Success(action.keyCode), inputDevice, - useShell = action.useShell, metaState = action.metaState, ) } @@ -121,10 +120,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( keyEventState.value = keyEventState.value.copy(keyCode = keyCodeState) } - fun setUseShell(checked: Boolean) { - keyEventState.value = keyEventState.value.copy(useShell = checked) - } - @SuppressLint("NullSafeMutableLiveData") fun chooseNoDevice() { keyEventState.value = keyEventState.value.copy(chosenDevice = null) @@ -157,7 +152,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( ActionData.InputKeyEvent( keyCode = keyCode, metaState = keyEventState.value.metaState, - useShell = keyEventState.value.useShell, device = device, ), ) @@ -171,7 +165,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( ): ConfigKeyEventUiState { val keyCode = state.keyCode val metaState = state.metaState - val useShell = state.useShell val chosenDevice = state.chosenDevice val keyCodeString = when (keyCode) { @@ -226,9 +219,8 @@ class ConfigKeyEventActionViewModel @Inject constructor( keyCodeErrorMessage = keyCode.errorOrNull()?.getFullMessage(this), keyCodeLabel = keyCodeLabel, showKeyCodeLabel = keyCode.isSuccess, - isUseShellChecked = useShell, - isDevicePickerShown = !useShell, - isModifierListShown = !useShell, + isDevicePickerShown = true, + isModifierListShown = true, modifierListItems = modifierListItems, isDoneButtonEnabled = keyCode.isSuccess, deviceListItems = deviceListItems, @@ -239,7 +231,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( private data class KeyEventState( val keyCode: KMResult = KMError.EmptyText, val chosenDevice: InputDeviceInfo? = null, - val useShell: Boolean = false, val metaState: Int = 0, ) } @@ -249,7 +240,6 @@ data class ConfigKeyEventUiState( val keyCodeErrorMessage: String?, val keyCodeLabel: String, val showKeyCodeLabel: Boolean, - val isUseShellChecked: Boolean, val isDevicePickerShown: Boolean, val isModifierListShown: Boolean, val modifierListItems: List, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index 6bf2901ee9..0e373fa5bb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -137,19 +137,20 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.APP_NOT_IN_FOREGROUND, ConstraintId.APP_PLAYING_MEDIA, ConstraintId.APP_NOT_PLAYING_MEDIA, - -> onSelectAppConstraint(constraintType) + -> onSelectAppConstraint(constraintType) ConstraintId.MEDIA_PLAYING -> returnResult.emit(Constraint.MediaPlaying()) ConstraintId.MEDIA_NOT_PLAYING -> returnResult.emit(Constraint.NoMediaPlaying()) ConstraintId.BT_DEVICE_CONNECTED, ConstraintId.BT_DEVICE_DISCONNECTED, - -> onSelectBluetoothConstraint( + -> onSelectBluetoothConstraint( constraintType, ) - ConstraintId.SCREEN_ON -> onSelectScreenOnConstraint() - ConstraintId.SCREEN_OFF -> onSelectScreenOffConstraint() + ConstraintId.SCREEN_ON -> returnResult.emit(Constraint.ScreenOn()) + + ConstraintId.SCREEN_OFF -> returnResult.emit(Constraint.ScreenOff()) ConstraintId.ORIENTATION_PORTRAIT -> returnResult.emit(Constraint.OrientationPortrait()) @@ -184,13 +185,13 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.WIFI_CONNECTED, ConstraintId.WIFI_DISCONNECTED, - -> onSelectWifiConnectedConstraint( + -> onSelectWifiConnectedConstraint( constraintType, ) ConstraintId.IME_CHOSEN, ConstraintId.IME_NOT_CHOSEN, - -> onSelectImeChosenConstraint(constraintType) + -> onSelectImeChosenConstraint(constraintType) ConstraintId.DEVICE_IS_LOCKED -> returnResult.emit(Constraint.DeviceIsLocked()) @@ -353,28 +354,6 @@ class ChooseConstraintViewModel @Inject constructor( } } - private suspend fun onSelectScreenOnConstraint() { - val response = showDialog( - "screen_on_constraint_limitation", - DialogModel.Ok(getString(R.string.dialog_message_screen_constraints_limitation)), - ) - - response ?: return - - returnResult.emit(Constraint.ScreenOn()) - } - - private suspend fun onSelectScreenOffConstraint() { - val response = showDialog( - "screen_on_constraint_limitation", - DialogModel.Ok(getString(R.string.dialog_message_screen_constraints_limitation)), - ) - - response ?: return - - returnResult.emit(Constraint.ScreenOff()) - } - private suspend fun onSelectBluetoothConstraint(type: ConstraintId) { val response = showDialog( "bluetooth_device_constraint_limitation", diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt index 41fa70eed9..ebe1f12199 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt @@ -28,7 +28,6 @@ import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.popup.ToastAdapter -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 kotlinx.coroutines.CoroutineScope @@ -46,7 +45,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( private val floatingButtonRepository: FloatingButtonRepository, private val groupRepository: GroupRepository, private val preferenceRepository: PreferenceRepository, - private val suAdapter: SuAdapter, private val volumeAdapter: VolumeAdapter, private val toastAdapter: ToastAdapter, private val resourceProvider: ResourceProvider, @@ -137,14 +135,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( keyMapList.filter { it.keyMap.trigger.triggerFromOtherApps }.map { it.keyMap } }.flowOn(Dispatchers.Default) - override val detectScreenOffTriggers: Flow = - combine( - allKeyMapList, - suAdapter.isRootGranted, - ) { keyMapList, isRootPermissionGranted -> - keyMapList.any { it.keyMap.trigger.screenOffTrigger } && isRootPermissionGranted - }.flowOn(Dispatchers.Default) - override val defaultLongPressDelay: Flow = preferenceRepository.get(Keys.defaultLongPressDelay) .map { it ?: PreferenceDefaults.LONG_PRESS_DELAY } @@ -239,7 +229,6 @@ interface DetectKeyMapsUseCase { val allKeyMapList: Flow> val requestFingerprintGestureDetection: Flow val keyMapsToTriggerFromOtherApps: Flow> - val detectScreenOffTriggers: Flow val defaultLongPressDelay: Flow val defaultDoublePressDelay: Flow diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt index a2f242f8bd..5d0267ad1e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt @@ -362,10 +362,6 @@ class KeyMapListItemCreator( labels.add(getString(R.string.flag_long_press_double_vibration)) } - if (trigger.isDetectingWhenScreenOffAllowed() && trigger.screenOffTrigger) { - labels.add(getString(R.string.flag_detect_triggers_screen_off)) - } - if (trigger.triggerFromOtherApps) { labels.add(getString(R.string.flag_trigger_from_other_apps)) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt index daa157dcda..965f6798d2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt @@ -483,7 +483,6 @@ private fun ActionConstraintChip( private fun getTriggerErrorMessage(error: TriggerError): String { return when (error) { TriggerError.DND_ACCESS_DENIED -> stringResource(R.string.trigger_error_dnd_access_denied) - TriggerError.SCREEN_OFF_ROOT_DENIED -> stringResource(R.string.trigger_error_screen_off_root_permission_denied) TriggerError.CANT_DETECT_IN_PHONE_CALL -> stringResource(R.string.trigger_error_cant_detect_in_phone_call) TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> stringResource(R.string.trigger_error_assistant_not_purchased) TriggerError.DPAD_IME_NOT_SELECTED -> stringResource(R.string.trigger_error_dpad_ime_not_selected) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt index a96848f03a..63657c3fab 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt @@ -66,10 +66,6 @@ class ConfigKeyMapOptionsViewModel( config.setLongPressDoubleVibrationEnabled(checked) } - override fun onScreenOffTriggerChanged(checked: Boolean) { - config.setTriggerWhenScreenOff(checked) - } - override fun onShowToastChanged(checked: Boolean) { config.setShowToastEnabled(checked) } @@ -166,9 +162,6 @@ class ConfigKeyMapOptionsViewModel( showLongPressDoubleVibration = keyMap.trigger.isLongPressDoubleVibrationAllowed(), longPressDoubleVibration = keyMap.trigger.longPressDoubleVibration, - showScreenOffTrigger = keyMap.trigger.isDetectingWhenScreenOffAllowed(), - screenOffTrigger = keyMap.trigger.screenOffTrigger, - triggerFromOtherApps = keyMap.trigger.triggerFromOtherApps, keyMapUid = keyMap.uid, isLauncherShortcutButtonEnabled = createKeyMapShortcut.isSupported, @@ -201,9 +194,6 @@ data class KeyMapOptionsState( val showLongPressDoubleVibration: Boolean, val longPressDoubleVibration: Boolean, - val showScreenOffTrigger: Boolean, - val screenOffTrigger: Boolean, - val triggerFromOtherApps: Boolean, val keyMapUid: String, val isLauncherShortcutButtonEnabled: Boolean, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt index 4c8b1c91cc..14d90459b9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt @@ -138,11 +138,6 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( override suspend fun fixTriggerError(error: TriggerError) { when (error) { TriggerError.DND_ACCESS_DENIED -> fixError(SystemError.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY)) - TriggerError.SCREEN_OFF_ROOT_DENIED -> fixError( - SystemError.PermissionDenied( - Permission.ROOT, - ), - ) TriggerError.CANT_DETECT_IN_PHONE_CALL -> fixError(KMError.CantDetectKeyEventsInPhoneCall) TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> fixError( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt index e2a06d926d..e4681beac9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt @@ -114,18 +114,6 @@ private fun Loaded( Spacer(Modifier.height(8.dp)) - if (state.showScreenOffTrigger) { - CheckBoxText( - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - text = stringResource(R.string.flag_detect_triggers_screen_off), - isChecked = state.screenOffTrigger, - onCheckedChange = callback::onScreenOffTriggerChanged, - ) - Spacer(Modifier.height(8.dp)) - } - CheckBoxText( modifier = Modifier .padding(horizontal = 8.dp) @@ -352,7 +340,6 @@ interface KeyMapOptionsCallback { fun onVibrateDurationChanged(duration: Int) = run { } fun onVibrateChanged(checked: Boolean) = run { } fun onLongPressDoubleVibrationChanged(checked: Boolean) = run { } - fun onScreenOffTriggerChanged(checked: Boolean) = run { } fun onShowToastChanged(checked: Boolean) = run { } fun onTriggerFromOtherAppsChanged(checked: Boolean) = run {} fun onCreateShortcutClick() = run { } @@ -388,9 +375,6 @@ private fun Preview() { showLongPressDoubleVibration = true, longPressDoubleVibration = false, - showScreenOffTrigger = true, - screenOffTrigger = false, - triggerFromOtherApps = true, keyMapUid = "00000-00000-00000-0000000000000000000000000000000000", isLauncherShortcutButtonEnabled = false, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapOptionsComparator.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapOptionsComparator.kt index 448e860b59..b91819ea80 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapOptionsComparator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapOptionsComparator.kt @@ -21,7 +21,6 @@ class KeyMapOptionsComparator( keyMap, otherKeyMap, { it.vibrate }, - { it.trigger.screenOffTrigger }, { it.trigger.triggerFromOtherApps }, { it.showToast }, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index 65846ee9a4..02ea2de089 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -485,10 +485,6 @@ class ConfigTriggerDelegate { return trigger.copy(longPressDoubleVibration = enabled).validate() } - fun setTriggerWhenScreenOff(trigger: Trigger, enabled: Boolean): Trigger { - return trigger.copy(screenOffTrigger = enabled).validate() - } - fun setTriggerFromOtherAppsEnabled(trigger: Trigger, enabled: Boolean): Trigger { return trigger.copy(triggerFromOtherApps = enabled).validate() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt index 740f3aefb5..b454952b71 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -234,12 +234,6 @@ class ConfigTriggerUseCaseImpl @Inject constructor( } } - override fun setTriggerWhenScreenOff(enabled: Boolean) { - updateTrigger { trigger -> - delegate.setTriggerWhenScreenOff(trigger, enabled) - } - } - override fun setTriggerFromOtherAppsEnabled(enabled: Boolean) { updateTrigger { trigger -> delegate.setTriggerFromOtherAppsEnabled(trigger, enabled) @@ -344,7 +338,6 @@ interface ConfigTriggerUseCase : GetDefaultKeyMapOptionsUseCase { fun setDoublePressDelay(delay: Int) fun setSequenceTriggerTimeout(delay: Int) fun setLongPressDoubleVibrationEnabled(enabled: Boolean) - fun setTriggerWhenScreenOff(enabled: Boolean) fun setTriggerFromOtherAppsEnabled(enabled: Boolean) fun setShowToastEnabled(enabled: Boolean) fun setScanCodeDetectionEnabled(keyUid: String, enabled: Boolean) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt index 575d3ad175..73b1dbba0a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt @@ -22,7 +22,6 @@ data class Trigger( val mode: TriggerMode = TriggerMode.Undefined, val vibrate: Boolean = false, val longPressDoubleVibration: Boolean = false, - val screenOffTrigger: Boolean = false, val longPressDelay: Int? = null, val doublePressDelay: Int? = null, val vibrateDuration: Int? = null, @@ -41,16 +40,6 @@ data class Trigger( fun isLongPressDoubleVibrationAllowed(): Boolean = (keys.size == 1 || (mode is TriggerMode.Parallel)) && keys.getOrNull(0)?.clickType == ClickType.LONG_PRESS - /** - * Must check that it is not empty otherwise it would be true from the "all" check. - * It is not allowed if the key is an assistant button because it is assumed to be true - * anyway. - */ - fun isDetectingWhenScreenOffAllowed(): Boolean { - // TODO triggers should always detect when screen is off if possible - return false - } - fun isChangingSequenceTriggerTimeoutAllowed(): Boolean = keys.isNotEmpty() && keys.size > 1 && mode is TriggerMode.Sequence fun updateFloatingButtonData(buttons: List): Trigger { @@ -126,7 +115,6 @@ object TriggerEntityMapper { triggerFromOtherApps = entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_FROM_OTHER_APPS), showToast = entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_SHOW_TOAST), - screenOffTrigger = entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_SCREEN_OFF_TRIGGERS), ) } @@ -185,10 +173,6 @@ object TriggerEntityMapper { flags = flags.withFlag(TriggerEntity.TRIGGER_FLAG_LONG_PRESS_DOUBLE_VIBRATION) } - if (trigger.isDetectingWhenScreenOffAllowed() && trigger.screenOffTrigger) { - flags = flags.withFlag(TriggerEntity.TRIGGER_FLAG_SCREEN_OFF_TRIGGERS) - } - if (trigger.triggerFromOtherApps) { flags = flags.withFlag(TriggerEntity.TRIGGER_FLAG_FROM_OTHER_APPS) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt index b50cc06efe..dd3727110a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.trigger enum class TriggerError(val isFixable: Boolean) { DND_ACCESS_DENIED(isFixable = true), - SCREEN_OFF_ROOT_DENIED(isFixable = true), CANT_DETECT_IN_PHONE_CALL(isFixable = true), // This error appears when a key map has an assistant trigger but the user hasn't purchased diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt index 2f5a6f33eb..d16971dfa0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.trigger -import android.os.Build import android.view.KeyEvent import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.keymaps.requiresImeKeyEventForwardingInPhoneCall @@ -57,18 +56,11 @@ data class TriggerErrorSnapshot( key is KeyEventTriggerKey && key.keyCode in keysThatRequireDndAccess if (requiresDndAccess) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isDndAccessGranted) { + if (!isDndAccessGranted) { return TriggerError.DND_ACCESS_DENIED } } - if (keyMap.trigger.screenOffTrigger && - !isRootGranted && - keyMap.trigger.isDetectingWhenScreenOffAllowed() - ) { - return TriggerError.SCREEN_OFF_ROOT_DENIED - } - val containsDpadKey = key is KeyEventTriggerKey && KeyEventUtils.isDpadKeyCode(key.keyCode) && key.requiresIme diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index f8f68af9d1..7198f65e61 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -258,7 +258,6 @@ fun TriggerKeyListItem( private fun getErrorMessage(error: TriggerError): String { return when (error) { TriggerError.DND_ACCESS_DENIED -> stringResource(R.string.trigger_error_dnd_access_denied) - TriggerError.SCREEN_OFF_ROOT_DENIED -> stringResource(R.string.trigger_error_screen_off_root_permission_denied) TriggerError.CANT_DETECT_IN_PHONE_CALL -> stringResource(R.string.trigger_error_cant_detect_in_phone_call) TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> stringResource(R.string.trigger_error_assistant_not_purchased) TriggerError.DPAD_IME_NOT_SELECTED -> stringResource(R.string.trigger_error_dpad_ime_not_selected) diff --git a/base/src/main/res/layout/fragment_config_key_event.xml b/base/src/main/res/layout/fragment_config_key_event.xml index 70b32c74a4..54ceababbe 100644 --- a/base/src/main/res/layout/fragment_config_key_event.xml +++ b/base/src/main/res/layout/fragment_config_key_event.xml @@ -88,22 +88,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/textViewKeycodeLabel" /> - - + app:layout_constraintTop_toBottomOf="@id/buttonChooseKeycode"> Send broadcast: %s Key map id - Use shell (ROOT only) Permission required to work properly in Do Not Disturb mode! - The option to trigger when the screen is off needs root permission to work! This trigger won\'t work while ringing or in a phone call! Android doesn\'t let accessibility services detect volume button presses while your phone is ringing or it is in a phone call but it does let input method services detect them. Therefore, you must use one of the Key Mapper keyboards if you want this trigger to work. Too many fingers to perform gesture due to android limitations. @@ -96,7 +94,6 @@ Open %s Type \'%s\' Input %s%s - Input %s through shell Input %s%s from %s Open %s Tap screen (%d, %d) @@ -435,7 +432,6 @@ Android doesn\'t allow apps to get a list of connected (not paired) Bluetooth devices. Apps can only detect when they are connected and disconnected. So if your Bluetooth device is already connected to your device when the accessibility service starts, you will have to reconnect it for the app to know it is connected. Automatic backup Change location or turn off automatic back up? - Screen on/off constraints will only work if you have turned on the \"detect trigger when screen is off\" key map option. This option will only show for some keys (e.g volume buttons) and if you are rooted. See a list of supported keys on the Help page. If you have any other screen lock chosen, such as PIN or Pattern then you don\'t have to worry. But if you have a Password screen lock you will *NOT* be able to unlock your phone if you use the Key Mapper Basic Input Method because it doesn\'t have a GUI. You can grant Key Mapper WRITE_SECURE_SETTINGS permission so it can show a notification to switch to and from the keyboard. There is a guide on how to do this if you tap the question mark at the bottom of the screen. Select the input method for actions that require one. You can change this later by tapping \"Select keyboard for actions\" in the bottom menu of the home screen. You need to choose the \"Caps Lock to camera\" keyboard layout for your keyboard otherwise the Caps Lock key will still lock caps. You can find this setting in your device settings -> Languages and Input -> Physical Keyboard -> Tap on your keyboard -> Set Up Keyboard Layouts. This will remap the Caps Lock key to KEYCODE_CAMERA so Key Mapper can remap it properly.\n\nAfter you\'ve done this you must remove the Caps Lock trigger key and record the Caps Lock key again. It should say \"Camera\" instead of \"Caps Lock\" if you did the steps correctly. @@ -715,7 +711,6 @@ Vibrate Show an on-screen message Vibrate again on long press - Detect trigger when the screen is off Repeat %dx @@ -1041,7 +1036,6 @@ You will only be able to log back in with your PIN. The fingerprint scanner and face unlock will be disabled. This is the only reliable way I have found to lock non-rooted devices before Android Pie 9.0. Sleep/wake device - You must turn on the option to detect the trigger when the screen is off! Do nothing diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index d358daeee6..9390eb2276 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -61,7 +61,8 @@ data class ActionEntity( const val EXTRA_KEY_EVENT_META_STATE = "extra_meta_state" const val EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR = "extra_device_descriptor" const val EXTRA_KEY_EVENT_DEVICE_NAME = "extra_device_name" - const val EXTRA_KEY_EVENT_USE_SHELL = "extra_key_event_use_shell" + +// const val EXTRA_KEY_EVENT_USE_SHELL = "extra_key_event_use_shell" const val EXTRA_IME_ID = "extra_ime_id" const val EXTRA_IME_NAME = "extra_ime_name" diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt index 1a2d114c70..ec2e77603d 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt @@ -39,7 +39,10 @@ data class TriggerEntity( // DON'T CHANGE THESE AND THEY MUST BE POWERS OF 2!! const val TRIGGER_FLAG_VIBRATE = 1 const val TRIGGER_FLAG_LONG_PRESS_DOUBLE_VIBRATION = 2 + + @Deprecated("This is now on by default for evdev trigger keys") const val TRIGGER_FLAG_SCREEN_OFF_TRIGGERS = 4 + const val TRIGGER_FLAG_FROM_OTHER_APPS = 8 const val TRIGGER_FLAG_SHOW_TOAST = 16 diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 9ff0ed0518..a613f939f8 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -86,7 +86,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // This stops Key Mapper going back if they are turning on wireless debugging // for another reason. if (isWirelessDebuggingEnabled.value && setupAssistantStepState.value == SystemBridgeSetupStep.WIRELESS_DEBUGGING) { - // TODO only go back if the ADB server is actually running. The first time wireless debugging is turned on in a new network, it shows a dialog getKeyMapperAppTask()?.moveToFront() } } From 4f2d712bb96d5a6039fb17c229195b4b25718f70 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 7 Sep 2025 15:40:35 +0200 Subject: [PATCH 213/215] #1394 complete more TODOs --- .../base/actions/PerformActionsUseCase.kt | 1 - .../keymapper/base/promode/ProModeSetupScreen.kt | 11 +++++------ .../permissions/RequestPermissionDelegate.kt | 15 +++------------ base/src/main/res/values/strings.xml | 3 +-- 4 files changed, 9 insertions(+), 21 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index ca975e24b1..92c1b23e78 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -122,7 +122,6 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( ) } - // TODO use system bridge where possible override suspend fun perform( action: ActionData, inputEventAction: InputEventAction, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index 6362024093..38796eb242 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -71,7 +70,7 @@ fun ProModeSetupScreen( state = state, onStepButtonClick = viewModel::onStepButtonClick, onAssistantClick = viewModel::onAssistantClick, - onWatchTutorialClick = { }, //TODO + onWatchTutorialClick = { }, onBackClick = viewModel::onBackClick ) } @@ -251,12 +250,12 @@ private fun StepContent( Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { - TextButton(onClick = onWatchTutorialClick) { - Text(text = stringResource(R.string.pro_mode_setup_wizard_watch_tutorial_button)) - } +// TextButton(onClick = onWatchTutorialClick) { +// Text(text = stringResource(R.string.pro_mode_setup_wizard_watch_tutorial_button)) +// } Button(onClick = onButtonClick) { Text(text = stepContent.buttonText) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt index c45bf06730..e4777e2af8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt @@ -70,10 +70,7 @@ class RequestPermissionDelegate( Permission.CALL_PHONE -> requestPermissionLauncher.launch(Manifest.permission.CALL_PHONE) Permission.ANSWER_PHONE_CALL -> requestPermissionLauncher.launch(Manifest.permission.ANSWER_PHONE_CALLS) Permission.FIND_NEARBY_DEVICES -> requestPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT) - Permission.ROOT -> { - require(navController != null) { "nav controller can't be null!" } - requestRootPermission(navController) - } + Permission.ROOT -> requestRootPermission() Permission.IGNORE_BATTERY_OPTIMISATION -> requestIgnoreBatteryOptimisations() @@ -179,24 +176,18 @@ class RequestPermissionDelegate( } } - // TODO show prompt requesting root permission. If not found then show a dialog explaining to grant permission manually in their root management app such as Magisk. - private fun requestRootPermission(navController: NavController) { + private fun requestRootPermission() { if (showDialogs) { activity.materialAlertDialog { titleResource = R.string.dialog_title_root_prompt messageResource = R.string.dialog_message_root_prompt setIcon(R.drawable.ic_baseline_warning_24) - okButton { -// navController.navigate(NavBaseAppDirections.toSettingsFragment()) - } - + okButton() negativeButton(R.string.neg_cancel) { it.cancel() } show() } - } else { -// navController.navigate(NavBaseAppDirections.toSettingsFragment()) } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 1d8081fece..1e5875f4e7 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -420,8 +420,7 @@ Can\'t find the accessibility settings page Unsaved changes You have unsaved changes. If you discard them, your edits will be lost. - If you know your phone isn\'t rooted or you don\'t know what root is, you can\'t use features which only work on rooted devices. When you tap \'OK\', you will be taken to the settings. - In the settings, scroll to the bottom and tap \'Key Mapper has root permission\' so you can use root features/actions. + Please grant Key Mapper root permission in your root management app, such as Magisk. Grant WRITE_SECURE_SETTINGS permission A PC/Mac is required to grant this permission. Read the online guide. From ccd0246ebd89e4c9b7bad0728dee27e8758dc338 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 7 Sep 2025 16:00:29 +0200 Subject: [PATCH 214/215] style: ktlint reformat --- .../sds100/keymapper/home/HomeViewModel.kt | 2 +- .../AccessibilityServiceController.kt | 4 +- .../keymapper/base/ActivityViewModel.kt | 2 +- .../sds100/keymapper/base/BaseMainActivity.kt | 2 +- .../sds100/keymapper/base/BaseMainNavHost.kt | 2 +- .../base/actions/ActionErrorSnapshot.kt | 2 +- .../keymapper/base/actions/ActionUiHelper.kt | 2 +- .../keymapper/base/actions/ActionUtils.kt | 57 +++---- .../base/actions/ChooseActionScreen.kt | 12 +- .../base/actions/ConfigActionsUseCase.kt | 7 +- .../base/actions/ConfigActionsViewModel.kt | 3 +- .../base/actions/GetActionErrorUseCase.kt | 6 +- .../base/actions/PerformActionsUseCase.kt | 4 +- .../constraints/ChooseConstraintViewModel.kt | 8 +- .../constraints/ConfigConstraintsUseCase.kt | 6 +- .../constraints/ConfigConstraintsViewModel.kt | 4 +- .../base/detection/KeyMapAlgorithm.kt | 2 +- .../base/detection/SimpleMappingController.kt | 2 +- .../TriggerKeyMapFromOtherAppsController.kt | 1 - .../keymapper/base/input/EvdevHandleCache.kt | 4 +- .../base/keymaps/ConfigKeyMapScreen.kt | 2 +- .../base/keymaps/ConfigKeyMapState.kt | 5 +- .../base/keymaps/ConfigKeyMapViewModel.kt | 1 - .../base/keymaps/DisplayKeyMapUseCase.kt | 4 +- .../base/logging/DisplayLogUseCase.kt | 1 - .../keymapper/base/logging/LogScreen.kt | 21 +-- .../keymapper/base/logging/LogViewModel.kt | 4 +- .../keymapper/base/promode/ProModeScreen.kt | 76 ++++----- .../base/promode/ProModeSetupScreen.kt | 131 ++++++++-------- .../base/promode/ProModeSetupViewModel.kt | 14 +- .../base/promode/ProModeViewModel.kt | 6 +- .../base/promode/ShizukuSetupState.kt | 4 +- .../base/promode/SystemBridgeAutoStarter.kt | 12 +- .../SystemBridgeSetupAssistantController.kt | 23 ++- .../base/promode/SystemBridgeSetupUseCase.kt | 10 +- .../AutomaticChangeImeSettingsScreen.kt | 28 ++-- .../base/settings/ConfigSettingsUseCase.kt | 2 +- .../settings/DefaultOptionsSettingsScreen.kt | 6 +- .../keymapper/base/settings/SettingsScreen.kt | 61 ++++---- .../base/settings/SettingsViewModel.kt | 8 +- .../sds100/keymapper/base/settings/Theme.kt | 2 +- .../shortcuts/CreateKeyMapShortcutScreen.kt | 2 +- .../BaseAccessibilityServiceController.kt | 2 +- .../AndroidNotificationAdapter.kt | 17 +-- .../notifications/NotificationController.kt | 16 +- .../trigger/BaseConfigTriggerViewModel.kt | 6 +- .../base/trigger/BaseTriggerScreen.kt | 16 +- .../base/trigger/ConfigTriggerDelegate.kt | 38 +++-- .../base/trigger/ConfigTriggerUseCase.kt | 11 +- .../keymapper/base/trigger/EvdevTriggerKey.kt | 4 +- .../base/trigger/KeyCodeTriggerKey.kt | 3 +- .../base/trigger/KeyEventTriggerKey.kt | 4 +- .../base/trigger/RecordTriggerButtonRow.kt | 10 +- .../base/trigger/RecordTriggerController.kt | 2 +- .../trigger/TriggerKeyOptionsBottomSheet.kt | 34 ++--- .../base/trigger/TriggerValidator.kt | 2 +- .../sds100/keymapper/base/utils/ErrorUtils.kt | 7 +- .../keymapper/base/utils/ScancodeStrings.kt | 4 +- .../ui/compose/KeyMapperSegmentedButtonRow.kt | 10 +- .../ui/compose/SwitchPreferenceCompose.kt | 10 +- .../utils/ui/compose/icons/FakeShizuku.kt | 2 +- .../utils/ui/compose/icons/FolderManaged.kt | 2 +- .../utils/ui/compose/icons/KeyMapperIcon.kt | 34 ++--- .../utils/ui/compose/icons/ProModeDisabled.kt | 18 +-- .../compose/icons/SignalWifiNotConnected.kt | 2 +- .../base/utils/ui/compose/icons/WandStars.kt | 2 +- .../keymapper/base/BackupManagerTest.kt | 3 +- .../base/actions/ConfigActionsUseCaseTest.kt | 15 +- .../base/actions/GetActionErrorUseCaseTest.kt | 2 +- .../base/actions/PerformActionsUseCaseTest.kt | 2 +- .../base/keymaps/KeyMapAlgorithmTest.kt | 50 +++--- .../ProcessKeyMapGroupsForDetectionTest.kt | 2 +- .../base/trigger/ConfigTriggerDelegateTest.kt | 144 +++++++++--------- .../keymapper/base/trigger/TriggerKeyTest.kt | 20 +-- .../notifications/KMNotificationAction.kt | 9 +- .../common/utils/InputDeviceUtils.kt | 2 +- .../keymapper/common/utils/SettingsUtils.kt | 2 +- .../data/entities/TriggerKeyEntity.kt | 2 +- 78 files changed, 525 insertions(+), 539 deletions(-) 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 5e9d90ee09..4ebfcfd6fc 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 @@ -3,8 +3,8 @@ package io.github.sds100.keymapper.home import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCase import io.github.sds100.keymapper.base.home.BaseHomeViewModel -import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase +import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 1f1b43b6f9..3989a01ffd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -29,7 +29,7 @@ class AccessibilityServiceController @AssistedInject constructor( keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, inputEventHub: InputEventHub, recordTriggerController: RecordTriggerController, - setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory + setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory, ) : BaseAccessibilityServiceController( service = service, accessibilityNodeRecorderFactory = accessibilityNodeRecorderFactory, @@ -42,7 +42,7 @@ class AccessibilityServiceController @AssistedInject constructor( keyEventRelayServiceWrapper = keyEventRelayServiceWrapper, inputEventHub = inputEventHub, recordTriggerController = recordTriggerController, - setupAssistantControllerFactory = setupAssistantControllerFactory + setupAssistantControllerFactory = setupAssistantControllerFactory, ) { @AssistedFactory interface Factory { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt index db6ccc08af..b734c2c94b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt @@ -16,7 +16,7 @@ import javax.inject.Inject class ActivityViewModel @Inject constructor( resourceProvider: ResourceProvider, dialogProvider: DialogProvider, - navigationProvider: NavigationProvider + navigationProvider: NavigationProvider, ) : ViewModel(), ResourceProvider by resourceProvider, DialogProvider by dialogProvider, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 7164ec369e..bd74f28121 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -227,7 +227,7 @@ abstract class BaseMainActivity : AppCompatActivity() { val gamepadEvent = KMGamePadEvent.fromMotionEvent(event) ?: return false val consume = inputEventHub.onInputEvent( gamepadEvent, - detectionSource = InputEventDetectionSource.INPUT_METHOD + detectionSource = InputEventDetectionSource.INPUT_METHOD, ) return if (consume) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt index 2b69425575..1fce8dbf81 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt @@ -128,7 +128,7 @@ fun BaseMainNavHost( LogScreen( modifier = Modifier.fillMaxSize(), viewModel = hiltViewModel(), - onBackClick = { navController.popBackStack() } + onBackClick = { navController.popBackStack() }, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index be7125e2d3..3f9fc360de 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -35,7 +35,7 @@ class LazyActionErrorSnapshot( private val ringtoneAdapter: RingtoneAdapter, private val buildConfigProvider: BuildConfigProvider, private val systemBridgeConnectionManager: SystemBridgeConnectionManager, - private val preferenceRepository: PreferenceRepository + private val preferenceRepository: PreferenceRepository, ) : ActionErrorSnapshot, IsActionSupportedUseCase by IsActionSupportedUseCaseImpl( systemFeatureAdapter, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt index caf5071d17..e3b76dd85d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt @@ -259,7 +259,7 @@ class ActionUiHelper( R.string.action_toggle_front_flashlight_with_strength, action.strengthPercent.toPercentString(), - ) + ) } else { getString( R.string.action_toggle_flashlight_with_strength, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index e28e305dd8..c995e13c39 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -495,26 +495,26 @@ object ActionUtils { ActionId.TOGGLE_DND_MODE, ActionId.ENABLE_DND_MODE, ActionId.DISABLE_DND_MODE, - -> Build.VERSION_CODES.M + -> Build.VERSION_CODES.M ActionId.DISABLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.TOGGLE_FLASHLIGHT, - -> Build.VERSION_CODES.M + -> Build.VERSION_CODES.M ActionId.CHANGE_FLASHLIGHT_STRENGTH, - -> Build.VERSION_CODES.TIRAMISU + -> Build.VERSION_CODES.TIRAMISU ActionId.TOGGLE_KEYBOARD, ActionId.SHOW_KEYBOARD, ActionId.HIDE_KEYBOARD, - -> Build.VERSION_CODES.N + -> Build.VERSION_CODES.N ActionId.TEXT_CUT, ActionId.TEXT_COPY, ActionId.TEXT_PASTE, ActionId.SELECT_WORD_AT_CURSOR, - -> Build.VERSION_CODES.JELLY_BEAN_MR2 + -> Build.VERSION_CODES.JELLY_BEAN_MR2 ActionId.SHOW_POWER_MENU -> Build.VERSION_CODES.LOLLIPOP ActionId.DEVICE_CONTROLS -> Build.VERSION_CODES.S @@ -539,20 +539,20 @@ object ActionUtils { ActionId.END_PHONE_CALL, ActionId.ANSWER_PHONE_CALL, ActionId.PHONE_CALL, - -> listOf(PackageManager.FEATURE_TELEPHONY) + -> listOf(PackageManager.FEATURE_TELEPHONY) ActionId.SECURE_LOCK_DEVICE, - -> listOf(PackageManager.FEATURE_DEVICE_ADMIN) + -> listOf(PackageManager.FEATURE_DEVICE_ADMIN) ActionId.TOGGLE_WIFI, ActionId.ENABLE_WIFI, ActionId.DISABLE_WIFI, - -> listOf(PackageManager.FEATURE_WIFI) + -> listOf(PackageManager.FEATURE_WIFI) ActionId.TOGGLE_MOBILE_DATA, ActionId.ENABLE_MOBILE_DATA, ActionId.DISABLE_MOBILE_DATA, - -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { listOf(PackageManager.FEATURE_TELEPHONY_DATA) } else { listOf(PackageManager.FEATURE_TELEPHONY) @@ -561,18 +561,18 @@ object ActionUtils { ActionId.TOGGLE_NFC, ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, - -> listOf(PackageManager.FEATURE_NFC) + -> listOf(PackageManager.FEATURE_NFC) ActionId.TOGGLE_BLUETOOTH, ActionId.ENABLE_BLUETOOTH, ActionId.DISABLE_BLUETOOTH, - -> listOf(PackageManager.FEATURE_BLUETOOTH) + -> listOf(PackageManager.FEATURE_BLUETOOTH) ActionId.TOGGLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.DISABLE_FLASHLIGHT, ActionId.CHANGE_FLASHLIGHT_STRENGTH, - -> listOf(PackageManager.FEATURE_CAMERA_FLASH) + -> listOf(PackageManager.FEATURE_CAMERA_FLASH) else -> emptyList() } @@ -582,27 +582,28 @@ object ActionUtils { return when (id) { ActionId.ENABLE_WIFI, ActionId.DISABLE_WIFI, - ActionId.TOGGLE_WIFI -> true + ActionId.TOGGLE_WIFI, + -> true ActionId.TOGGLE_MOBILE_DATA, ActionId.ENABLE_MOBILE_DATA, ActionId.DISABLE_MOBILE_DATA, - -> true + -> true ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, ActionId.TOGGLE_NFC, - -> true + -> true ActionId.TOGGLE_AIRPLANE_MODE, ActionId.ENABLE_AIRPLANE_MODE, ActionId.DISABLE_AIRPLANE_MODE, - -> true + -> true ActionId.TOGGLE_BLUETOOTH, ActionId.ENABLE_BLUETOOTH, ActionId.DISABLE_BLUETOOTH, - -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 + -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 ActionId.POWER_ON_OFF_DEVICE -> true @@ -615,7 +616,7 @@ object ActionUtils { ActionId.TOGGLE_MOBILE_DATA, ActionId.ENABLE_MOBILE_DATA, ActionId.DISABLE_MOBILE_DATA, - -> return if (isSystemBridgeSupported) { + -> return if (isSystemBridgeSupported) { emptyList() } else { listOf(Permission.ROOT) @@ -628,7 +629,7 @@ object ActionUtils { ActionId.PREVIOUS_TRACK_PACKAGE, ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, - -> return listOf(Permission.NOTIFICATION_LISTENER) + -> return listOf(Permission.NOTIFICATION_LISTENER) ActionId.VOLUME_UP, ActionId.VOLUME_DOWN, @@ -644,7 +645,7 @@ object ActionUtils { ActionId.TOGGLE_DND_MODE, ActionId.DISABLE_DND_MODE, ActionId.ENABLE_DND_MODE, - -> return listOf(Permission.ACCESS_NOTIFICATION_POLICY) + -> return listOf(Permission.ACCESS_NOTIFICATION_POLICY) ActionId.TOGGLE_AUTO_ROTATE, ActionId.ENABLE_AUTO_ROTATE, @@ -653,25 +654,25 @@ object ActionUtils { ActionId.LANDSCAPE_MODE, ActionId.SWITCH_ORIENTATION, ActionId.CYCLE_ROTATIONS, - -> return listOf(Permission.WRITE_SETTINGS) + -> return listOf(Permission.WRITE_SETTINGS) ActionId.TOGGLE_AUTO_BRIGHTNESS, ActionId.ENABLE_AUTO_BRIGHTNESS, ActionId.DISABLE_AUTO_BRIGHTNESS, ActionId.INCREASE_BRIGHTNESS, ActionId.DECREASE_BRIGHTNESS, - -> return listOf(Permission.WRITE_SETTINGS) + -> return listOf(Permission.WRITE_SETTINGS) ActionId.TOGGLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.DISABLE_FLASHLIGHT, ActionId.CHANGE_FLASHLIGHT_STRENGTH, - -> return listOf(Permission.CAMERA) + -> return listOf(Permission.CAMERA) ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, ActionId.TOGGLE_NFC, - -> return if (isSystemBridgeSupported) { + -> return if (isSystemBridgeSupported) { emptyList() } else { listOf(Permission.ROOT) @@ -689,7 +690,7 @@ object ActionUtils { ActionId.TOGGLE_AIRPLANE_MODE, ActionId.ENABLE_AIRPLANE_MODE, ActionId.DISABLE_AIRPLANE_MODE, - -> return if (isSystemBridgeSupported) { + -> return if (isSystemBridgeSupported) { emptyList() } else { listOf(Permission.ROOT) @@ -712,11 +713,11 @@ object ActionUtils { ActionId.DISMISS_ALL_NOTIFICATIONS, ActionId.DISMISS_MOST_RECENT_NOTIFICATION, - -> return listOf(Permission.NOTIFICATION_LISTENER) + -> return listOf(Permission.NOTIFICATION_LISTENER) ActionId.ANSWER_PHONE_CALL, ActionId.END_PHONE_CALL, - -> return listOf(Permission.ANSWER_PHONE_CALL) + -> return listOf(Permission.ANSWER_PHONE_CALL) ActionId.PHONE_CALL -> return listOf(Permission.CALL_PHONE) @@ -898,7 +899,7 @@ fun ActionData.isEditable(): Boolean = when (this) { is ActionData.HttpRequest, is ActionData.InteractUiElement, is ActionData.MoveCursor, - -> true + -> true else -> false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt index 9ebd5b8010..490ae30a1c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt @@ -119,7 +119,7 @@ private fun ChooseActionScreen( end = endPadding, ), - ) { + ) { when (state) { State.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize()) @@ -224,7 +224,7 @@ private fun PreviewList() { icon = ComposeIconInfo.Vector(Icons.Rounded.Android), ), - ), + ), ), SimpleListItemGroup( header = "Connectivity", @@ -245,7 +245,7 @@ private fun PreviewList() { isEnabled = false, ), - ), + ), ), ), ), @@ -274,7 +274,7 @@ private fun PreviewGrid() { icon = ComposeIconInfo.Vector(Icons.Rounded.Android), ), - ), + ), ), SimpleListItemGroup( header = "Connectivity", @@ -304,10 +304,10 @@ private fun PreviewGrid() { isEnabled = false, ), - ), + ), ), - ), + ), ), ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt index cf0362d682..c5212a6585 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt @@ -26,7 +26,7 @@ class ConfigActionsUseCaseImpl @Inject constructor( private val state: ConfigKeyMapState, private val preferenceRepository: PreferenceRepository, private val configConstraints: ConfigConstraintsUseCase, - defaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase + defaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase, ) : ConfigActionsUseCase, GetDefaultKeyMapOptionsUseCase by defaultKeyMapOptionsUseCase { override val keyMap: StateFlow> = state.keyMap @@ -64,7 +64,6 @@ class ConfigActionsUseCaseImpl @Inject constructor( keyMap.copy(actionList = newActionList) } - } override fun moveAction(fromIndex: Int, toIndex: Int) { @@ -258,7 +257,6 @@ class ConfigActionsUseCaseImpl @Inject constructor( ) } } - } interface ConfigActionsUseCase : GetDefaultKeyMapOptionsUseCase { @@ -283,5 +281,4 @@ interface ConfigActionsUseCase : GetDefaultKeyMapOptionsUseCase { fun setActionStopRepeatingWhenTriggerReleased(uid: String) fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) - -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index bd5c032166..f0b45b975e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -52,7 +52,8 @@ class ConfigActionsViewModel @Inject constructor( resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, -) : ViewModel(), ActionOptionsBottomSheetCallback, +) : ViewModel(), + ActionOptionsBottomSheetCallback, ResourceProvider by resourceProvider, DialogProvider by dialogProvider, NavigationProvider by navigationProvider { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt index 889863bb1e..eec3255131 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt @@ -33,7 +33,7 @@ class GetActionErrorUseCaseImpl @Inject constructor( private val ringtoneAdapter: RingtoneAdapter, private val buildConfigProvider: BuildConfigProvider, private val systemBridgeConnectionManager: SystemBridgeConnectionManager, - private val preferenceRepository: PreferenceRepository + private val preferenceRepository: PreferenceRepository, ) : GetActionErrorUseCase { private val invalidateActionErrors = merge( @@ -46,7 +46,7 @@ class GetActionErrorUseCaseImpl @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { merge( systemBridgeConnectionManager.connectionState.drop(1).map { }, - preferenceRepository.get(Keys.isSystemBridgeUsed) + preferenceRepository.get(Keys.isSystemBridgeUsed), ) } else { emptyFlow() @@ -72,7 +72,7 @@ class GetActionErrorUseCaseImpl @Inject constructor( ringtoneAdapter, buildConfigProvider, systemBridgeConnectionManager, - preferenceRepository + preferenceRepository, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 92c1b23e78..ce156b0299 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -105,7 +105,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( private val ringtoneAdapter: RingtoneAdapter, private val settingsRepository: PreferenceRepository, private val inputEventHub: InputEventHub, - private val systemBridgeConnectionManager: SystemBridgeConnectionManager + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, ) : PerformActionsUseCase { @AssistedFactory @@ -797,7 +797,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( metaState = 0, deviceId = -1, scanCode = Scancode.KEY_POWER, - source = InputDevice.SOURCE_UNKNOWN + source = InputDevice.SOURCE_UNKNOWN, ) result = inputEventHub.injectKeyEvent(model) .then { inputEventHub.injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index 0e373fa5bb..cb7f9f2985 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -137,14 +137,14 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.APP_NOT_IN_FOREGROUND, ConstraintId.APP_PLAYING_MEDIA, ConstraintId.APP_NOT_PLAYING_MEDIA, - -> onSelectAppConstraint(constraintType) + -> onSelectAppConstraint(constraintType) ConstraintId.MEDIA_PLAYING -> returnResult.emit(Constraint.MediaPlaying()) ConstraintId.MEDIA_NOT_PLAYING -> returnResult.emit(Constraint.NoMediaPlaying()) ConstraintId.BT_DEVICE_CONNECTED, ConstraintId.BT_DEVICE_DISCONNECTED, - -> onSelectBluetoothConstraint( + -> onSelectBluetoothConstraint( constraintType, ) @@ -185,13 +185,13 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.WIFI_CONNECTED, ConstraintId.WIFI_DISCONNECTED, - -> onSelectWifiConnectedConstraint( + -> onSelectWifiConnectedConstraint( constraintType, ) ConstraintId.IME_CHOSEN, ConstraintId.IME_NOT_CHOSEN, - -> onSelectImeChosenConstraint(constraintType) + -> onSelectImeChosenConstraint(constraintType) ConstraintId.DEVICE_IS_LOCKED -> returnResult.emit(Constraint.DeviceIsLocked()) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt index 449597251e..ebcd303914 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt @@ -20,7 +20,7 @@ import javax.inject.Inject @ViewModelScoped class ConfigConstraintsUseCaseImpl @Inject constructor( private val state: ConfigKeyMapState, - private val preferenceRepository: PreferenceRepository + private val preferenceRepository: PreferenceRepository, ) : ConfigConstraintsUseCase { override val keyMap: StateFlow> = state.keyMap @@ -95,7 +95,6 @@ class ConfigConstraintsUseCaseImpl @Inject constructor( } } - private suspend fun getConstraintShortcuts(json: String?): List { if (json == null) { return emptyList() @@ -112,7 +111,6 @@ class ConfigConstraintsUseCaseImpl @Inject constructor( return emptyList() } } - } interface ConfigConstraintsUseCase { @@ -123,4 +121,4 @@ interface ConfigConstraintsUseCase { fun removeConstraint(id: String) fun setAndMode() fun setOrMode() -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt index 85c0b0abbc..3f2b67d5f7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject - @HiltViewModel class ConfigConstraintsViewModel @Inject constructor( private val config: ConfigConstraintsUseCase, @@ -44,7 +43,8 @@ class ConfigConstraintsViewModel @Inject constructor( resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, -) : ViewModel(), ResourceProvider by resourceProvider, +) : ViewModel(), + ResourceProvider by resourceProvider, DialogProvider by dialogProvider, NavigationProvider by navigationProvider { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index 59a46876c5..809266c0e6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -1857,7 +1857,7 @@ class KeyMapAlgorithm( KeyEvent.KEYCODE_SYM, KeyEvent.KEYCODE_NUM, KeyEvent.KEYCODE_FUNCTION, - -> true + -> true else -> false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt index 7d84d5c76d..872ca159d0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt @@ -194,4 +194,4 @@ abstract class SimpleMappingController( } private class RepeatJob(val actionUid: String, launch: () -> Job) : Job by launch.invoke() -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt index 1f76b46fa8..9f0b8101e1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.keymaps.KeyMap -import io.github.sds100.keymapper.base.detection.SimpleMappingController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt index 62e432421f..5d7147abe3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt @@ -22,7 +22,7 @@ import java.util.concurrent.ConcurrentHashMap class EvdevHandleCache( private val coroutineScope: CoroutineScope, private val devicesAdapter: DevicesAdapter, - private val systemBridgeConnectionManager: SystemBridgeConnectionManager + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, ) { private val devicesByPath: MutableMap = ConcurrentHashMap() @@ -30,7 +30,7 @@ class EvdevHandleCache( coroutineScope.launch { combine( devicesAdapter.connectedInputDevices, - systemBridgeConnectionManager.connectionState + systemBridgeConnectionManager.connectionState, ) { _, connectionState -> if (connectionState !is SystemBridgeConnectionState.Connected) { devicesByPath.clear() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt index 8a49420851..e178a384ca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt @@ -58,4 +58,4 @@ fun ConfigKeyMapScreen( onConstraintTapTargetCompleted = keyMapViewModel::onConstraintTapTargetCompleted, onSkipTutorialClick = keyMapViewModel::onSkipTutorialClick, ) -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt index 705a1adc7c..8db0a071b5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt @@ -24,7 +24,7 @@ import javax.inject.Singleton class ConfigKeyMapStateImpl @Inject constructor( private val coroutineScope: CoroutineScope, private val keyMapRepository: KeyMapRepository, - private val floatingButtonRepository: FloatingButtonRepository + private val floatingButtonRepository: FloatingButtonRepository, ) : ConfigKeyMapState { private var originalKeyMap: KeyMap? = null @@ -105,7 +105,6 @@ class ConfigKeyMapStateImpl @Inject constructor( override fun update(block: (keyMap: KeyMap) -> KeyMap) { _keyMap.update { value -> value.mapData { block.invoke(it) } } } - } interface ConfigKeyMapState { @@ -120,4 +119,4 @@ interface ConfigKeyMapState { fun loadNewKeyMap(groupUid: String?) val floatingButtonToUse: MutableStateFlow -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt index e9887e95c0..88c7e0af5c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt @@ -97,5 +97,4 @@ class ConfigKeyMapViewModel @Inject constructor( fun onEnabledChanged(enabled: Boolean) { configTrigger.setEnabled(enabled) } - } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt index 14d90459b9..9f41439665 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt @@ -58,7 +58,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( private val getActionErrorUseCase: GetActionErrorUseCase, private val getConstraintErrorUseCase: GetConstraintErrorUseCase, private val buildConfigProvider: BuildConfigProvider, - private val navigationProvider: NavigationProvider + private val navigationProvider: NavigationProvider, ) : DisplayKeyMapUseCase, GetActionErrorUseCase by getActionErrorUseCase, GetConstraintErrorUseCase by getConstraintErrorUseCase { @@ -195,7 +195,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( is SystemBridgeError.Disconnected -> navigationProvider.navigate( "fix_system_bridge", - NavDestination.ProMode + NavDestination.ProMode, ) else -> Unit diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt index 26b76607e2..de3afe7ede 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt @@ -48,7 +48,6 @@ class DisplayLogUseCaseImpl @Inject constructor( ) } - private fun createLogText(logEntries: List): String { return logEntries.joinToString(separator = "\n") { entry -> val date = dateFormat.format(Date(entry.time)) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt index 7dd3708ebc..ca68e3ad0b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt @@ -80,11 +80,11 @@ private fun LogScreen( actions = { OutlinedButton( modifier = Modifier.padding(horizontal = 16.dp), - onClick = onClearLogClick + onClick = onClearLogClick, ) { Text(stringResource(R.string.action_clear_log)) } - } + }, ) }, bottomBar = { @@ -99,7 +99,7 @@ private fun LogScreen( IconButton(onClick = onCopyToClipboardClick) { Icon( imageVector = Icons.Outlined.ContentCopy, - contentDescription = stringResource(R.string.action_copy_log) + contentDescription = stringResource(R.string.action_copy_log), ) } } @@ -143,7 +143,7 @@ private fun Content( LazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(8.dp) + contentPadding = PaddingValues(8.dp), ) { items(logListItems, key = { it.id }) { item -> val color = when (item.severity) { @@ -158,14 +158,14 @@ private fun Content( text = item.time, color = color, style = MaterialTheme.typography.bodySmall, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Spacer(modifier = Modifier.width(8.dp)) Text( text = item.message, color = color, style = MaterialTheme.typography.bodySmall, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) } } @@ -186,26 +186,27 @@ private fun Preview() { 2, "12:34:57.123", LogSeverity.WARNING, - "This is a warning message" + "This is a warning message", ), LogListItem( 3, "12:34:58.456", LogSeverity.ERROR, - "This is an error message. It is a bit long to see how it overflows inside the available space." + "This is an error message. It is a bit long to see how it overflows inside the available space.", ), LogListItem(4, "12:34:59.000", LogSeverity.INFO, "Another info message"), LogListItem( 5, "12:35:00.000", LogSeverity.ERROR, - "Error recording trigger" + "Error recording trigger", ), LogListItem(6, "12:35:01.000", LogSeverity.WARNING, "I am a warning"), LogListItem(7, "12:35:02.000", LogSeverity.INFO, "I am some info..."), LogListItem(8, "12:35:03.000", LogSeverity.INFO, "This more info"), ), ) - }) + }, + ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt index 9f3e4ec7d4..3eedcaaf36 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt @@ -27,14 +27,14 @@ class LogViewModel @Inject constructor( id = it.id, time = dateFormat.format(it.time), message = it.message, - severity = it.severity + severity = it.severity, ) } } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList() + initialValue = emptyList(), ) fun onCopyToClipboardClick() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt index 7505551b5a..0ba677bdfd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -84,7 +84,7 @@ fun ProModeScreen( modifier = modifier, onBackClick = viewModel::onBackClick, onHelpClick = { viewModel.showInfoCard() }, - showHelpIcon = !viewModel.showInfoCard + showHelpIcon = !viewModel.showInfoCard, ) { Content( warningState = proModeWarningState, @@ -121,16 +121,16 @@ private fun ProModeScreen( AnimatedVisibility( visible = showHelpIcon, enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() + exit = fadeOut() + shrinkVertically(), ) { IconButton(onClick = onHelpClick) { Icon( imageVector = Icons.AutoMirrored.Rounded.HelpOutline, - contentDescription = stringResource(R.string.pro_mode_info_card_show_content_description) + contentDescription = stringResource(R.string.pro_mode_info_card_show_content_description), ) } } - } + }, ) }, bottomBar = { @@ -183,13 +183,13 @@ private fun Content( AnimatedVisibility( visible = showInfoCard, enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() + exit = fadeOut() + shrinkVertically(), ) { ProModeInfoCard( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), - onDismiss = onInfoCardDismiss + onDismiss = onInfoCardDismiss, ) } @@ -247,7 +247,7 @@ private fun LoadedContent( onSetupWithKeyMapperClick: () -> Unit, onRequestNotificationPermissionClick: () -> Unit = {}, autoStartAtBoot: Boolean, - onAutoStartAtBootToggled: (Boolean) -> Unit = {} + onAutoStartAtBootToggled: (Boolean) -> Unit = {}, ) { Column(modifier) { OptionsHeaderRow( @@ -269,7 +269,7 @@ private fun LoadedContent( Icon( imageVector = Icons.Rounded.Notifications, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface + tint = MaterialTheme.colorScheme.onSurface, ) }, title = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_title), @@ -280,7 +280,7 @@ private fun LoadedContent( ) }, buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_button), - onButtonClick = onRequestNotificationPermissionClick + onButtonClick = onRequestNotificationPermissionClick, ) Spacer(modifier = Modifier.height(8.dp)) } @@ -290,7 +290,7 @@ private fun LoadedContent( EmergencyTipCard( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp) + .padding(horizontal = 8.dp), ) Spacer(modifier = Modifier.height(8.dp)) @@ -299,7 +299,7 @@ private fun LoadedContent( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), - onStopClick = onStopServiceClick + onStopClick = onStopServiceClick, ) } @@ -314,7 +314,7 @@ private fun LoadedContent( Icon( imageVector = Icons.Rounded.Numbers, contentDescription = null, - tint = LocalCustomColorsPalette.current.magiskTeal + tint = LocalCustomColorsPalette.current.magiskTeal, ) }, title = stringResource(R.string.pro_mode_root_detected_title), @@ -326,7 +326,7 @@ private fun LoadedContent( }, buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service), onButtonClick = onRootButtonClick, - enabled = state.isNotificationPermissionGranted + enabled = state.isNotificationPermissionGranted, ) Spacer(modifier = Modifier.height(8.dp)) @@ -360,7 +360,7 @@ private fun LoadedContent( }, buttonText = shizukuButtonText, onButtonClick = onShizukuButtonClick, - enabled = state.isNotificationPermissionGranted + enabled = state.isNotificationPermissionGranted, ) } @@ -387,7 +387,7 @@ private fun LoadedContent( content = {}, buttonText = setupKeyMapperText, onButtonClick = onSetupWithKeyMapperClick, - enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && state.isNotificationPermissionGranted + enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && state.isNotificationPermissionGranted, ) } } @@ -409,7 +409,7 @@ private fun LoadedContent( text = stringResource(R.string.summary_pref_pro_mode_auto_start_at_boot), icon = Icons.Rounded.RestartAlt, isChecked = autoStartAtBoot, - onCheckedChange = onAutoStartAtBootToggled + onCheckedChange = onAutoStartAtBootToggled, ) } } @@ -429,7 +429,7 @@ private fun WarningCard( OutlinedCard( modifier = modifier, border = borderStroke, - elevation = CardDefaults.elevatedCardElevation() + elevation = CardDefaults.elevatedCardElevation(), ) { Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.padding(horizontal = 16.dp)) { @@ -498,14 +498,14 @@ private fun ProModeStartedCard( ) { OutlinedCard(modifier) { Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Spacer(modifier = Modifier.width(16.dp)) Icon( imageVector = Icons.Rounded.Check, contentDescription = null, - tint = LocalCustomColorsPalette.current.green + tint = LocalCustomColorsPalette.current.green, ) Spacer(modifier = Modifier.width(16.dp)) @@ -515,14 +515,14 @@ private fun ProModeStartedCard( .weight(1f) .padding(vertical = 8.dp), text = stringResource(R.string.pro_mode_service_started), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) Spacer(modifier = Modifier.width(16.dp)) TextButton( onClick = onStopClick, - colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error), ) { Text(stringResource(R.string.pro_mode_stop_service_button)) } @@ -541,7 +541,7 @@ private fun SetupCard( content: @Composable () -> Unit, buttonText: String, onButtonClick: () -> Unit = {}, - enabled: Boolean = true + enabled: Boolean = true, ) { OutlinedCard(modifier = modifier) { Spacer(modifier = Modifier.height(16.dp)) @@ -590,12 +590,12 @@ private fun SetupCard( @Composable private fun EmergencyTipCard( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { OutlinedCard( modifier = modifier, border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary), - elevation = CardDefaults.elevatedCardElevation() + elevation = CardDefaults.elevatedCardElevation(), ) { Spacer(modifier = Modifier.height(16.dp)) Row(modifier = Modifier.padding(horizontal = 16.dp)) { @@ -610,7 +610,7 @@ private fun EmergencyTipCard( Text( text = stringResource(R.string.pro_mode_emergency_tip_title), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } @@ -629,18 +629,18 @@ private fun EmergencyTipCard( @Composable private fun ProModeInfoCard( modifier: Modifier = Modifier, - onDismiss: () -> Unit = {} + onDismiss: () -> Unit = {}, ) { OutlinedCard( modifier = modifier, border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), - elevation = CardDefaults.elevatedCardElevation() + elevation = CardDefaults.elevatedCardElevation(), ) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), - verticalAlignment = Alignment.Top + verticalAlignment = Alignment.Top, ) { Column(modifier = Modifier.weight(1f)) { Row { @@ -655,7 +655,7 @@ private fun ProModeInfoCard( Text( text = stringResource(R.string.pro_mode_info_card_title), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } @@ -671,12 +671,12 @@ private fun ProModeInfoCard( IconButton( onClick = onDismiss, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(24.dp), ) { Icon( imageVector = Icons.Rounded.Close, contentDescription = stringResource(R.string.pro_mode_info_card_dismiss_content_description), - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -695,12 +695,12 @@ private fun Preview() { isRootGranted = false, shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED, isNotificationPermissionGranted = true, - ) + ), ), showInfoCard = true, onInfoCardDismiss = {}, autoStartAtBoot = false, - onAutoStartAtBootToggled = {} + onAutoStartAtBootToggled = {}, ) } } @@ -717,7 +717,7 @@ private fun PreviewDark() { showInfoCard = false, onInfoCardDismiss = {}, autoStartAtBoot = true, - onAutoStartAtBootToggled = {} + onAutoStartAtBootToggled = {}, ) } } @@ -736,7 +736,7 @@ private fun PreviewCountingDown() { showInfoCard = true, onInfoCardDismiss = {}, autoStartAtBoot = false, - onAutoStartAtBootToggled = {} + onAutoStartAtBootToggled = {}, ) } } @@ -753,7 +753,7 @@ private fun PreviewStarted() { showInfoCard = false, onInfoCardDismiss = {}, autoStartAtBoot = false, - onAutoStartAtBootToggled = {} + onAutoStartAtBootToggled = {}, ) } } @@ -771,12 +771,12 @@ private fun PreviewNotificationPermissionNotGranted() { isRootGranted = true, shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED, isNotificationPermissionGranted = false, - ) + ), ), showInfoCard = false, onInfoCardDismiss = {}, autoStartAtBoot = false, - onAutoStartAtBootToggled = {} + onAutoStartAtBootToggled = {}, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt index 38796eb242..1e597b1581 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -71,7 +71,7 @@ fun ProModeSetupScreen( onStepButtonClick = viewModel::onStepButtonClick, onAssistantClick = viewModel::onAssistantClick, onWatchTutorialClick = { }, - onBackClick = viewModel::onBackClick + onBackClick = viewModel::onBackClick, ) } @@ -82,7 +82,7 @@ fun ProModeSetupScreen( onBackClick: () -> Unit = {}, onStepButtonClick: () -> Unit = {}, onAssistantClick: () -> Unit = {}, - onWatchTutorialClick: () -> Unit = {} + onWatchTutorialClick: () -> Unit = {}, ) { Scaffold( topBar = { @@ -92,12 +92,12 @@ fun ProModeSetupScreen( IconButton(onClick = onBackClick) { Icon( imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = stringResource(id = R.string.action_go_back) + contentDescription = stringResource(id = R.string.action_go_back), ) } - } + }, ) - } + }, ) { paddingValues -> when (state) { State.Loading -> { @@ -105,7 +105,7 @@ fun ProModeSetupScreen( Modifier .padding(paddingValues) .fillMaxSize(), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } @@ -124,8 +124,8 @@ fun ProModeSetupScreen( targetValue = targetProgress, animationSpec = tween( durationMillis = 800, - easing = EaseInOut - ) + easing = EaseInOut, + ), ) } @@ -135,8 +135,8 @@ fun ProModeSetupScreen( targetValue = targetProgress, animationSpec = tween( durationMillis = 1000, - easing = EaseInOut - ) + easing = EaseInOut, + ), ) } @@ -145,11 +145,11 @@ fun ProModeSetupScreen( .fillMaxSize() .padding(paddingValues) .padding(vertical = 16.dp, horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { LinearProgressIndicator( modifier = Modifier.fillMaxWidth(), - progress = { progressAnimatable.value } + progress = { progressAnimatable.value }, ) Spacer(modifier = Modifier.height(16.dp)) @@ -157,19 +157,19 @@ fun ProModeSetupScreen( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = stringResource( R.string.pro_mode_setup_wizard_step_n, state.data.stepNumber, - state.data.stepCount + state.data.stepCount, ), - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, ) Text( text = stringResource(R.string.pro_mode_app_bar_title), - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, ) } Spacer(modifier = Modifier.height(16.dp)) @@ -178,7 +178,7 @@ fun ProModeSetupScreen( modifier = Modifier.fillMaxWidth(), isEnabled = state.data.isSetupAssistantButtonEnabled, isChecked = state.data.isSetupAssistantChecked, - onAssistantClick = onAssistantClick + onAssistantClick = onAssistantClick, ) val iconTint = if (state.data.step == SystemBridgeSetupStep.STARTED) { @@ -195,7 +195,7 @@ fun ProModeSetupScreen( stepContent, onWatchTutorialClick, onStepButtonClick, - iconTint = iconTint + iconTint = iconTint, ) } } @@ -220,13 +220,13 @@ private fun StepContent( .fillMaxWidth() .weight(1f), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( modifier = Modifier.size(64.dp), imageVector = stepContent.icon, contentDescription = null, - tint = iconTint + tint = iconTint, ) Spacer(modifier = Modifier.height(16.dp)) @@ -234,7 +234,7 @@ private fun StepContent( Text( text = stepContent.title, style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(16.dp)) @@ -242,7 +242,7 @@ private fun StepContent( Text( text = stepContent.message, textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) } @@ -251,7 +251,7 @@ private fun StepContent( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { // TextButton(onClick = onWatchTutorialClick) { // Text(text = stringResource(R.string.pro_mode_setup_wizard_watch_tutorial_button)) @@ -268,12 +268,13 @@ private fun AssistantCheckBoxRow( modifier: Modifier, isEnabled: Boolean, isChecked: Boolean, - onAssistantClick: () -> Unit + onAssistantClick: () -> Unit, ) { Surface( - modifier = modifier, shape = MaterialTheme.shapes.medium, + modifier = modifier, + shape = MaterialTheme.shapes.medium, enabled = isEnabled, - onClick = onAssistantClick + onClick = onAssistantClick, ) { val contentColor = if (isEnabled) { LocalContentColor.current @@ -284,16 +285,17 @@ private fun AssistantCheckBoxRow( CompositionLocalProvider(LocalContentColor provides contentColor) { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Checkbox( enabled = isEnabled, checked = isChecked, - onCheckedChange = { onAssistantClick() }) + onCheckedChange = { onAssistantClick() }, + ) Column { Text( text = stringResource(R.string.pro_mode_setup_wizard_use_assistant), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) val text = if (isEnabled) { @@ -304,7 +306,7 @@ private fun AssistantCheckBoxRow( Text( text = text, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) } } @@ -319,56 +321,56 @@ private fun getStepContent(step: SystemBridgeSetupStep): StepContent { title = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_title), message = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_description), icon = Icons.Rounded.Accessibility, - buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_button) + buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_button), ) SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> StepContent( title = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_title), message = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_description), icon = Icons.Rounded.Notifications, - buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_button) + buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_button), ) SystemBridgeSetupStep.DEVELOPER_OPTIONS -> StepContent( title = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_title), message = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_description), icon = Icons.Rounded.Build, - buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button), ) SystemBridgeSetupStep.WIFI_NETWORK -> StepContent( title = stringResource(R.string.pro_mode_setup_wizard_connect_wifi_title), message = stringResource(R.string.pro_mode_setup_wizard_connect_wifi_description), icon = KeyMapperIcons.SignalWifiNotConnected, - buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button), ) SystemBridgeSetupStep.WIRELESS_DEBUGGING -> StepContent( title = stringResource(R.string.pro_mode_setup_wizard_enable_wireless_debugging_title), message = stringResource(R.string.pro_mode_setup_wizard_enable_wireless_debugging_description), icon = Icons.Rounded.BugReport, - buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button), ) SystemBridgeSetupStep.ADB_PAIRING -> StepContent( title = stringResource(R.string.pro_mode_setup_wizard_pair_wireless_debugging_title), message = stringResource(R.string.pro_mode_setup_wizard_pair_wireless_debugging_description), icon = Icons.Rounded.Link, - buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button) + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button), ) SystemBridgeSetupStep.START_SERVICE -> StepContent( title = stringResource(R.string.pro_mode_setup_wizard_start_service_title), message = stringResource(R.string.pro_mode_setup_wizard_start_service_description), icon = Icons.Rounded.PlayArrow, - buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service) + buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service), ) SystemBridgeSetupStep.STARTED -> StepContent( title = stringResource(R.string.pro_mode_setup_wizard_complete_title), message = stringResource(R.string.pro_mode_setup_wizard_complete_text), icon = Icons.Rounded.CheckCircleOutline, - buttonText = stringResource(R.string.pro_mode_setup_wizard_complete_button) + buttonText = stringResource(R.string.pro_mode_setup_wizard_complete_button), ) } } @@ -391,9 +393,9 @@ private fun ProModeSetupScreenAccessibilityServicePreview() { stepCount = 6, step = SystemBridgeSetupStep.ACCESSIBILITY_SERVICE, isSetupAssistantChecked = false, - isSetupAssistantButtonEnabled = false - ) - ) + isSetupAssistantButtonEnabled = false, + ), + ), ) } } @@ -409,9 +411,9 @@ private fun ProModeSetupScreenNotificationPermissionPreview() { stepCount = 6, step = SystemBridgeSetupStep.NOTIFICATION_PERMISSION, isSetupAssistantChecked = false, - isSetupAssistantButtonEnabled = true - ) - ) + isSetupAssistantButtonEnabled = true, + ), + ), ) } } @@ -427,9 +429,9 @@ private fun ProModeSetupScreenDeveloperOptionsPreview() { stepCount = 6, step = SystemBridgeSetupStep.DEVELOPER_OPTIONS, isSetupAssistantChecked = false, - isSetupAssistantButtonEnabled = true - ) - ) + isSetupAssistantButtonEnabled = true, + ), + ), ) } } @@ -445,9 +447,9 @@ private fun ProModeSetupScreenWifiNetworkPreview() { stepCount = 6, step = SystemBridgeSetupStep.WIFI_NETWORK, isSetupAssistantChecked = false, - isSetupAssistantButtonEnabled = true - ) - ) + isSetupAssistantButtonEnabled = true, + ), + ), ) } } @@ -463,9 +465,9 @@ private fun ProModeSetupScreenWirelessDebuggingPreview() { stepCount = 6, step = SystemBridgeSetupStep.WIRELESS_DEBUGGING, isSetupAssistantChecked = false, - isSetupAssistantButtonEnabled = true - ) - ) + isSetupAssistantButtonEnabled = true, + ), + ), ) } } @@ -481,9 +483,9 @@ private fun ProModeSetupScreenAdbPairingPreview() { stepCount = 6, step = SystemBridgeSetupStep.ADB_PAIRING, isSetupAssistantChecked = true, - isSetupAssistantButtonEnabled = true - ) - ) + isSetupAssistantButtonEnabled = true, + ), + ), ) } } @@ -499,14 +501,13 @@ private fun ProModeSetupScreenStartServicePreview() { stepCount = 6, step = SystemBridgeSetupStep.START_SERVICE, isSetupAssistantChecked = true, - isSetupAssistantButtonEnabled = true - ) - ) + isSetupAssistantButtonEnabled = true, + ), + ), ) } } - @Preview(name = "Started", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ProModeSetupScreenStartedPreview() { @@ -518,21 +519,19 @@ private fun ProModeSetupScreenStartedPreview() { stepCount = 8, step = SystemBridgeSetupStep.STARTED, isSetupAssistantChecked = true, - isSetupAssistantButtonEnabled = true - ) - ) + isSetupAssistantButtonEnabled = true, + ), + ), ) } } - @Preview(name = "Loading", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ProModeSetupScreenLoadingPreview() { KeyMapperTheme { ProModeSetupScreen( - state = State.Loading + state = State.Loading, ) } } - diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt index 20e97df8e0..442089f9f9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt @@ -19,13 +19,13 @@ import javax.inject.Inject class ProModeSetupViewModel @Inject constructor( private val useCase: SystemBridgeSetupUseCase, navigationProvider: NavigationProvider, - resourceProvider: ResourceProvider + resourceProvider: ResourceProvider, ) : ViewModel(), NavigationProvider by navigationProvider, ResourceProvider by resourceProvider { val setupState: StateFlow> = combine(useCase.nextSetupStep, useCase.isSetupAssistantEnabled, ::buildState).stateIn( viewModelScope, SharingStarted.Eagerly, - State.Loading + State.Loading, ) fun onStepButtonClick() { @@ -57,7 +57,7 @@ class ProModeSetupViewModel @Inject constructor( private fun buildState( step: SystemBridgeSetupStep, - isSetupAssistantUserEnabled: Boolean + isSetupAssistantUserEnabled: Boolean, ): State.Data { // Uncheck the setup assistant if the accessibility service is disabled since it is // required for the setup assistant to work @@ -73,8 +73,8 @@ class ProModeSetupViewModel @Inject constructor( stepCount = SystemBridgeSetupStep.entries.size, step = step, isSetupAssistantChecked = isSetupAssistantChecked, - isSetupAssistantButtonEnabled = step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE && step != SystemBridgeSetupStep.STARTED - ) + isSetupAssistantButtonEnabled = step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE && step != SystemBridgeSetupStep.STARTED, + ), ) } } @@ -84,5 +84,5 @@ data class ProModeSetupState( val stepCount: Int, val step: SystemBridgeSetupStep, val isSetupAssistantChecked: Boolean, - val isSetupAssistantButtonEnabled: Boolean -) \ No newline at end of file + val isSetupAssistantButtonEnabled: Boolean, +) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt index 72dfb38980..70fb145df6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -59,7 +59,7 @@ class ProModeViewModel @Inject constructor( useCase.isRootGranted, useCase.shizukuSetupState, useCase.isNotificationPermissionGranted, - ::buildSetupState + ::buildSetupState, ).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) val autoStartBootEnabled: StateFlow = @@ -153,7 +153,7 @@ class ProModeViewModel @Inject constructor( isSystemBridgeConnected: Boolean, isRootGranted: Boolean, shizukuSetupState: ShizukuSetupState, - isNotificationPermissionGranted: Boolean + isNotificationPermissionGranted: Boolean, ): State { if (isSystemBridgeConnected) { return State.Data(ProModeState.Started) @@ -163,7 +163,7 @@ class ProModeViewModel @Inject constructor( isRootGranted = isRootGranted, shizukuSetupState = shizukuSetupState, isNotificationPermissionGranted = isNotificationPermissionGranted, - ) + ), ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt index 69077c5102..1c9814a519 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt @@ -4,5 +4,5 @@ enum class ShizukuSetupState { NOT_FOUND, INSTALLED, STARTED, - PERMISSION_GRANTED -} \ No newline at end of file + PERMISSION_GRANTED, +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt index 38429d3244..5c7225683b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt @@ -59,10 +59,12 @@ class SystemBridgeAutoStarter @Inject constructor( private val networkAdapter: NetworkAdapter, private val permissionAdapter: PermissionAdapter, private val notificationAdapter: NotificationAdapter, - private val resourceProvider: ResourceProvider + private val resourceProvider: ResourceProvider, ) : ResourceProvider by resourceProvider { enum class AutoStartType { - ADB, SHIZUKU, ROOT + ADB, + SHIZUKU, + ROOT, } // Use flatMapLatest so that any calls to ADB are only done if strictly necessary. @@ -78,7 +80,7 @@ class SystemBridgeAutoStarter @Inject constructor( } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { combine( permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), - networkAdapter.isWifiConnected + networkAdapter.isWifiConnected, ) { isWriteSecureSettingsGranted, isWifiConnected -> isWriteSecureSettingsGranted && isWifiConnected && setupController.isAdbPaired() }.distinctUntilChanged() @@ -225,7 +227,7 @@ class SystemBridgeAutoStarter @Inject constructor( priority = NotificationCompat.PRIORITY_MAX, onGoing = true, showIndeterminateProgress = true, - showOnLockscreen = false + showOnLockscreen = false, ) notificationAdapter.showNotification(model) @@ -247,4 +249,4 @@ class SystemBridgeAutoStarter @Inject constructor( notificationAdapter.showNotification(model) } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index 09b66a1956..059c165387 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -45,7 +45,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import timber.log.Timber - @Suppress("KotlinConstantConditions") @RequiresApi(Build.VERSION_CODES.Q) class SystemBridgeSetupAssistantController @AssistedInject constructor( @@ -58,13 +57,13 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private val preferenceRepository: PreferenceRepository, private val systemBridgeConnectionManager: SystemBridgeConnectionManager, private val keyMapperClassProvider: KeyMapperClassProvider, - resourceProvider: ResourceProvider + resourceProvider: ResourceProvider, ) : ResourceProvider by resourceProvider { @AssistedFactory interface Factory { fun create( coroutineScope: CoroutineScope, - accessibilityService: BaseAccessibilityService + accessibilityService: BaseAccessibilityService, ): SystemBridgeSetupAssistantController } @@ -93,7 +92,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( .stateIn( coroutineScope, SharingStarted.Eagerly, - PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT + PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT, ) private var interactionStep: InteractionStep? = null @@ -139,7 +138,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( val notificationChannel = NotificationChannelModel( id = NotificationController.Companion.CHANNEL_SETUP_ASSISTANT, name = getString(R.string.pro_mode_setup_assistant_notification_channel), - importance = NotificationManagerCompat.IMPORTANCE_MAX + importance = NotificationManagerCompat.IMPORTANCE_MAX, ) manageNotifications.createChannel(notificationChannel) } @@ -206,7 +205,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( foundPairingCode = pairingCode - Timber.i("Pairing code found ${pairingCode}. Pairing ADB...") + Timber.i("Pairing code found $pairingCode. Pairing ADB...") setupController.pairWirelessAdb(pairingCode).onSuccess { onPairingSuccess() }.onFailure { @@ -216,7 +215,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( showNotification( getString(R.string.pro_mode_setup_notification_invalid_pairing_code_title), getString(R.string.pro_mode_setup_notification_invalid_pairing_code_text), - actions = listOf(KMNotificationAction.RemoteInput.PairingCode to getString(R.string.pro_mode_setup_notification_action_input_pairing_code)) + actions = listOf(KMNotificationAction.RemoteInput.PairingCode to getString(R.string.pro_mode_setup_notification_action_input_pairing_code)), ) } } @@ -245,7 +244,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( showNotification( getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_title), getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_text), - onClickAction = KMNotificationAction.Activity.MainActivity(BaseMainActivity.ACTION_START_SYSTEM_BRIDGE) + onClickAction = KMNotificationAction.Activity.MainActivity(BaseMainActivity.ACTION_START_SYSTEM_BRIDGE), ) } } @@ -275,7 +274,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( title: String, text: String, onClickAction: KMNotificationAction? = null, - actions: List> = emptyList() + actions: List> = emptyList(), ) { val notification = NotificationModel( // Use the same notification id for all so they overwrite each other. @@ -291,7 +290,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( bigTextStyle = true, // Must not be silent so it is shown as a heads up notification silent = false, - actions = actions + actions = actions, ) manageNotifications.show(notification) } @@ -342,7 +341,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( showNotification( title = getString(R.string.pro_mode_setup_notification_pairing_button_not_found_title), text = getString(R.string.pro_mode_setup_notification_pairing_button_not_found_text), - actions = listOf(KMNotificationAction.RemoteInput.PairingCode to getString(R.string.pro_mode_setup_notification_action_input_pairing_code)) + actions = listOf(KMNotificationAction.RemoteInput.PairingCode to getString(R.string.pro_mode_setup_notification_action_input_pairing_code)), ) // Give the user 30 seconds to input the pairing code and then dismiss the notification. @@ -366,4 +365,4 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( .firstOrNull { it.taskInfo.topActivity?.className == keyMapperClassProvider.getMainActivity().name } return task } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt index 16ddd87581..11a2ff26a0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import javax.inject.Inject - @RequiresApi(Build.VERSION_CODES.Q) @ViewModelScoped class SystemBridgeSetupUseCaseImpl @Inject constructor( @@ -40,7 +39,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( private val shizukuAdapter: ShizukuAdapter, private val permissionAdapter: PermissionAdapter, private val accessibilityServiceAdapter: AccessibilityServiceAdapter, - private val networkAdapter: NetworkAdapter + private val networkAdapter: NetworkAdapter, ) : SystemBridgeSetupUseCase { override val isWarningUnderstood: Flow = preferences.get(Keys.isProModeWarningUnderstood).map { it ?: false } @@ -49,7 +48,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { combine( permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), - networkAdapter.isWifiConnected + networkAdapter.isWifiConnected, ) { isWriteSecureSettingsGranted, isWifiConnected -> isWriteSecureSettingsGranted && isWifiConnected && systemBridgeSetupController.isAdbPaired() }.flowOn(Dispatchers.IO) @@ -100,7 +99,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( systemBridgeSetupController.isDeveloperOptionsEnabled, networkAdapter.isWifiConnected, systemBridgeSetupController.isWirelessDebuggingEnabled, - ::getNextStep + ::getNextStep, ) } } @@ -112,7 +111,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( override val shizukuSetupState: Flow = combine( shizukuAdapter.isInstalled, shizukuAdapter.isStarted, - permissionAdapter.isGrantedFlow(Permission.SHIZUKU) + permissionAdapter.isGrantedFlow(Permission.SHIZUKU), ) { isInstalled, isStarted, isPermissionGranted -> when { isPermissionGranted -> ShizukuSetupState.PERMISSION_GRANTED @@ -214,7 +213,6 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( else -> SystemBridgeSetupStep.START_SERVICE } } - } interface SystemBridgeSetupUseCase { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt index 97e1b9d3fb..f9c3af7d16 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt @@ -50,7 +50,7 @@ fun AutomaticChangeImeSettingsScreen(modifier: Modifier = Modifier, viewModel: S AutomaticChangeImeSettingsScreen( modifier, onBackClick = viewModel::onBackClick, - snackbarHostState = snackbarHostState + snackbarHostState = snackbarHostState, ) { Content( state = state, @@ -59,7 +59,7 @@ fun AutomaticChangeImeSettingsScreen(modifier: Modifier = Modifier, viewModel: S onChangeImeOnDeviceConnectToggled = viewModel::onChangeImeOnDeviceConnectToggled, onDevicesThatChangeImeClick = viewModel::onDevicesThatChangeImeClick, onToggleKeyboardOnToggleKeymapsToggled = viewModel::onToggleKeyboardOnToggleKeymapsToggled, - onShowToggleKeyboardNotificationClick = viewModel::onShowToggleKeyboardNotificationClick + onShowToggleKeyboardNotificationClick = viewModel::onShowToggleKeyboardNotificationClick, ) } } @@ -70,14 +70,14 @@ private fun AutomaticChangeImeSettingsScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit = {}, snackbarHostState: SnackbarHostState = SnackbarHostState(), - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Scaffold( modifier = modifier.displayCutoutPadding(), snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( - title = { Text(stringResource(R.string.title_pref_automatically_change_ime)) } + title = { Text(stringResource(R.string.title_pref_automatically_change_ime)) }, ) }, bottomBar = { @@ -124,7 +124,7 @@ private fun Content( Column( modifier .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp), ) { Spacer(modifier = Modifier.height(8.dp)) @@ -133,7 +133,7 @@ private fun Content( text = stringResource(R.string.summary_pref_auto_change_ime_on_input_focus), icon = Icons.Rounded.SwapHoriz, isChecked = state.changeImeOnInputFocus, - onCheckedChange = onChangeImeOnInputFocusToggled + onCheckedChange = onChangeImeOnInputFocusToggled, ) Spacer(modifier = Modifier.height(8.dp)) @@ -143,7 +143,7 @@ private fun Content( text = stringResource(R.string.summary_pref_auto_change_ime_on_connection), icon = Icons.Rounded.SwapHoriz, isChecked = state.changeImeOnDeviceConnect, - onCheckedChange = onChangeImeOnDeviceConnectToggled + onCheckedChange = onChangeImeOnDeviceConnectToggled, ) Spacer(modifier = Modifier.height(8.dp)) @@ -152,7 +152,7 @@ private fun Content( title = stringResource(R.string.title_pref_automatically_change_ime_choose_devices), text = stringResource(R.string.summary_pref_automatically_change_ime_choose_devices), icon = Icons.Rounded.Devices, - onClick = onDevicesThatChangeImeClick + onClick = onDevicesThatChangeImeClick, ) Spacer(modifier = Modifier.height(8.dp)) @@ -162,7 +162,7 @@ private fun Content( text = stringResource(R.string.summary_pref_toggle_keyboard_on_toggle_keymaps), icon = Icons.Rounded.SwapHoriz, isChecked = state.toggleKeyboardOnToggleKeymaps, - onCheckedChange = onToggleKeyboardOnToggleKeymapsToggled + onCheckedChange = onToggleKeyboardOnToggleKeymapsToggled, ) Spacer(modifier = Modifier.height(8.dp)) @@ -170,7 +170,7 @@ private fun Content( OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Outlined.Notifications, - text = stringResource(R.string.settings_section_notifications) + text = stringResource(R.string.settings_section_notifications), ) Spacer(modifier = Modifier.height(8.dp)) @@ -180,7 +180,7 @@ private fun Content( text = stringResource(R.string.summary_pref_show_toast_when_auto_changing_ime), icon = Icons.Rounded.Notifications, isChecked = state.showToastWhenAutoChangingIme, - onCheckedChange = onShowToastWhenAutoChangingImeToggled + onCheckedChange = onShowToastWhenAutoChangingImeToggled, ) Spacer(modifier = Modifier.height(8.dp)) @@ -190,7 +190,7 @@ private fun Content( title = stringResource(R.string.title_pref_show_toggle_keyboard_notification), text = stringResource(R.string.summary_pref_show_toggle_keyboard_notification), icon = Icons.Rounded.Notifications, - onClick = onShowToggleKeyboardNotificationClick + onClick = onShowToggleKeyboardNotificationClick, ) } else { // For older Android versions, this would be a switch but since we're targeting newer versions @@ -199,7 +199,7 @@ private fun Content( title = stringResource(R.string.title_pref_show_toggle_keyboard_notification), text = stringResource(R.string.summary_pref_show_toggle_keyboard_notification), icon = Icons.Rounded.Notifications, - onClick = onShowToggleKeyboardNotificationClick + onClick = onShowToggleKeyboardNotificationClick, ) } @@ -213,7 +213,7 @@ private fun Preview() { KeyMapperTheme { AutomaticChangeImeSettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { Content( - state = AutomaticChangeImeSettingsState() + state = AutomaticChangeImeSettingsState(), ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index 51783a0e22..eb274210bc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -38,7 +38,7 @@ class ConfigSettingsUseCaseImpl @Inject constructor( private val shizukuAdapter: ShizukuAdapter, private val devicesAdapter: DevicesAdapter, private val buildConfigProvider: BuildConfigProvider, - private val notificationAdapter: NotificationAdapter + private val notificationAdapter: NotificationAdapter, ) : ConfigSettingsUseCase { private val imeHelper by lazy { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt index edcd149615..bdad749bad 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt @@ -46,7 +46,7 @@ fun DefaultOptionsSettingsScreen(modifier: Modifier = Modifier, viewModel: Setti ) { Content( state = state, - callback = viewModel + callback = viewModel, ) } } @@ -56,7 +56,7 @@ fun DefaultOptionsSettingsScreen(modifier: Modifier = Modifier, viewModel: Setti fun DefaultOptionsSettingsScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit = {}, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Scaffold( modifier = modifier.displayCutoutPadding(), @@ -215,7 +215,7 @@ private fun Preview() { KeyMapperTheme { DefaultOptionsSettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { Content( - state = DefaultSettingsState() + state = DefaultSettingsState(), ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 3be4ee87ee..d657cd8b48 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -112,7 +112,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) snackbarHostState.showSnackbar(activityNotFoundText) } } - } + }, ) { Text(stringResource(R.string.pos_change_location)) } @@ -122,11 +122,11 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onClick = { showAutomaticBackupDialog = false viewModel.disableAutomaticBackup() - } + }, ) { Text(stringResource(R.string.neg_turn_off)) } - } + }, ) } @@ -134,7 +134,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) modifier, onBackClick = viewModel::onBackClick, viewModel::onResetAllSettingsClick, - snackbarHostState = snackbarHostState + snackbarHostState = snackbarHostState, ) { Content( state = state, @@ -149,8 +149,8 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) snackbarHostState.showSnackbar( context.getString( R.string.error_sdk_version_too_low, - BuildUtils.getSdkVersionName(Build.VERSION_CODES.Q) - ) + BuildUtils.getSdkVersionName(Build.VERSION_CODES.Q), + ), ) } } @@ -167,7 +167,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) } else { showAutomaticBackupDialog = true } - } + }, ) } } @@ -179,7 +179,7 @@ private fun SettingsScreen( onBackClick: () -> Unit = {}, onResetClick: () -> Unit = {}, snackbarHostState: SnackbarHostState = SnackbarHostState(), - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Scaffold( modifier = modifier.displayCutoutPadding(), @@ -190,11 +190,11 @@ private fun SettingsScreen( actions = { OutlinedButton( modifier = Modifier.padding(horizontal = 16.dp), - onClick = onResetClick + onClick = onResetClick, ) { Text(stringResource(R.string.settings_reset_app_bar_button)) } - } + }, ) }, bottomBar = { @@ -246,21 +246,21 @@ private fun Content( Column( modifier .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp), ) { Spacer(modifier = Modifier.height(8.dp)) OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = KeyMapperIcons.WandStars, - text = stringResource(R.string.settings_section_customize_experience_title) + text = stringResource(R.string.settings_section_customize_experience_title), ) Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(R.string.title_pref_dark_theme), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, ) val buttonStates: List> = listOf( @@ -284,7 +284,7 @@ private fun Content( title = stringResource(R.string.title_pref_show_toggle_keymaps_notification), text = stringResource(R.string.summary_pref_show_toggle_keymaps_notification), icon = Icons.Rounded.PlayCircleOutline, - onClick = onPauseResumeNotificationClick + onClick = onPauseResumeNotificationClick, ) Spacer(modifier = Modifier.height(8.dp)) @@ -294,7 +294,7 @@ private fun Content( text = stringResource(R.string.summary_pref_hide_home_screen_alerts), icon = Icons.Rounded.VisibilityOff, isChecked = state.hideHomeScreenAlerts, - onCheckedChange = onHideHomeScreenAlertsToggled + onCheckedChange = onHideHomeScreenAlertsToggled, ) Spacer(modifier = Modifier.height(8.dp)) @@ -302,7 +302,7 @@ private fun Content( OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Outlined.Gamepad, - text = stringResource(R.string.settings_section_key_maps_title) + text = stringResource(R.string.settings_section_key_maps_title), ) Spacer(modifier = Modifier.height(8.dp)) @@ -311,7 +311,7 @@ private fun Content( title = stringResource(R.string.title_pref_default_options), text = stringResource(R.string.summary_pref_default_options), icon = Icons.Rounded.Tune, - onClick = onDefaultOptionsClick + onClick = onDefaultOptionsClick, ) Spacer(modifier = Modifier.height(8.dp)) @@ -321,7 +321,7 @@ private fun Content( text = stringResource(R.string.summary_pref_force_vibrate), icon = Icons.Rounded.Vibration, isChecked = state.forceVibrate, - onCheckedChange = onForceVibrateToggled + onCheckedChange = onForceVibrateToggled, ) Spacer(modifier = Modifier.height(8.dp)) @@ -331,7 +331,7 @@ private fun Content( text = stringResource(R.string.summary_pref_show_device_descriptors), icon = Icons.Rounded.Devices, isChecked = state.showDeviceDescriptors, - onCheckedChange = onShowDeviceDescriptorsToggled + onCheckedChange = onShowDeviceDescriptorsToggled, ) Spacer(modifier = Modifier.height(8.dp)) @@ -339,7 +339,7 @@ private fun Content( OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = KeyMapperIcons.FolderManaged, - text = stringResource(R.string.settings_section_data_management_title) + text = stringResource(R.string.settings_section_data_management_title), ) Spacer(modifier = Modifier.height(8.dp)) @@ -353,7 +353,7 @@ private fun Content( text = state.autoBackupLocation ?: stringResource(R.string.summary_pref_automatic_backup_location_disabled), icon = Icons.Rounded.Tune, - onClick = onAutomaticBackupClick + onClick = onAutomaticBackupClick, ) Spacer(modifier = Modifier.height(8.dp)) @@ -361,7 +361,7 @@ private fun Content( OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Rounded.Construction, - text = stringResource(R.string.settings_section_power_user_title) + text = stringResource(R.string.settings_section_power_user_title), ) Spacer(modifier = Modifier.height(8.dp)) @@ -377,18 +377,18 @@ private fun Content( } else { stringResource( R.string.error_sdk_version_too_low, - BuildUtils.getSdkVersionName(Build.VERSION_CODES.Q) + BuildUtils.getSdkVersionName(Build.VERSION_CODES.Q), ) }, icon = KeyMapperIcons.ProModeIcon, - onClick = onProModeClick + onClick = onProModeClick, ) OptionPageButton( title = stringResource(R.string.title_pref_automatically_change_ime), text = stringResource(R.string.summary_pref_automatically_change_ime), icon = Icons.Rounded.Keyboard, - onClick = onAutomaticChangeImeClick + onClick = onAutomaticChangeImeClick, ) Spacer(modifier = Modifier.height(8.dp)) @@ -396,7 +396,7 @@ private fun Content( OptionsHeaderRow( modifier = Modifier.fillMaxWidth(), icon = Icons.Rounded.Code, - text = stringResource(R.string.settings_section_debugging_title) + text = stringResource(R.string.settings_section_debugging_title), ) Spacer(modifier = Modifier.height(8.dp)) @@ -406,7 +406,7 @@ private fun Content( text = stringResource(R.string.summary_pref_toggle_logging), icon = Icons.Outlined.BugReport, isChecked = state.loggingEnabled, - onCheckedChange = onLoggingToggled + onCheckedChange = onLoggingToggled, ) Spacer(modifier = Modifier.height(8.dp)) @@ -415,11 +415,10 @@ private fun Content( title = stringResource(R.string.title_pref_view_and_share_log), text = stringResource(R.string.summary_pref_view_and_share_log), icon = Icons.Outlined.FindInPage, - onClick = onViewLogClick + onClick = onViewLogClick, ) Spacer(modifier = Modifier.height(8.dp)) - } } @@ -429,8 +428,8 @@ private fun Preview() { KeyMapperTheme { SettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { Content( - state = MainSettingsState() + state = MainSettingsState(), ) } } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 48660d2f28..7f88595d6e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -67,7 +67,7 @@ class SettingsViewModel @Inject constructor( autoBackupLocation = values[2] as String?, forceVibrate = values[3] as Boolean? ?: false, hideHomeScreenAlerts = values[4] as Boolean? ?: false, - showDeviceDescriptors = values[5] as Boolean? ?: false + showDeviceDescriptors = values[5] as Boolean? ?: false, ) }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) @@ -146,7 +146,7 @@ class SettingsViewModel @Inject constructor( if (soundFiles.isEmpty()) { showDialog( "no sound files", - DialogModel.Toast(getString(R.string.toast_no_sound_files)) + DialogModel.Toast(getString(R.string.toast_no_sound_files)), ) return@launch } @@ -404,7 +404,7 @@ data class MainSettingsState( val forceVibrate: Boolean = false, val loggingEnabled: Boolean = false, val hideHomeScreenAlerts: Boolean = false, - val showDeviceDescriptors: Boolean = false + val showDeviceDescriptors: Boolean = false, ) data class DefaultSettingsState( @@ -432,4 +432,4 @@ data class AutomaticChangeImeSettingsState( val changeImeOnInputFocus: Boolean = PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS, val changeImeOnDeviceConnect: Boolean = false, val toggleKeyboardOnToggleKeymaps: Boolean = false, -) \ No newline at end of file +) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt index da513b5c79..0ef9163046 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt @@ -3,5 +3,5 @@ package io.github.sds100.keymapper.base.settings enum class Theme(val value: Int) { DARK(0), LIGHT(1), - AUTO(2); + AUTO(2), } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt index ed431008fb..134cf79287 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt @@ -42,9 +42,9 @@ import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.groups.GroupBreadcrumbRow import io.github.sds100.keymapper.base.groups.GroupListItemModel import io.github.sds100.keymapper.base.groups.GroupRow +import io.github.sds100.keymapper.base.home.KeyMapAppBarState import io.github.sds100.keymapper.base.home.KeyMapList import io.github.sds100.keymapper.base.home.KeyMapListState -import io.github.sds100.keymapper.base.home.KeyMapAppBarState import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.TriggerError import io.github.sds100.keymapper.base.utils.ui.UnsavedChangesDialog diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 4627eea10b..c3ef0be0f0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -64,7 +64,7 @@ abstract class BaseAccessibilityServiceController( private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, private val inputEventHub: InputEventHub, private val recordTriggerController: RecordTriggerController, - private val setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory + private val setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory, ) { companion object { private const val DEFAULT_NOTIFICATION_TIMEOUT = 200L diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt index 0c09d4a61e..ec320f82e5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt @@ -58,7 +58,6 @@ class AndroidNotificationAdapter @Inject constructor( } } } - } init { @@ -71,7 +70,7 @@ class AndroidNotificationAdapter @Inject constructor( ctx, broadcastReceiver, intentFilter, - ContextCompat.RECEIVER_EXPORTED + ContextCompat.RECEIVER_EXPORTED, ) } @@ -205,15 +204,15 @@ class AndroidNotificationAdapter @Inject constructor( } private fun createActionIntent( - notificationAction: KMNotificationAction + notificationAction: KMNotificationAction, ): PendingIntent { return when (notificationAction) { KMNotificationAction.Activity.AccessibilitySettings -> createActivityPendingIntent( - Settings.ACTION_ACCESSIBILITY_SETTINGS + Settings.ACTION_ACCESSIBILITY_SETTINGS, ) is KMNotificationAction.Activity.MainActivity -> createMainActivityPendingIntent( - notificationAction.action + notificationAction.action, ) is KMNotificationAction.Broadcast -> createBroadcastPendingIntent(notificationAction.intentAction.name) @@ -230,7 +229,7 @@ class AndroidNotificationAdapter @Inject constructor( ctx, 0, intent, - PendingIntent.FLAG_MUTABLE + PendingIntent.FLAG_MUTABLE, ) } @@ -243,7 +242,7 @@ class AndroidNotificationAdapter @Inject constructor( ctx, 0, intent, - PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_IMMUTABLE, ) } @@ -254,7 +253,7 @@ class AndroidNotificationAdapter @Inject constructor( ctx, 0, intent, - PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_IMMUTABLE, ) } @@ -267,7 +266,7 @@ class AndroidNotificationAdapter @Inject constructor( ctx, 0, intent, - PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_IMMUTABLE, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 001530d7c3..3a730951cc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -158,7 +158,7 @@ class NotificationController @Inject constructor( KMNotificationAction.IntentAction.RESUME_KEY_MAPS -> pauseMappings.resume() KMNotificationAction.IntentAction.PAUSE_KEY_MAPS -> pauseMappings.pause() KMNotificationAction.IntentAction.DISMISS_TOGGLE_KEY_MAPS_NOTIFICATION -> manageNotifications.dismiss( - ID_TOGGLE_MAPPINGS + ID_TOGGLE_MAPPINGS, ) KMNotificationAction.IntentAction.STOP_ACCESSIBILITY_SERVICE -> controlAccessibilityService.stopService() @@ -267,7 +267,7 @@ class NotificationController @Inject constructor( actions = listOf( KMNotificationAction.Broadcast.ResumeKeyMaps to getString(R.string.notification_action_resume), KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), - stopServiceAction to getString(R.string.notification_action_stop_acc_service) + stopServiceAction to getString(R.string.notification_action_stop_acc_service), ), ) } @@ -295,7 +295,7 @@ class NotificationController @Inject constructor( actions = listOf( KMNotificationAction.Broadcast.PauseKeyMaps to getString(R.string.notification_action_pause), KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), - stopServiceAction to getString(R.string.notification_action_stop_acc_service) + stopServiceAction to getString(R.string.notification_action_stop_acc_service), ), ) } @@ -323,7 +323,7 @@ class NotificationController @Inject constructor( actions = listOf( KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), - ), + ), ) } @@ -349,7 +349,7 @@ class NotificationController @Inject constructor( priority = NotificationCompat.PRIORITY_MIN, bigTextStyle = true, actions = listOf( - restartServiceAction to getString(R.string.notification_action_restart_accessibility_service) + restartServiceAction to getString(R.string.notification_action_restart_accessibility_service), ), ) } @@ -364,7 +364,7 @@ class NotificationController @Inject constructor( onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - KMNotificationAction.Broadcast.TogglerKeyMapperIme to getString(R.string.notification_toggle_keyboard_action) + KMNotificationAction.Broadcast.TogglerKeyMapperIme to getString(R.string.notification_toggle_keyboard_action), ), ) @@ -394,7 +394,6 @@ class NotificationController @Inject constructor( bigTextStyle = true, ) - private fun showSystemBridgeStartedNotification() { val model = NotificationModel( id = ID_SYSTEM_BRIDGE_STATUS, @@ -405,10 +404,9 @@ class NotificationController @Inject constructor( onGoing = false, showOnLockscreen = false, autoCancel = true, - timeout = 5000 + timeout = 5000, ) manageNotifications.show(model) } - } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index d64141bab1..cabd960617 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -377,7 +377,7 @@ abstract class BaseConfigTriggerViewModel( keyCode = key.keyCode, scanCode = key.scanCode, isScanCodeDetectionSelected = key.detectWithScancode(), - isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable() + isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable(), ) } @@ -411,7 +411,7 @@ abstract class BaseConfigTriggerViewModel( keyCode = key.keyCode, scanCode = key.scanCode, isScanCodeDetectionSelected = key.detectWithScancode(), - isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable() + isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable(), ) } } @@ -639,7 +639,7 @@ abstract class BaseConfigTriggerViewModel( is RecordTriggerState.Completed, RecordTriggerState.Idle, - -> recordTrigger.startRecording() + -> recordTrigger.startRecording() } // Show dialog if the accessibility service is disabled or crashed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 7ca52b2538..8e2231106d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -97,7 +97,7 @@ fun BaseTriggerScreen(modifier: Modifier = Modifier, viewModel: BaseConfigTrigge onEditFloatingButtonClick = viewModel::onEditFloatingButtonClick, onEditFloatingLayoutClick = viewModel::onEditFloatingLayoutClick, onSelectFingerprintGestureType = viewModel::onSelectFingerprintGestureType, - onScanCodeDetectionChanged = viewModel::onSelectScanCodeDetection + onScanCodeDetectionChanged = viewModel::onSelectScanCodeDetection, ) } @@ -251,7 +251,7 @@ private fun TriggerScreenVertical( clickTypes = configState.clickTypeButtons, checkedClickType = configState.checkedClickType, onSelectClickType = onSelectClickType, - isCompact = isCompact + isCompact = isCompact, ) if (!isCompact) { @@ -401,7 +401,7 @@ private fun TriggerScreenHorizontal( clickTypes = configState.clickTypeButtons, checkedClickType = configState.checkedClickType, onSelectClickType = onSelectClickType, - isCompact = false + isCompact = false, ) } @@ -561,7 +561,7 @@ private fun ClickTypeSegmentedButtons( buttonStates = clickTypeButtonContent, selectedState = checkedClickType, onStateSelected = onSelectClickType, - isCompact = isCompact + isCompact = isCompact, ) } @@ -572,11 +572,11 @@ private fun TriggerModeSegmentedButtons( isEnabled: Boolean, onSelectParallelMode: () -> Unit, onSelectSequenceMode: () -> Unit, - isCompact: Boolean + isCompact: Boolean, ) { val triggerModeButtonContent = listOf( "parallel" to stringResource(R.string.radio_button_parallel), - "sequence" to stringResource(R.string.radio_button_sequence) + "sequence" to stringResource(R.string.radio_button_sequence), ) KeyMapperSegmentedButtonRow( @@ -594,7 +594,7 @@ private fun TriggerModeSegmentedButtons( } }, isCompact = isCompact, - isEnabled = isEnabled + isEnabled = isEnabled, ) } @@ -712,7 +712,7 @@ private fun HorizontalEmptyPreview() { ), ), - ), + ), recordTriggerState = RecordTriggerState.Idle, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index 02ea2de089..e2a72f7d22 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -33,7 +33,6 @@ class ConfigTriggerDelegate { return addTriggerKey(trigger, triggerKey) } - fun addAssistantTriggerKey(trigger: Trigger, type: AssistantTriggerType): Trigger { val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -75,7 +74,7 @@ class ConfigTriggerDelegate { scanCode: Int, device: KeyEventTriggerDevice, requiresIme: Boolean, - otherTriggerKeys: List = emptyList() + otherTriggerKeys: List = emptyList(), ): Trigger { val isPowerKey = isPowerButtonKey(keyCode, scanCode) @@ -101,9 +100,9 @@ class ConfigTriggerDelegate { val logicallyEqualKeys = otherTriggerKeys.plus(trigger.keys) .filterIsInstance() .filter { - it.keyCode == keyCode - && it.scanCode != scanCode - && it.device == device + it.keyCode == keyCode && + it.scanCode != scanCode && + it.device == device } val triggerKey = KeyEventTriggerKey( @@ -113,7 +112,7 @@ class ConfigTriggerDelegate { scanCode = scanCode, consumeEvent = consumeKeyEvent, requiresIme = requiresIme, - detectWithScanCodeUserSetting = logicallyEqualKeys.isNotEmpty() + detectWithScanCodeUserSetting = logicallyEqualKeys.isNotEmpty(), ) var newKeys = trigger.keys.filter { it !is EvdevTriggerKey } @@ -136,7 +135,7 @@ class ConfigTriggerDelegate { keyCode: Int, scanCode: Int, device: EvdevDeviceInfo, - otherTriggerKeys: List = emptyList() + otherTriggerKeys: List = emptyList(), ): Trigger { val isPowerKey = isPowerButtonKey(keyCode, scanCode) @@ -155,9 +154,9 @@ class ConfigTriggerDelegate { val conflictingKeys = otherTriggerKeys.plus(trigger.keys) .filterIsInstance() .filter { - it.keyCode == keyCode - && it.scanCode != scanCode - && it.device == device + it.keyCode == keyCode && + it.scanCode != scanCode && + it.device == device } val triggerKey = EvdevTriggerKey( @@ -166,7 +165,7 @@ class ConfigTriggerDelegate { device = device, clickType = clickType, consumeEvent = true, - detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty() + detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty(), ) var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey } @@ -186,7 +185,7 @@ class ConfigTriggerDelegate { private fun addTriggerKey( trigger: Trigger, - key: TriggerKey + key: TriggerKey, ): Trigger { // Check whether the trigger already contains the key because if so // then it must be converted to a sequence trigger. @@ -264,7 +263,6 @@ class ConfigTriggerDelegate { .validate() } - fun setSequenceTriggerMode(trigger: Trigger): Trigger { if (trigger.mode == TriggerMode.Sequence) return trigger // undefined mode only allowed if one or no keys @@ -352,7 +350,7 @@ class ConfigTriggerDelegate { fun setTriggerKeyDevice( trigger: Trigger, keyUid: String, - device: KeyEventTriggerDevice + device: KeyEventTriggerDevice, ): Trigger { val newKeys = trigger.keys.map { key -> if (key.uid == keyUid) { @@ -372,7 +370,7 @@ class ConfigTriggerDelegate { fun setTriggerKeyConsumeKeyEvent( trigger: Trigger, keyUid: String, - consumeKeyEvent: Boolean + consumeKeyEvent: Boolean, ): Trigger { val newKeys = trigger.keys.map { key -> if (key.uid == keyUid) { @@ -400,7 +398,7 @@ class ConfigTriggerDelegate { fun setAssistantTriggerKeyType( trigger: Trigger, keyUid: String, - type: AssistantTriggerType + type: AssistantTriggerType, ): Trigger { val newKeys = trigger.keys.map { key -> if (key.uid == keyUid) { @@ -420,7 +418,7 @@ class ConfigTriggerDelegate { fun setFingerprintGestureType( trigger: Trigger, keyUid: String, - type: FingerprintGestureType + type: FingerprintGestureType, ): Trigger { val newKeys = trigger.keys.map { key -> if (key.uid == keyUid) { @@ -444,7 +442,7 @@ class ConfigTriggerDelegate { fun setVibrationDuration( trigger: Trigger, duration: Int, - defaultVibrateDuration: Int + defaultVibrateDuration: Int, ): Trigger { return if (duration == defaultVibrateDuration) { trigger.copy(vibrateDuration = null).validate() @@ -472,7 +470,7 @@ class ConfigTriggerDelegate { fun setSequenceTriggerTimeout( trigger: Trigger, delay: Int, - defaultSequenceTriggerTimeout: Int + defaultSequenceTriggerTimeout: Int, ): Trigger { return if (delay == defaultSequenceTriggerTimeout) { trigger.copy(sequenceTriggerTimeout = null).validate() @@ -512,4 +510,4 @@ class ConfigTriggerDelegate { return trigger.copy(keys = newKeys).validate() } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt index b454952b71..65d633f0ab 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -40,7 +40,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( private val devicesAdapter: DevicesAdapter, private val floatingLayoutRepository: FloatingLayoutRepository, private val getDefaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase, - private val keyMapRepository: KeyMapRepository + private val keyMapRepository: KeyMapRepository, ) : ConfigTriggerUseCase, GetDefaultKeyMapOptionsUseCase by getDefaultKeyMapOptionsUseCase { override val keyMap: StateFlow> = state.keyMap @@ -66,7 +66,6 @@ class ConfigTriggerUseCaseImpl @Inject constructor( is AssistantTriggerKeyEntity, is FingerprintTriggerKeyEntity, is FloatingButtonKeyEntity -> null } }.filterIsInstance() - }.firstBlocking() } @@ -106,7 +105,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( keyCode: Int, scanCode: Int, device: KeyEventTriggerDevice, - requiresIme: Boolean + requiresIme: Boolean, ) = updateTrigger { trigger -> delegate.addKeyEventTriggerKey( trigger, @@ -114,7 +113,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( scanCode, device, requiresIme, - otherTriggerKeys = otherTriggerKeys + otherTriggerKeys = otherTriggerKeys, ) } @@ -128,7 +127,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( keyCode, scanCode, device, - otherTriggerKeys = otherTriggerKeys + otherTriggerKeys = otherTriggerKeys, ) } @@ -346,4 +345,4 @@ interface ConfigTriggerUseCase : GetDefaultKeyMapOptionsUseCase { val floatingButtonToUse: MutableStateFlow suspend fun getFloatingLayoutCount(): Int -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt index 776f353ef2..7962e19da8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt @@ -20,7 +20,7 @@ data class EvdevTriggerKey( val device: EvdevDeviceInfo, override val clickType: ClickType = ClickType.SHORT_PRESS, override val consumeEvent: Boolean = true, - override val detectWithScanCodeUserSetting: Boolean = false + override val detectWithScanCodeUserSetting: Boolean = false, ) : TriggerKey(), KeyCodeTriggerKey { override val allowedDoublePress: Boolean = true override val allowedLongPress: Boolean = true @@ -59,7 +59,7 @@ data class EvdevTriggerKey( ), clickType = clickType, consumeEvent = consumeEvent, - detectWithScanCodeUserSetting = detectWithScancode + detectWithScanCodeUserSetting = detectWithScancode, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt index 3ebc041f5d..b059d8a4b6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt @@ -53,9 +53,8 @@ fun KeyCodeTriggerKey.getCodeLabel(resourceProvider: ResourceProvider): String { ?: resourceProvider.getString(R.string.trigger_key_unknown_scan_code, scanCode!!) return "$codeLabel (${resourceProvider.getString(R.string.trigger_key_scan_code_detection_flag)})" - } else { return KeyCodeStrings.keyCodeToString(keyCode) ?: resourceProvider.getString(R.string.trigger_key_unknown_key_code, keyCode) } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt index c405652920..5cefbd600f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -21,7 +21,7 @@ data class KeyEventTriggerKey( */ val requiresIme: Boolean = false, override val scanCode: Int? = null, - override val detectWithScanCodeUserSetting: Boolean = false + override val detectWithScanCodeUserSetting: Boolean = false, ) : TriggerKey(), KeyCodeTriggerKey { override val allowedLongPress: Boolean = true @@ -83,7 +83,7 @@ data class KeyEventTriggerKey( consumeEvent = consumeEvent, requiresIme = requiresIme, scanCode = entity.scanCode, - detectWithScanCodeUserSetting = detectWithScancode + detectWithScanCodeUserSetting = detectWithScancode, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt index 92c28666d4..d8db9346d2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt @@ -124,9 +124,9 @@ private fun RecordTriggerButton( targetValue = 1.0f, animationSpec = infiniteRepeatable( animation = tween(1000), - repeatMode = RepeatMode.Reverse + repeatMode = RepeatMode.Reverse, ), - label = "recording_dot_alpha" + label = "recording_dot_alpha", ) FilledTonalButton( @@ -135,7 +135,7 @@ private fun RecordTriggerButton( colors = colors, ) { Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { // White recording dot if (state is RecordTriggerState.CountingDown) { @@ -145,8 +145,8 @@ private fun RecordTriggerButton( .alpha(alpha) .background( color = Color.White, - shape = CircleShape - ) + shape = CircleShape, + ), ) Spacer(modifier = Modifier.width(8.dp)) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt index 915db7fe44..d369b0710c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -226,7 +226,7 @@ class RecordTriggerControllerImpl @Inject constructor( dpadMotionEventTracker.reset() downKeyEvents.clear() - //TODO + // TODO // if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { inputEventHub.registerClient( INPUT_EVENT_HUB_ID, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 4519929112..6650c95883 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -75,7 +75,6 @@ fun TriggerKeyOptionsBottomSheet( // Hide drag handle because other bottom sheets don't have it dragHandle = {}, ) { - val scope = rememberCoroutineScope() Column(modifier = Modifier.verticalScroll(rememberScrollState())) { @@ -89,17 +88,16 @@ fun TriggerKeyOptionsBottomSheet( textAlign = TextAlign.Center, text = stringResource(R.string.trigger_key_options_title), style = MaterialTheme.typography.headlineMedium, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) HelpIconButton( modifier = Modifier .align(Alignment.TopEnd) - .padding(horizontal = 8.dp) + .padding(horizontal = 8.dp), ) } - Spacer(modifier = Modifier.height(8.dp)) if (state is TriggerKeyOptionsState.KeyEvent) { @@ -110,7 +108,7 @@ fun TriggerKeyOptionsBottomSheet( keyCode = state.keyCode, scanCode = state.scanCode, onSelectedChange = onScanCodeDetectionChanged, - isCompact = isCompact + isCompact = isCompact, ) CheckBoxText( @@ -129,7 +127,7 @@ fun TriggerKeyOptionsBottomSheet( keyCode = state.keyCode, scanCode = state.scanCode, onSelectedChange = onScanCodeDetectionChanged, - isCompact = isCompact + isCompact = isCompact, ) CheckBoxText( @@ -144,8 +142,10 @@ fun TriggerKeyOptionsBottomSheet( ClickTypeSection( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), state, onSelectClickType, - isCompact = isCompact + .padding(horizontal = 16.dp), + state, + onSelectClickType, + isCompact = isCompact, ) Spacer(Modifier.height(8.dp)) @@ -292,7 +292,7 @@ fun TriggerKeyOptionsBottomSheet( @Composable private fun HelpIconButton( - modifier: Modifier + modifier: Modifier, ) { val uriHandler = LocalUriHandler.current val helpUrl = stringResource(R.string.url_trigger_key_options_guide) @@ -314,7 +314,7 @@ private fun ClickTypeSection( modifier: Modifier, state: TriggerKeyOptionsState, onSelectClickType: (ClickType) -> Unit, - isCompact: Boolean + isCompact: Boolean, ) { Column(modifier) { Text( @@ -337,7 +337,7 @@ private fun ClickTypeSection( buttonStates = clickTypeButtonContent, selectedState = state.clickType, onStateSelected = onSelectClickType, - isCompact = isCompact + isCompact = isCompact, ) } } @@ -356,7 +356,7 @@ private fun ScanCodeDetectionButtonRow( Text( modifier = Modifier.padding(horizontal = 16.dp), text = stringResource(R.string.trigger_scan_code_detection_explanation), - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) Spacer(Modifier.height(8.dp)) @@ -370,7 +370,7 @@ private fun ScanCodeDetectionButtonRow( stringResource(R.string.trigger_use_scan_code_button_disabled) } else { stringResource(R.string.trigger_use_scan_code_button_enabled, scanCode) - } + }, ) KeyMapperSegmentedButtonRow( @@ -381,7 +381,7 @@ private fun ScanCodeDetectionButtonRow( selectedState = isScanCodeSelected, onStateSelected = onSelectedChange, isCompact = isCompact, - isEnabled = isEnabled + isEnabled = isEnabled, ) } } @@ -425,7 +425,7 @@ private fun PreviewKeyEvent() { keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = Scancode.KEY_VOLUMEDOWN, isScanCodeDetectionSelected = true, - isScanCodeSettingEnabled = true + isScanCodeSettingEnabled = true, ), ) } @@ -463,7 +463,7 @@ private fun PreviewKeyEventTiny() { keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = Scancode.KEY_VOLUMEDOWN, isScanCodeDetectionSelected = true, - isScanCodeSettingEnabled = true + isScanCodeSettingEnabled = true, ), ) } @@ -489,7 +489,7 @@ private fun PreviewEvdev() { keyCode = KeyEvent.KEYCODE_UNKNOWN, scanCode = Scancode.KEY_VOLUMEDOWN, isScanCodeDetectionSelected = true, - isScanCodeSettingEnabled = false + isScanCodeSettingEnabled = false, ), ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt index 0231d8f9b2..26bdd87986 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt @@ -97,4 +97,4 @@ private fun validateParallelTrigger(trigger: Trigger): Trigger { } return trigger.copy(mode = newMode, keys = newKeys) -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt index ce55a921e6..e0fb856f14 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt @@ -57,7 +57,7 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { PackageManager.FEATURE_DEVICE_ADMIN -> resourceProvider.getString(R.string.error_system_feature_device_admin_unsupported) PackageManager.FEATURE_CAMERA_FLASH -> resourceProvider.getString(R.string.error_system_feature_camera_flash_unsupported) PackageManager.FEATURE_TELEPHONY, PackageManager.FEATURE_TELEPHONY_DATA -> resourceProvider.getString( - R.string.error_system_feature_telephony_unsupported + R.string.error_system_feature_telephony_unsupported, ) else -> throw Exception("Don't know how to get error message for this system feature ${this.feature}") @@ -91,7 +91,7 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { is KMError.InvalidNumber -> resourceProvider.getString(R.string.error_invalid_number) is KMError.NumberTooSmall -> resourceProvider.getString( R.string.error_number_too_small, - min + min, ) is KMError.NumberTooBig -> resourceProvider.getString(R.string.error_number_too_big, max) @@ -233,7 +233,8 @@ val KMError.isFixable: Boolean is SystemError.PermissionDenied, is KMError.ShizukuNotStarted, is KMError.CantDetectKeyEventsInPhoneCall, - is SystemBridgeError.Disconnected -> true + is SystemBridgeError.Disconnected, + -> true else -> false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt index 43885b2719..b1b9c512fc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt @@ -570,10 +570,10 @@ object ScancodeStrings { Scancode.KEY_MACRO7 to "Macro 7", Scancode.KEY_MACRO8 to "Macro 8", Scancode.KEY_MACRO9 to "Macro 9", - Scancode.KEY_MACRO10 to "Macro 10" + Scancode.KEY_MACRO10 to "Macro 10", ) fun getScancodeLabel(scancode: Int): String? { return SCANCODE_LABELS[scancode] } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt index ff2bc00d0a..75d7a65ef7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt @@ -43,7 +43,7 @@ fun KeyMapperSegmentedButtonRow( // The disabled border color of the inactive button is by default not greyed out enough SegmentedButtonDefaults.colors( disabledInactiveBorderColor = - SegmentedButtonDefaults.colors().inactiveBorderColor.copy(alpha = 0.5f), + SegmentedButtonDefaults.colors().inactiveBorderColor.copy(alpha = 0.5f), ) } @@ -66,9 +66,9 @@ fun KeyMapperSegmentedButtonRow( shape = SegmentedButtonDefaults.itemShape( index = buttonStates.indexOf(content), count = buttonStates.size, - baseShape = MaterialTheme.shapes.extraSmall + baseShape = MaterialTheme.shapes.extraSmall, ), - colors = colors + colors = colors, ) { BasicText( modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier, @@ -77,7 +77,7 @@ fun KeyMapperSegmentedButtonRow( overflow = TextOverflow.Ellipsis, autoSize = TextAutoSize.StepBased( maxFontSize = LocalTextStyle.current.fontSize, - minFontSize = 10.sp + minFontSize = 10.sp, ), ) } @@ -96,7 +96,7 @@ fun KeyMapperSegmentedButtonRow( modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier, text = label, maxLines = 2, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt index c6db07d8da..b7a5b3eecc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt @@ -29,18 +29,18 @@ fun SwitchPreferenceCompose( shape = MaterialTheme.shapes.medium, onClick = { onCheckedChange(!isChecked) - } + }, ) { Row( modifier = Modifier.Companion .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp) + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Icon( imageVector = icon, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface + tint = MaterialTheme.colorScheme.onSurface, ) Column(modifier = Modifier.Companion.weight(1f)) { @@ -56,8 +56,8 @@ fun SwitchPreferenceCompose( Switch( checked = isChecked, - onCheckedChange = onCheckedChange + onCheckedChange = onCheckedChange, ) } } -} \ No newline at end of file +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt index 2c728437fc..3fbf8d0150 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt @@ -16,7 +16,7 @@ val KeyMapperIcons.FakeShizuku: ImageVector defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 24f, - viewportHeight = 24f + viewportHeight = 24f, ).apply { path(fill = SolidColor(Color(0xFF4053B9))) { moveTo(12f, 24f) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt index aaf9195c9b..84e2a94dea 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt @@ -16,7 +16,7 @@ val KeyMapperIcons.FolderManaged: ImageVector defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 960f, - viewportHeight = 960f + viewportHeight = 960f, ).apply { path(fill = SolidColor(Color.Black)) { moveTo(720f, 760f) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt index 8fe3042070..9a918c25f7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt @@ -18,7 +18,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector defaultWidth = 61.637.dp, defaultHeight = 61.637.dp, viewportWidth = 61.637f, - viewportHeight = 61.637f + viewportHeight = 61.637f, ).apply { group( clipPathData = PathData { @@ -27,7 +27,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(102f, 103.637f) lineTo(-42f, 103.637f) close() - } + }, ) { } group( @@ -37,7 +37,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(102f, 103.637f) lineTo(-42f, 103.637f) close() - } + }, ) { } group( @@ -47,7 +47,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(102.095f, 103.731f) lineTo(-42.094f, 103.731f) close() - } + }, ) { } group( @@ -57,7 +57,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(102.047f, 103.684f) lineTo(-42.047f, 103.684f) close() - } + }, ) { } group( @@ -67,7 +67,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(102.047f, 103.684f) lineTo(-42.047f, 103.684f) close() - } + }, ) { } group( @@ -77,7 +77,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(64.667f, 66.304f) lineTo(-4.667f, 66.304f) close() - } + }, ) { } group( @@ -87,7 +87,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(54f, 55.637f) lineTo(6f, 55.637f) close() - } + }, ) { } group( @@ -97,7 +97,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(64.8f, 55.771f) lineTo(-4.8f, 55.771f) close() - } + }, ) { } group( @@ -107,7 +107,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(54.133f, 66.437f) lineTo(5.867f, 66.437f) close() - } + }, ) { } group( @@ -117,7 +117,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(59.467f, 61.104f) lineTo(0.533f, 61.104f) close() - } + }, ) { } group( @@ -127,12 +127,12 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector lineTo(78f, 79.637f) lineTo(-18f, 79.637f) close() - } + }, ) { } path( fill = SolidColor(Color(0xFFD32F2F)), - strokeLineWidth = 1.27586f + strokeLineWidth = 1.27586f, ) { moveTo(4f, 24.637f) lineTo(33f, 24.637f) @@ -147,7 +147,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector } path( fill = SolidColor(Color.White), - strokeLineWidth = 1.3f + strokeLineWidth = 1.3f, ) { moveTo(21.9f, 37.514f) lineTo(21.9f, 31.937f) @@ -201,7 +201,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector path( fill = SolidColor(Color(0xFF212121)), fillAlpha = 0f, - strokeLineWidth = 1.27586f + strokeLineWidth = 1.27586f, ) { moveTo(22.717f, 4.354f) curveTo(21.679f, 5.075f, 21f, 6.272f, 21f, 7.637f) @@ -218,7 +218,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector } path( fill = SolidColor(Color(0xFF1565C0)), - strokeLineWidth = 1.27586f + strokeLineWidth = 1.27586f, ) { moveTo(55f, 2.862f) lineTo(26f, 2.862f) @@ -233,7 +233,7 @@ val KeyMapperIcons.KeyMapperIcon: ImageVector } path( fill = SolidColor(Color.White), - strokeLineWidth = 1.3f + strokeLineWidth = 1.3f, ) { moveTo(51.4f, 11.437f) lineTo(30.6f, 11.437f) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt index cd00c50e4e..7e6435500b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt @@ -20,7 +20,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector defaultWidth = 32.dp, defaultHeight = 32.dp, viewportWidth = 32f, - viewportHeight = 32f + viewportHeight = 32f, ).apply { group( clipPathData = PathData { @@ -29,7 +29,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector lineTo(32f, 32f) lineTo(0f, 32f) close() - } + }, ) { } group( @@ -39,7 +39,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector lineTo(32f, 32f) lineTo(0f, 32f) close() - } + }, ) { } group( @@ -49,7 +49,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector lineTo(32f, 32f) lineTo(-0f, 32f) close() - } + }, ) { } group( @@ -59,7 +59,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector lineTo(32f, -0f) lineTo(-0f, -0f) close() - } + }, ) { } group( @@ -69,7 +69,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector lineTo(32f, 32f) lineTo(0f, 32f) close() - } + }, ) { } group( @@ -79,7 +79,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector lineTo(32f, -0f) lineTo(-0f, -0f) close() - } + }, ) { } group( @@ -89,7 +89,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector lineTo(32f, -0f) lineTo(-0f, -0f) close() - } + }, ) { } path(fill = SolidColor(Color.Black)) { @@ -148,7 +148,7 @@ val KeyMapperIcons.ProModeIconDisabled: ImageVector stroke = SolidColor(Color.Black), strokeLineWidth = 2f, strokeLineCap = StrokeCap.Round, - pathFillType = PathFillType.EvenOdd + pathFillType = PathFillType.EvenOdd, ) { moveTo(26.664f, 5.353f) lineTo(5.354f, 26.753f) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt index f04f9321f6..00e8a9f9db 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt @@ -16,7 +16,7 @@ val KeyMapperIcons.SignalWifiNotConnected: ImageVector defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 960f, - viewportHeight = 960f + viewportHeight = 960f, ).apply { path(fill = SolidColor(Color.Black)) { moveTo(423f, 783f) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt index e25bf328da..0215618831 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt @@ -16,7 +16,7 @@ val KeyMapperIcons.WandStars: ImageVector defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 960f, - viewportHeight = 960f + viewportHeight = 960f, ).apply { path(fill = SolidColor(Color.Black)) { moveToRelative(646f, 522f) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt index e47b9d3290..425d6343a4 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt @@ -159,7 +159,8 @@ class BackupManagerTest { val expectedGroup = GroupEntity( uid = "child_group", name = "Child", - parentUid = null, // The parent is null because it did not exist. + // The parent is null because it did not exist. + parentUid = null, lastOpenedDate = 0L, ) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt index 364822726d..156a03ca19 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt @@ -37,7 +37,7 @@ class ConfigActionsUseCaseTest { configKeyMapState = ConfigKeyMapStateImpl( testScope, keyMapRepository = mock(), - floatingButtonRepository = mock() + floatingButtonRepository = mock(), ) mockConfigConstraintsUseCase = mock() @@ -46,7 +46,7 @@ class ConfigActionsUseCaseTest { state = configKeyMapState, preferenceRepository = mock(), configConstraints = mockConfigConstraintsUseCase, - defaultKeyMapOptionsUseCase = mock() + defaultKeyMapOptionsUseCase = mock(), ) } @@ -60,10 +60,10 @@ class ConfigActionsUseCaseTest { keyCode = KeyEvent.KEYCODE_DPAD_LEFT, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - requiresIme = true - ) - ) - ) + requiresIme = true, + ), + ), + ), ) useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) @@ -150,5 +150,4 @@ class ConfigActionsUseCaseTest { } } } - -} \ No newline at end of file +} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt index 4047760af2..da52dcfa41 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt @@ -72,7 +72,7 @@ class GetActionErrorUseCaseTest { ringtoneAdapter = mock(), buildConfigProvider = TestBuildConfigProvider(), systemBridgeConnectionManager = mock(), - preferenceRepository = mock() + preferenceRepository = mock(), ) } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index e65a0ace00..6a164c9581 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -80,7 +80,7 @@ class PerformActionsUseCaseTest { notificationReceiverAdapter = mock(), ringtoneAdapter = mock(), inputEventHub = mockInputEventHub, - systemBridgeConnectionManager = mock() + systemBridgeConnectionManager = mock(), ) } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 8b188a6b55..d34f57c237 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -248,7 +248,7 @@ class KeyMapAlgorithmTest { inputDownEvdevEvent( KeyEvent.KEYCODE_UNKNOWN, Scancode.BTN_LEFT, - FAKE_CONTROLLER_EVDEV_DEVICE + FAKE_CONTROLLER_EVDEV_DEVICE, ) inputUpEvdevEvent(KeyEvent.KEYCODE_UNKNOWN, Scancode.BTN_LEFT, FAKE_CONTROLLER_EVDEV_DEVICE) @@ -281,7 +281,7 @@ class KeyMapAlgorithmTest { keyCode = KeyEvent.KEYCODE_A, scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -300,7 +300,7 @@ class KeyMapAlgorithmTest { keyCode = KeyEvent.KEYCODE_A, scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -320,7 +320,7 @@ class KeyMapAlgorithmTest { keyCode = KeyEvent.KEYCODE_A, scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -338,15 +338,17 @@ class KeyMapAlgorithmTest { val trigger = sequenceTrigger( EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, - scanCode = Scancode.KEY_B, // Different scan code + // Different scan code + scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_C, - scanCode = Scancode.KEY_D, // Different scan code + // Different scan code + scanCode = Scancode.KEY_D, device = FAKE_CONTROLLER_EVDEV_DEVICE, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) @@ -368,15 +370,17 @@ class KeyMapAlgorithmTest { val trigger = parallelTrigger( EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_A, - scanCode = Scancode.KEY_B, // Different scan code + // Different scan code + scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), EvdevTriggerKey( keyCode = KeyEvent.KEYCODE_C, - scanCode = Scancode.KEY_D, // Different scan code + // Different scan code + scanCode = Scancode.KEY_D, device = FAKE_CONTROLLER_EVDEV_DEVICE, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) @@ -392,7 +396,6 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } - @Test fun `Scan code detection works with long press evdev trigger`() = runTest(testDispatcher) { @@ -402,7 +405,7 @@ class KeyMapAlgorithmTest { scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, clickType = ClickType.LONG_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -424,7 +427,7 @@ class KeyMapAlgorithmTest { scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_EVDEV_DEVICE, clickType = ClickType.DOUBLE_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -471,7 +474,8 @@ class KeyMapAlgorithmTest { scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false // It will be automatically enabled even if the user hasn't explicitly turned it on + // It will be automatically enabled even if the user hasn't explicitly turned it on + detectWithScanCodeUserSetting = false, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -480,13 +484,13 @@ class KeyMapAlgorithmTest { keyCode = KeyEvent.KEYCODE_B, action = KeyEvent.ACTION_DOWN, device = FAKE_CONTROLLER_INPUT_DEVICE, - scanCode = Scancode.KEY_B + scanCode = Scancode.KEY_B, ) inputKeyEvent( keyCode = KeyEvent.KEYCODE_B, action = KeyEvent.ACTION_UP, device = FAKE_CONTROLLER_INPUT_DEVICE, - scanCode = Scancode.KEY_B + scanCode = Scancode.KEY_B, ) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) @@ -501,7 +505,7 @@ class KeyMapAlgorithmTest { scanCode = Scancode.KEY_B, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -510,13 +514,13 @@ class KeyMapAlgorithmTest { keyCode = KeyEvent.KEYCODE_B, action = KeyEvent.ACTION_DOWN, device = FAKE_CONTROLLER_INPUT_DEVICE, - scanCode = Scancode.KEY_B + scanCode = Scancode.KEY_B, ) inputKeyEvent( keyCode = KeyEvent.KEYCODE_B, action = KeyEvent.ACTION_UP, device = FAKE_CONTROLLER_INPUT_DEVICE, - scanCode = Scancode.KEY_B + scanCode = Scancode.KEY_B, ) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) @@ -4703,8 +4707,7 @@ class KeyMapAlgorithmTest { metaState: Int? = null, scanCode: Int = 0, repeatCount: Int = 0, - - ): Boolean = controller.onInputEvent( + ): Boolean = controller.onInputEvent( KMKeyEvent( keyCode = keyCode, action = action, @@ -4831,5 +4834,4 @@ class KeyMapAlgorithmTest { sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD, ) } - } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt index 64151f4738..97fb879f63 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt @@ -3,9 +3,9 @@ package io.github.sds100.keymapper.base.keymaps import io.github.sds100.keymapper.base.constraints.Constraint import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintState -import io.github.sds100.keymapper.base.groups.Group import io.github.sds100.keymapper.base.detection.DetectKeyMapModel import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.base.groups.Group import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.junit.Test diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt index e5302b0b6e..312edb60aa 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt @@ -27,7 +27,6 @@ class ConfigTriggerDelegateTest { @Before fun before() { - mockedKeyEvent = mockStatic(KeyEvent::class.java) mockedKeyEvent.`when` { KeyEvent.getMaxKeyCode() }.thenReturn(1000) @@ -126,14 +125,14 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), KeyEventTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = Scancode.KEY_VOLUMEUP, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) @@ -158,12 +157,12 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), AssistantTriggerKey( type = AssistantTriggerType.ANY, clickType = ClickType.SHORT_PRESS, - ) + ), ) val newTrigger = delegate.addKeyEventTriggerKey( @@ -186,7 +185,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ) val trigger = sequenceTrigger( @@ -196,7 +195,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), ) @@ -214,7 +213,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) val trigger = parallelTrigger( KeyEventTriggerKey( @@ -222,9 +221,9 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), - key + key, ) val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) @@ -240,7 +239,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ) val trigger = sequenceTrigger( KeyEventTriggerKey( @@ -248,16 +247,16 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), - key + key, ) val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) assertThat(newTrigger.keys, hasSize(2)) assertThat( newTrigger.keys, - contains(trigger.keys[0], key.copy(detectWithScanCodeUserSetting = true)) + contains(trigger.keys[0], key.copy(detectWithScanCodeUserSetting = true)), ) } @@ -268,7 +267,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEUP, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) val trigger = parallelTrigger( @@ -277,9 +276,9 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), - key + key, ) val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, false) @@ -288,7 +287,6 @@ class ConfigTriggerDelegateTest { assertThat(newTrigger.keys[1], `is`(key.copy(detectWithScanCodeUserSetting = false))) } - @Test fun `Do not remove other keys from different devices when enabling scan code detection`() { val key = KeyEventTriggerKey( @@ -296,7 +294,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.External(descriptor = "keyboard0", name = "Keyboard"), clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) val trigger = parallelTrigger( KeyEventTriggerKey( @@ -304,20 +302,19 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), - key + key, ) val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) assertThat(newTrigger.keys, hasSize(2)) assertThat( newTrigger.keys, - contains(trigger.keys[0], key.copy(detectWithScanCodeUserSetting = true)) + contains(trigger.keys[0], key.copy(detectWithScanCodeUserSetting = true)), ) } - /** * Issue #761 */ @@ -338,7 +335,7 @@ class ConfigTriggerDelegateTest { keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = 123, device = device1, - clickType = ClickType.SHORT_PRESS + clickType = ClickType.SHORT_PRESS, ), ) @@ -346,9 +343,10 @@ class ConfigTriggerDelegateTest { trigger = Trigger(), keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = 124, - device = device2, // Different device + // Different device + device = device2, requiresIme = false, - otherTriggers + otherTriggers, ) assertThat( @@ -377,7 +375,7 @@ class ConfigTriggerDelegateTest { keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = 123, device = device1, - clickType = ClickType.SHORT_PRESS + clickType = ClickType.SHORT_PRESS, ), ) @@ -385,7 +383,8 @@ class ConfigTriggerDelegateTest { trigger = trigger, keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = 124, - device = device2, // Different device + // Different device + device = device2, requiresIme = false, ) @@ -420,7 +419,7 @@ class ConfigTriggerDelegateTest { keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = 124, device = device, - otherTriggers + otherTriggers, ) assertThat( @@ -444,7 +443,7 @@ class ConfigTriggerDelegateTest { keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = 123, device = device, - clickType = ClickType.SHORT_PRESS + clickType = ClickType.SHORT_PRESS, ), ) @@ -454,7 +453,7 @@ class ConfigTriggerDelegateTest { scanCode = 124, device = device, requiresIme = false, - otherTriggers + otherTriggers, ) assertThat( @@ -478,7 +477,7 @@ class ConfigTriggerDelegateTest { keyCode = KeyEvent.KEYCODE_VOLUME_UP, scanCode = 123, device = device, - clickType = ClickType.SHORT_PRESS + clickType = ClickType.SHORT_PRESS, ), ) @@ -560,7 +559,7 @@ class ConfigTriggerDelegateTest { vendor = 1, product = 2, ), - ) + ), ) val newTrigger = delegate.addKeyEventTriggerKey( @@ -704,9 +703,9 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, clickType = ClickType.SHORT_PRESS, device = device, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ), - AssistantTriggerKey(type = AssistantTriggerType.ANY, clickType = ClickType.SHORT_PRESS) + AssistantTriggerKey(type = AssistantTriggerType.ANY, clickType = ClickType.SHORT_PRESS), ) val newTrigger = delegate.addEvdevTriggerKey( @@ -734,8 +733,8 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, clickType = ClickType.SHORT_PRESS, device = device, - detectWithScanCodeUserSetting = true - ) + detectWithScanCodeUserSetting = true, + ), ) val newTrigger = delegate.addEvdevTriggerKey( @@ -828,7 +827,7 @@ class ConfigTriggerDelegateTest { val triggerWithAssistant = delegate.addAssistantTriggerKey( trigger = triggerWithKeyEvent, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) val finalTrigger = delegate.setTriggerDoublePress(triggerWithAssistant) @@ -852,7 +851,7 @@ class ConfigTriggerDelegateTest { val triggerWithAssistant = delegate.addAssistantTriggerKey( trigger = triggerWithKeyEvent, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) val finalTrigger = delegate.setTriggerLongPress(triggerWithAssistant) @@ -868,7 +867,7 @@ class ConfigTriggerDelegateTest { val triggerWithAssistant = delegate.addAssistantTriggerKey( trigger = emptyTrigger, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) val finalTrigger = delegate.setTriggerDoublePress(triggerWithAssistant) @@ -883,7 +882,7 @@ class ConfigTriggerDelegateTest { val triggerWithAssistant = delegate.addAssistantTriggerKey( trigger = emptyTrigger, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) val finalTrigger = delegate.setTriggerLongPress(triggerWithAssistant) @@ -908,7 +907,7 @@ class ConfigTriggerDelegateTest { val finalTrigger = delegate.addAssistantTriggerKey( trigger = triggerWithDoublePress, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) @@ -932,7 +931,7 @@ class ConfigTriggerDelegateTest { val finalTrigger = delegate.addFingerprintGesture( trigger = triggerWithDoublePress, - type = FingerprintGestureType.SWIPE_UP + type = FingerprintGestureType.SWIPE_UP, ) assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) @@ -956,7 +955,7 @@ class ConfigTriggerDelegateTest { val finalTrigger = delegate.addAssistantTriggerKey( trigger = triggerWithLongPress, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) @@ -980,7 +979,7 @@ class ConfigTriggerDelegateTest { val finalTrigger = delegate.addFingerprintGesture( trigger = triggerWithLongPress, - type = FingerprintGestureType.SWIPE_UP + type = FingerprintGestureType.SWIPE_UP, ) assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) @@ -1005,12 +1004,12 @@ class ConfigTriggerDelegateTest { val triggerWithVoiceAssistant = delegate.addAssistantTriggerKey( trigger = triggerWithKeyEvent, - type = AssistantTriggerType.VOICE + type = AssistantTriggerType.VOICE, ) val triggerWithDeviceAssistant = delegate.addAssistantTriggerKey( trigger = triggerWithVoiceAssistant, - type = AssistantTriggerType.DEVICE + type = AssistantTriggerType.DEVICE, ) val finalTrigger = delegate.setParallelTriggerMode(triggerWithDeviceAssistant) @@ -1037,12 +1036,12 @@ class ConfigTriggerDelegateTest { val triggerWithDeviceAssistant = delegate.addAssistantTriggerKey( trigger = triggerWithKeyEvent, - type = AssistantTriggerType.DEVICE + type = AssistantTriggerType.DEVICE, ) val triggerWithVoiceAssistant = delegate.addAssistantTriggerKey( trigger = triggerWithDeviceAssistant, - type = AssistantTriggerType.VOICE + type = AssistantTriggerType.VOICE, ) val finalTrigger = delegate.setParallelTriggerMode(triggerWithVoiceAssistant) @@ -1079,7 +1078,7 @@ class ConfigTriggerDelegateTest { val finalTrigger = delegate.addAssistantTriggerKey( trigger = triggerWithLongPress, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) @@ -1101,7 +1100,7 @@ class ConfigTriggerDelegateTest { val finalTrigger = delegate.addAssistantTriggerKey( trigger = triggerWithDoublePress, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) @@ -1123,7 +1122,7 @@ class ConfigTriggerDelegateTest { val finalTrigger = delegate.addAssistantTriggerKey( trigger = triggerWithLongPress, - type = AssistantTriggerType.ANY + type = AssistantTriggerType.ANY, ) assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) @@ -1207,7 +1206,6 @@ class ConfigTriggerDelegateTest { assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(true)) } - @Test fun `Remove keys with same key code from the same internal device when converting to a parallel trigger`() { val key = KeyEventTriggerKey( @@ -1215,7 +1213,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) val trigger = sequenceTrigger( @@ -1225,7 +1223,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), ) @@ -1242,17 +1240,17 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), KeyEventTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.External( descriptor = "keyboard0", - name = "Keyboard" + name = "Keyboard", ), clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), ) @@ -1269,14 +1267,14 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEUP, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), KeyEventTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), ) @@ -1292,7 +1290,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) val anyDeviceKey = KeyEventTriggerKey( @@ -1300,7 +1298,7 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) val trigger = sequenceTrigger(internalKey, anyDeviceKey) @@ -1317,10 +1315,10 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.External( descriptor = "keyboard0", - name = "Keyboard" + name = "Keyboard", ), clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) val trigger = sequenceTrigger( @@ -1330,10 +1328,10 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.External( descriptor = "keyboard0", - name = "Keyboard" + name = "Keyboard", ), clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), ) @@ -1350,38 +1348,38 @@ class ConfigTriggerDelegateTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), KeyEventTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), KeyEventTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), KeyEventTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.External( descriptor = "keyboard0", - name = "Keyboard" + name = "Keyboard", ), clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), KeyEventTriggerKey( keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ), ) @@ -1389,4 +1387,4 @@ class ConfigTriggerDelegateTest { assertThat(newTrigger.keys, hasSize(1)) assertThat(newTrigger.keys, contains(trigger.keys[0])) } -} \ No newline at end of file +} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt index 43fe608d48..060313fb27 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt @@ -25,7 +25,7 @@ class TriggerKeyTest { fun tearDown() { mockedKeyEvent.close() } - + @Test fun `User can not change scan code detection if the scan code is null`() { val triggerKey = KeyEventTriggerKey( @@ -33,7 +33,7 @@ class TriggerKeyTest { scanCode = null, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ) assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(false)) } @@ -45,7 +45,7 @@ class TriggerKeyTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ) assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(false)) } @@ -57,7 +57,7 @@ class TriggerKeyTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ) assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(true)) } @@ -69,7 +69,7 @@ class TriggerKeyTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ) assertThat(triggerKey.detectWithScancode(), `is`(true)) } @@ -81,7 +81,7 @@ class TriggerKeyTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) assertThat(triggerKey.detectWithScancode(), `is`(true)) } @@ -93,7 +93,7 @@ class TriggerKeyTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ) assertThat(triggerKey.detectWithScancode(), `is`(true)) } @@ -105,7 +105,7 @@ class TriggerKeyTest { scanCode = null, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = true + detectWithScanCodeUserSetting = true, ) assertThat(triggerKey.detectWithScancode(), `is`(false)) } @@ -117,8 +117,8 @@ class TriggerKeyTest { scanCode = Scancode.KEY_VOLUMEDOWN, device = KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, - detectWithScanCodeUserSetting = false + detectWithScanCodeUserSetting = false, ) assertThat(triggerKey.detectWithScancode(), `is`(false)) } -} \ No newline at end of file +} diff --git a/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt b/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt index 07b03f5908..bf28ed29c7 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt @@ -10,7 +10,7 @@ sealed class KMNotificationAction { START_ACCESSIBILITY_SERVICE, RESTART_ACCESSIBILITY_SERVICE, TOGGLE_KEY_MAPPER_IME, - SHOW_KEYBOARD + SHOW_KEYBOARD, } sealed class Broadcast(val intentAction: IntentAction) : KMNotificationAction() { @@ -29,12 +29,13 @@ sealed class KMNotificationAction { } sealed class RemoteInput( - val key: String, val intentAction: IntentAction + val key: String, + val intentAction: IntentAction, ) : KMNotificationAction() { data object PairingCode : RemoteInput( key = "pairing_code", - intentAction = IntentAction.PAIRING_CODE_REPLY + intentAction = IntentAction.PAIRING_CODE_REPLY, ) } @@ -42,4 +43,4 @@ sealed class KMNotificationAction { data object AccessibilitySettings : Activity() data class MainActivity(val action: String? = null) : Activity() } -} \ No newline at end of file +} diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt index b493223af8..7c38413e92 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt @@ -36,4 +36,4 @@ val InputDevice.isExternalCompat: Boolean e.printStackTrace() false } - } \ No newline at end of file + } diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt index b5f21c0871..c38e7e074f 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt @@ -180,4 +180,4 @@ object SettingsUtils { this.awaitClose { ctx.contentResolver.unregisterContentObserver(observer) } } -} \ No newline at end of file +} diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt index cda4075f50..1aa057abef 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt @@ -145,7 +145,7 @@ sealed class TriggerKeyEntity : Parcelable { clickType, flags ?: 0, uid, - scanCode + scanCode, ) } } From ec7fe2c410a29c5711c67e5078d68936f4107e52 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 7 Sep 2025 16:12:24 +0200 Subject: [PATCH 215/215] #1394 fix tests --- .../base/actions/GetActionErrorUseCaseTest.kt | 44 ------------------- .../base/actions/PerformActionsUseCaseTest.kt | 2 - .../data/entities/TriggerKeyEntity.kt | 2 +- 3 files changed, 1 insertion(+), 47 deletions(-) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt index da52dcfa41..139b645fd4 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt @@ -7,11 +7,8 @@ import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.system.inputmethod.ImeInfo import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -52,13 +49,11 @@ class GetActionErrorUseCaseTest { private lateinit var useCase: GetActionErrorUseCaseImpl - private lateinit var mockShizukuAdapter: ShizukuAdapter private lateinit var fakeInputMethodAdapter: FakeInputMethodAdapter private lateinit var mockPermissionAdapter: PermissionAdapter @Before fun init() { - mockShizukuAdapter = mock() fakeInputMethodAdapter = FakeInputMethodAdapter() mockPermissionAdapter = mock() @@ -77,7 +72,6 @@ class GetActionErrorUseCaseTest { } private fun setupKeyEventActionTest(chosenIme: ImeInfo) { - whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(false) } whenever(mockPermissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)).then { true } fakeInputMethodAdapter.chosenIme.value = chosenIme fakeInputMethodAdapter.inputMethods.value = listOf(GBOARD_IME_INFO, GUI_KEYBOARD_IME_INFO) @@ -221,42 +215,4 @@ class GetActionErrorUseCaseTest { assertThat(errors[1], nullValue()) assertThat(errors[2], `is`(KMError.NoCompatibleImeChosen)) } - - /** - * #776 - */ - @Test - fun `don't show Shizuku errors if a compatible ime is selected`() = testScope.runTest { - // GIVEN - whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) } - fakeInputMethodAdapter.chosenIme.update { GUI_KEYBOARD_IME_INFO } - - val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) - - // WHEN - val errorMap = useCase.actionErrorSnapshot.first().getErrors(listOf(action)) - - // THEN - assertThat(errorMap[action], 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) } - fakeInputMethodAdapter.chosenIme.update { GBOARD_IME_INFO } - - val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) - - // WHEN - val errorMap = useCase.actionErrorSnapshot.first().getErrors(listOf(action)) - - // THEN - assertThat(errorMap[action], `is`(KMError.ShizukuNotStarted)) - } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index 6a164c9581..8c67f904d1 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -254,7 +254,6 @@ class PerformActionsUseCaseTest { val action = ActionData.InputKeyEvent( keyCode = 1, metaState = 0, - useShell = false, device = ActionData.InputKeyEvent.Device( descriptor = descriptor, name = "fake_name_2", @@ -316,7 +315,6 @@ class PerformActionsUseCaseTest { val action = ActionData.InputKeyEvent( keyCode = 1, metaState = 0, - useShell = false, device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""), ) diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt index 1aa057abef..e294f7c795 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt @@ -132,7 +132,7 @@ sealed class TriggerKeyEntity : Parcelable { uid: String, ): KeyEventTriggerKeyEntity { val keyCode by json.byInt(KeyEventTriggerKeyEntity.NAME_KEYCODE) - val scanCode by json.byInt(KeyEventTriggerKeyEntity.NAME_SCANCODE) + val scanCode by json.byNullableInt(KeyEventTriggerKeyEntity.NAME_SCANCODE) val deviceId by json.byString(KeyEventTriggerKeyEntity.NAME_DEVICE_ID) val deviceName by json.byNullableString(KeyEventTriggerKeyEntity.NAME_DEVICE_NAME) val clickType by json.byInt(NAME_CLICK_TYPE)