diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc2ae67..876a386 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,16 @@ jobs: with: clang-format-version: 15 + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '21' + + - name: check Java formatting + uses: axel-op/googlejavaformat-action@v4 + with: + args: "--aosp --set-exit-if-changed" + clippy: runs-on: ${{ matrix.os }} strategy: @@ -70,6 +80,8 @@ jobs: - target: x86_64-pc-windows-msvc - target: x86_64-unknown-linux-gnu - target: x86_64-unknown-linux-musl + - target: aarch64-linux-android + - target: x86_64-linux-android name: cargo-deny ${{ matrix.target }} runs-on: ubuntu-22.04 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c121f5a..2a4c6c5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,6 +51,14 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu path: linux/x86_64 + - os: ubuntu-latest + target: aarch64-linux-android + cmake-options: -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 + path: android/arm64-v8a + - os: ubuntu-latest + target: x86_64-linux-android + cmake-options: -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=x86_64 -DANDROID_PLATFORM=android-28 + path: android/x86_64 name: Build steps: diff --git a/.gitignore b/.gitignore index 5a3dec8..8ddf517 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build lib target +.cxx diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ae7868..16946c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(accesskit-c) option(ACCESSKIT_BUILD_HEADERS "Whether to build header files" OFF) option(ACCESSKIT_BUILD_LIBRARIES "Whether to build libraries" ON) +option(ACCESSKIT_ANDROID_EMBEDDED_DEX "Whether to embed the classes.dex file in the library (Android only)" OFF) if (ACCESSKIT_BUILD_LIBRARIES) include(FetchContent) @@ -19,6 +20,9 @@ if (ACCESSKIT_BUILD_LIBRARIES) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) set(CMAKE_PDB_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) corrosion_import_crate(MANIFEST_PATH Cargo.toml) + if (ACCESSKIT_ANDROID_EMBEDDED_DEX) + corrosion_set_features(accesskit FEATURES android-embedded-dex) + endif() endif() if (ACCESSKIT_BUILD_HEADERS) diff --git a/Cargo.lock b/Cargo.lock index b124450..bb2e907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,11 +13,24 @@ name = "accesskit-c" version = "0.19.0" dependencies = [ "accesskit", + "accesskit_android", "accesskit_macos", "accesskit_unix", "accesskit_windows", ] +[[package]] +name = "accesskit_android" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d28b60a573c7165b1eb346d66c14e85a1f7923fe2e71e396ce936ca6afb519ae" +dependencies = [ + "accesskit", + "accesskit_consumer", + "jni", + "log", +] + [[package]] name = "accesskit_atspi_common" version = "0.15.0" @@ -307,6 +320,18 @@ dependencies = [ "piper", ] +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -319,6 +344,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -500,6 +535,28 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "libc" version = "0.2.159" @@ -512,6 +569,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "memchr" version = "2.5.0" @@ -705,6 +768,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.210" @@ -784,6 +856,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -849,6 +941,16 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "winapi" version = "0.3.9" @@ -865,6 +967,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -972,13 +1083,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -987,7 +1107,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -996,28 +1131,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1030,24 +1183,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 8e510a6..0bebb9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ doc = false [features] cbindgen = [] +android-embedded-dex = ["accesskit_android/embedded-dex"] [dependencies] accesskit = "0.22.0" @@ -27,6 +28,9 @@ accesskit_macos = "0.23.0" [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] accesskit_unix = "0.18.0" +[target.'cfg(target_os = "android")'.dependencies] +accesskit_android = "0.5.0" + [profile.release] lto = true opt-level = "z" diff --git a/cbindgen.toml b/cbindgen.toml index f28984a..bf0fa00 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -10,6 +10,9 @@ include_guard = "ACCESSKIT_H" cpp_compat = true after_includes = """#ifdef _WIN32 #include +#endif +#ifdef __ANDROID__ +#include #endif""" usize_is_size_t = true @@ -20,6 +23,7 @@ prefix = "accesskit_" renaming_overrides_prefixing = true [defines] +"target_os = android" = "__ANDROID__" "target_os = linux" = "__linux__" "target_os = dragonfly" = "__DragonFly__" "target_os = freebsd" = "__FreeBSD__" @@ -40,6 +44,10 @@ renaming_overrides_prefixing = true "HWND" = "HWND" "HasPopup" = "accesskit_has_popup" "Invalid" = "accesskit_invalid" +"JNIEnv" = "JNIEnv" +"jfloat" = "jfloat" +"jint" = "jint" +"jobject" = "jobject" "LPARAM" = "LPARAM" "LRESULT" = "LRESULT" "ListStyle" = "accesskit_list_style" diff --git a/examples/sdl/README.md b/examples/sdl/README.md index ea27200..8db3331 100644 --- a/examples/sdl/README.md +++ b/examples/sdl/README.md @@ -34,3 +34,26 @@ First download an SDL2 package from the project's [GitHub release page](https:// cmake -S . -B build -DACCESSKIT_DIR="../.." -DCMAKE_BUILD_TYPE=Release cmake --build build ``` + +### Android + +You will need to build SDL2 for Android (see [SDL Android README](https://github.com/libsdl-org/SDL/blob/main/docs/README-android.md)). + +#### Building the Example APK + +1. Copy pre-built libraries to `android/app/src/main/jniLibs//` +2. Configure `android/local.properties`: + ```properties + sdk.dir=/path/to/Android/Sdk + sdl2.dir=/path/to/SDL2-source + accesskit.dir=/path/to/accesskit-c + ``` + + - `sdl2.dir`: Path to SDL2 source directory containing `include/SDL.h` + - `accesskit.dir`: Path to accesskit-c root directory containing `include/accesskit.h` + +3. Build and install: + ```bash + cd android + ./gradlew installDebug + ``` diff --git a/examples/sdl/android/.gitignore b/examples/sdl/android/.gitignore new file mode 100644 index 0000000..6757714 --- /dev/null +++ b/examples/sdl/android/.gitignore @@ -0,0 +1,18 @@ +# Gradle +.gradle/ +build/ +local.properties + +# Android Studio +*.iml +.idea/ +.DS_Store + +# Native libraries (user-provided) +app/src/main/jniLibs/ + +# Generated files +*.apk +*.ap_ +*.dex +*.class diff --git a/examples/sdl/android/app/build.gradle b/examples/sdl/android/app/build.gradle new file mode 100644 index 0000000..384d4f3 --- /dev/null +++ b/examples/sdl/android/app/build.gradle @@ -0,0 +1,74 @@ +plugins { + id 'com.android.application' +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withInputStream { localProperties.load(it) } +} + +def sdl2Dir = localProperties.getProperty('sdl2.dir') +def accesskitDir = localProperties.getProperty('accesskit.dir') + +android { + namespace = 'dev.accesskit.sdl_example' + compileSdk = 34 + + defaultConfig { + applicationId = "dev.accesskit.sdl_example" + minSdk = 28 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + ndk { + abiFilters 'arm64-v8a', 'x86_64' + } + + externalNativeBuild { + cmake { + def cmakeArgs = ["-DANDROID_STL=c++_shared"] + if (sdl2Dir != null) { + cmakeArgs.add("-DSDL2_DIR=${sdl2Dir}") + } + if (accesskitDir != null) { + cmakeArgs.add("-DACCESSKIT_DIR=${accesskitDir}") + } + arguments(*cmakeArgs) + } + } + } + + buildTypes { + release { + minifyEnabled = false + proguardFiles(getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro') + } + } + + externalNativeBuild { + cmake { + path = "src/main/jni/CMakeLists.txt" + version = "3.22.1" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + sourceSets { + main { + jniLibs.srcDirs = ['src/main/jniLibs'] + if (sdl2Dir != null) { + java.srcDirs += "${sdl2Dir}/android-project/app/src/main/java" + } + } + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' +} diff --git a/examples/sdl/android/app/proguard-rules.pro b/examples/sdl/android/app/proguard-rules.pro new file mode 100644 index 0000000..e61e443 --- /dev/null +++ b/examples/sdl/android/app/proguard-rules.pro @@ -0,0 +1,8 @@ +# Add project specific ProGuard rules here. + +# Keep native method names +-keepclasseswithmembernames class * { + native ; +} + +-keep class dev.accesskit.sdl_example.AccessKitSDLActivity { *; } diff --git a/examples/sdl/android/app/src/main/AndroidManifest.xml b/examples/sdl/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5370e0b --- /dev/null +++ b/examples/sdl/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/examples/sdl/android/app/src/main/java/dev/accesskit/sdl_example/AccessKitSDLActivity.java b/examples/sdl/android/app/src/main/java/dev/accesskit/sdl_example/AccessKitSDLActivity.java new file mode 100644 index 0000000..e87bf5b --- /dev/null +++ b/examples/sdl/android/app/src/main/java/dev/accesskit/sdl_example/AccessKitSDLActivity.java @@ -0,0 +1,130 @@ +package dev.accesskit.sdl_example; + +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import org.libsdl.app.SDLActivity; +import org.libsdl.app.SDLSurface; + +public class AccessKitSDLActivity extends SDLActivity { + private static long mAccessKitAdapter = 0; + + private static native long nativeCreateAccessKitAdapter(); + + private static native void nativeFreeAccessKitAdapter(long adapter); + + private static native AccessibilityNodeInfo nativeCreateAccessibilityNodeInfo( + long adapter, View host, int virtualViewId); + + private static native AccessibilityNodeInfo nativeFindFocus( + long adapter, View host, int focusType); + + private static native boolean nativePerformAction( + long adapter, View host, int virtualViewId, int action, Bundle arguments); + + private static native void nativeUpdateAccessibility(long adapter, View host); + + private static native boolean nativeOnHoverEvent( + long adapter, View host, int action, float x, float y); + + @Override + protected String[] getLibraries() { + return new String[] {"SDL2", "hello_world"}; + } + + @Override + protected SDLSurface createSDLSurface(Context context) { + return new AccessKitSDLSurface(context); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAccessKitAdapter = nativeCreateAccessKitAdapter(); + } + + @Override + protected void onDestroy() { + if (mAccessKitAdapter != 0) { + nativeFreeAccessKitAdapter(mAccessKitAdapter); + mAccessKitAdapter = 0; + } + super.onDestroy(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + if (event.getAction() == KeyEvent.ACTION_UP) { + finish(); + } + return true; + } + return super.dispatchKeyEvent(event); + } + + @Override + public void setOrientationBis(int w, int h, boolean resizable, String hint) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + + static long getAccessKitAdapter() { + return mAccessKitAdapter; + } + + public static void requestAccessibilityUpdate() { + if (mSingleton != null && mSurface != null && mAccessKitAdapter != 0) { + mSingleton.runOnUiThread(() -> nativeUpdateAccessibility(mAccessKitAdapter, mSurface)); + } + } + + private class AccessKitSDLSurface extends SDLSurface { + public AccessKitSDLSurface(Context context) { + super(context); + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + long adapter = getAccessKitAdapter(); + if (adapter != 0 + && nativeOnHoverEvent( + adapter, this, event.getAction(), event.getX(), event.getY())) { + return true; + } + return super.onHoverEvent(event); + } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + long adapter = getAccessKitAdapter(); + if (adapter == 0) { + return super.getAccessibilityNodeProvider(); + } + return new AccessibilityNodeProvider() { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return nativeCreateAccessibilityNodeInfo( + adapter, AccessKitSDLSurface.this, virtualViewId); + } + + @Override + public AccessibilityNodeInfo findFocus(int focusType) { + return nativeFindFocus(adapter, AccessKitSDLSurface.this, focusType); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return nativePerformAction( + adapter, AccessKitSDLSurface.this, virtualViewId, action, arguments); + } + }; + } + } +} diff --git a/examples/sdl/android/app/src/main/jni/CMakeLists.txt b/examples/sdl/android/app/src/main/jni/CMakeLists.txt new file mode 100644 index 0000000..fcf1c27 --- /dev/null +++ b/examples/sdl/android/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.22.1) +project(hello_world) +set(JNILIBS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}") +set(SDL2_DIR "" CACHE PATH "Path to SDL2 source/headers for Android") + +if(SDL2_DIR STREQUAL "") + message(FATAL_ERROR "SDL2_DIR must be set to the SDL2 source directory (containing include/SDL.h)") +endif() +add_library(SDL2 SHARED IMPORTED) +set_target_properties(SDL2 PROPERTIES + IMPORTED_LOCATION "${JNILIBS_DIR}/libSDL2.so" +) +set(SDL2_INCLUDE_DIR "${SDL2_DIR}/include") +set(ACCESSKIT_DIR "" CACHE PATH "Path to AccessKit C root directory") + +if(ACCESSKIT_DIR STREQUAL "") + message(FATAL_ERROR "ACCESSKIT_DIR must be set to the accesskit-c root directory (containing include/accesskit.h)") +endif() +add_library(accesskit STATIC IMPORTED) +set_target_properties(accesskit PROPERTIES + IMPORTED_LOCATION "${JNILIBS_DIR}/libaccesskit.a" +) +set(ACCESSKIT_INCLUDE_DIR "${ACCESSKIT_DIR}/include") +set(HELLO_WORLD_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../hello_world.c") +add_library(hello_world SHARED + ${HELLO_WORLD_SRC} + android_jni.c +) + +target_include_directories(hello_world PRIVATE + ${SDL2_INCLUDE_DIR} + ${ACCESSKIT_INCLUDE_DIR} +) + +target_link_libraries(hello_world + SDL2 + accesskit + android + log +) + diff --git a/examples/sdl/android/app/src/main/jni/android_jni.c b/examples/sdl/android/app/src/main/jni/android_jni.c new file mode 100644 index 0000000..a92ea7f --- /dev/null +++ b/examples/sdl/android/app/src/main/jni/android_jni.c @@ -0,0 +1,170 @@ +#include +#include +#include +#include +#include + +extern accesskit_tree_update *build_initial_tree(void *userdata); +extern void do_action(accesskit_action_request *request, void *userdata); +extern void *get_window_state(void); +extern void *get_action_handler_state(void); +extern accesskit_tree_update *get_pending_update(void); + +void android_request_accessibility_update(void) { + JNIEnv *env = SDL_AndroidGetJNIEnv(); + if (env != NULL) { + jclass cls = (*env)->FindClass( + env, "dev/accesskit/sdl_example/AccessKitSDLActivity"); + if (cls != NULL) { + jmethodID method = (*env)->GetStaticMethodID( + env, cls, "requestAccessibilityUpdate", "()V"); + if (method != NULL) { + (*env)->CallStaticVoidMethod(env, cls, method); + } + (*env)->DeleteLocalRef(env, cls); + } + } +} + +static accesskit_android_adapter *g_adapter = NULL; + +static accesskit_tree_update *pending_update_factory(void *userdata) { + (void)userdata; + return get_pending_update(); +} + +static jobject g_host = NULL; + +JNIEXPORT jlong JNICALL +Java_dev_accesskit_sdl_1example_AccessKitSDLActivity_nativeCreateAccessKitAdapter( + JNIEnv *env, jclass cls) { + (void)env; + (void)cls; + g_adapter = accesskit_android_adapter_new(); + return (jlong)(uintptr_t)g_adapter; +} + +JNIEXPORT void JNICALL +Java_dev_accesskit_sdl_1example_AccessKitSDLActivity_nativeFreeAccessKitAdapter( + JNIEnv *env, jclass cls, jlong adapter) { + (void)cls; + (void)adapter; + if (g_host != NULL) { + (*env)->DeleteGlobalRef(env, g_host); + g_host = NULL; + } + if (g_adapter != NULL) { + accesskit_android_adapter_free(g_adapter); + g_adapter = NULL; + } +} + +JNIEXPORT jobject JNICALL +Java_dev_accesskit_sdl_1example_AccessKitSDLActivity_nativeCreateAccessibilityNodeInfo( + JNIEnv *env, jclass cls, jlong adapter_ptr, jobject host, + jint virtualViewId) { + (void)cls; + (void)adapter_ptr; + + if (g_host == NULL && host != NULL) { + g_host = (*env)->NewGlobalRef(env, host); + } + + void *window_state = get_window_state(); + if (window_state == NULL || g_adapter == NULL) { + return NULL; + } + + return accesskit_android_adapter_create_accessibility_node_info( + g_adapter, build_initial_tree, window_state, env, host, virtualViewId); +} + +JNIEXPORT jobject JNICALL +Java_dev_accesskit_sdl_1example_AccessKitSDLActivity_nativeFindFocus( + JNIEnv *env, jclass cls, jlong adapter_ptr, jobject host, jint focusType) { + (void)cls; + (void)adapter_ptr; + + void *window_state = get_window_state(); + if (window_state == NULL || g_adapter == NULL) { + return NULL; + } + + return accesskit_android_adapter_find_focus( + g_adapter, build_initial_tree, window_state, env, host, focusType); +} + +JNIEXPORT jboolean JNICALL +Java_dev_accesskit_sdl_1example_AccessKitSDLActivity_nativePerformAction( + JNIEnv *env, jclass cls, jlong adapter_ptr, jobject host, + jint virtualViewId, jint action, jobject arguments) { + (void)cls; + (void)adapter_ptr; + + void *action_handler_state = get_action_handler_state(); + if (action_handler_state == NULL || g_adapter == NULL) { + return JNI_FALSE; + } + + accesskit_android_platform_action *platform_action = + accesskit_android_platform_action_from_java(env, action, arguments); + if (platform_action == NULL) { + return JNI_FALSE; + } + + accesskit_android_queued_events *events = + accesskit_android_adapter_perform_action(g_adapter, do_action, + action_handler_state, + virtualViewId, platform_action); + + accesskit_android_platform_action_free(platform_action); + + if (events != NULL) { + accesskit_android_queued_events_raise(events, env, host); + } + + return JNI_TRUE; +} + +JNIEXPORT void JNICALL +Java_dev_accesskit_sdl_1example_AccessKitSDLActivity_nativeUpdateAccessibility( + JNIEnv *env, jclass cls, jlong adapter_ptr, jobject host) { + (void)cls; + (void)adapter_ptr; + + if (g_adapter == NULL || host == NULL) { + return; + } + + accesskit_android_queued_events *events = + accesskit_android_adapter_update_if_active(g_adapter, + pending_update_factory, NULL); + + if (events != NULL) { + accesskit_android_queued_events_raise(events, env, host); + } +} + +JNIEXPORT jboolean JNICALL +Java_dev_accesskit_sdl_1example_AccessKitSDLActivity_nativeOnHoverEvent( + JNIEnv *env, jclass cls, jlong adapter_ptr, jobject host, jint action, + jfloat x, jfloat y) { + (void)cls; + (void)adapter_ptr; + + void *window_state = get_window_state(); + if (window_state == NULL || g_adapter == NULL) { + return JNI_FALSE; + } + + accesskit_android_queued_events *events = + accesskit_android_adapter_on_hover_event(g_adapter, build_initial_tree, + window_state, action, x, y); + + if (events != NULL) { + accesskit_android_queued_events_raise(events, env, host); + return JNI_TRUE; + } + + return JNI_FALSE; +} diff --git a/examples/sdl/android/app/src/main/res/values/strings.xml b/examples/sdl/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c725031 --- /dev/null +++ b/examples/sdl/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + AccessKit SDL Example + diff --git a/examples/sdl/android/build.gradle b/examples/sdl/android/build.gradle new file mode 100644 index 0000000..d7fbab3 --- /dev/null +++ b/examples/sdl/android/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '8.7.3' apply false +} diff --git a/examples/sdl/android/gradle.properties b/examples/sdl/android/gradle.properties new file mode 100644 index 0000000..3fe510f --- /dev/null +++ b/examples/sdl/android/gradle.properties @@ -0,0 +1,10 @@ +# Project-wide Gradle settings. + +# Android operating system will allocate maximum heap size +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + +# AndroidX package structure +android.useAndroidX=true + +# Enables namespacing of each library's R class +android.nonTransitiveRClass=true diff --git a/examples/sdl/android/gradle/wrapper/gradle-wrapper.jar b/examples/sdl/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/examples/sdl/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/sdl/android/gradle/wrapper/gradle-wrapper.properties b/examples/sdl/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a76285b --- /dev/null +++ b/examples/sdl/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon May 02 15:39:12 BST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/examples/sdl/android/gradlew b/examples/sdl/android/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/examples/sdl/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/examples/sdl/android/gradlew.bat b/examples/sdl/android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/examples/sdl/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/sdl/android/settings.gradle b/examples/sdl/android/settings.gradle new file mode 100644 index 0000000..b2a934f --- /dev/null +++ b/examples/sdl/android/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "AccessKitSDLExample" +include ':app' diff --git a/examples/sdl/hello_world.c b/examples/sdl/hello_world.c index 9deb453..6444b01 100644 --- a/examples/sdl/hello_world.c +++ b/examples/sdl/hello_world.c @@ -5,11 +5,23 @@ #include "accesskit.h" -#if (defined(__linux__) || defined(__DragonFly__) || defined(__FreeBSD__) || \ - defined(__NetBSD__) || defined(__OpenBSD__)) +#if defined(__ANDROID__) +extern void android_request_accessibility_update(void); +#endif + +#if ((defined(__linux__) || defined(__DragonFly__) || defined(__FreeBSD__) || \ + defined(__NetBSD__) || defined(__OpenBSD__)) && \ + !defined(__ANDROID__)) #define UNIX #endif +#if defined(__ANDROID__) +/* Android-specific globals for marshaling updates to UI thread */ +static accesskit_tree_update_factory g_pending_update_factory = NULL; +static void *g_pending_update_userdata = NULL; +static SDL_mutex *g_update_mutex = NULL; +#endif + const char WINDOW_TITLE[] = "Hello world"; const accesskit_node_id WINDOW_ID = 0; @@ -18,9 +30,9 @@ const accesskit_node_id BUTTON_2_ID = 2; const accesskit_node_id ANNOUNCEMENT_ID = 3; #define INITIAL_FOCUS BUTTON_1_ID -const accesskit_rect BUTTON_1_RECT = {20.0, 20.0, 100.0, 60.0}; +const accesskit_rect BUTTON_1_RECT = {20.0, 50.0, 400.0, 200.0}; -const accesskit_rect BUTTON_2_RECT = {20.0, 60.0, 100.0, 100.0}; +const accesskit_rect BUTTON_2_RECT = {20.0, 250.0, 400.0, 400.0}; const Sint32 SET_FOCUS_MSG = 0; const Sint32 DO_DEFAULT_ACTION_MSG = 1; @@ -55,6 +67,10 @@ struct accesskit_sdl_adapter { accesskit_unix_adapter *adapter; #elif defined(_WIN32) accesskit_windows_subclassing_adapter *adapter; +#elif defined(__ANDROID__) + /* On Android, the adapter is owned by the UI thread (JNI side). + This struct only exists for API compatibility. */ + int dummy; #endif }; @@ -86,19 +102,38 @@ void accesskit_sdl_adapter_init( adapter->adapter = accesskit_windows_subclassing_adapter_new( wmInfo.info.win.window, activation_handler, activation_handler_userdata, action_handler, action_handler_userdata); +#elif defined(__ANDROID__) + (void)adapter; + (void)window; + (void)activation_handler; + (void)activation_handler_userdata; + (void)action_handler; + (void)action_handler_userdata; + (void)deactivation_handler; + (void)deactivation_handler_userdata; + /* On Android, the adapter is owned by the UI thread (JNI side). + Nothing to do here. */ #endif } void accesskit_sdl_adapter_destroy(struct accesskit_sdl_adapter *adapter) { - if (adapter->adapter != NULL) { #if defined(__APPLE__) + if (adapter->adapter != NULL) { accesskit_macos_subclassing_adapter_free(adapter->adapter); + } #elif defined(UNIX) + if (adapter->adapter != NULL) { accesskit_unix_adapter_free(adapter->adapter); + } #elif defined(_WIN32) + if (adapter->adapter != NULL) { accesskit_windows_subclassing_adapter_free(adapter->adapter); -#endif } +#elif defined(__ANDROID__) + /* On Android, the adapter is owned by the UI thread (JNI side). + Nothing to do here. */ + (void)adapter; +#endif } void accesskit_sdl_adapter_update_if_active( @@ -122,6 +157,14 @@ void accesskit_sdl_adapter_update_if_active( if (events != NULL) { accesskit_windows_queued_events_raise(events); } +#elif defined(__ANDROID__) + (void)adapter; + SDL_LockMutex(g_update_mutex); + g_pending_update_factory = update_factory; + g_pending_update_userdata = update_factory_userdata; + SDL_UnlockMutex(g_update_mutex); + + android_request_accessibility_update(); #endif } @@ -137,6 +180,10 @@ void accesskit_sdl_adapter_update_window_focus_state( #elif defined(UNIX) accesskit_unix_adapter_update_window_focus_state(adapter->adapter, is_focused); +#elif defined(__ANDROID__) + /* On Android, focus is handled by the system */ + (void)adapter; + (void)is_focused; #endif /* On Windows, the subclassing adapter takes care of this. */ } @@ -154,6 +201,10 @@ void accesskit_sdl_adapter_update_root_window_bounds( accesskit_rect inner_bounds = {x, y, x + width, y + height}; accesskit_unix_adapter_set_root_window_bounds(adapter->adapter, outer_bounds, inner_bounds); +#elif defined(__ANDROID__) + /* On Android, bounds are managed by the system */ + (void)adapter; + (void)window; #endif } @@ -286,7 +337,36 @@ void deactivate_accessibility(void *userdata) { is active, so there's nothing to do here. */ } +#if defined(__ANDROID__) +/* On Android, we need global state accessible from JNI. + The adapter is owned by the UI thread (JNI side), not here. */ +static struct window_state *g_window_state = NULL; +static struct action_handler_state *g_action_handler_state = NULL; + +void *get_window_state(void) { return g_window_state; } +void *get_action_handler_state(void) { return g_action_handler_state; } + +/* Called from JNI on UI thread to get and clear the pending update */ +accesskit_tree_update *get_pending_update(void) { + SDL_LockMutex(g_update_mutex); + accesskit_tree_update *update = NULL; + if (g_pending_update_factory != NULL) { + update = g_pending_update_factory(g_pending_update_userdata); + g_pending_update_factory = NULL; + g_pending_update_userdata = NULL; + } + SDL_UnlockMutex(g_update_mutex); + return update; +} + +/* SDL uses SDL_main on Android */ +#define main SDL_main +#endif + int main(int argc, char *argv[]) { + (void)argc; + (void)argv; +#if !defined(__ANDROID__) printf("This example has no visible GUI, and a keyboard interface:\n"); printf("- [Tab] switches focus between two logical buttons.\n"); printf( @@ -299,6 +379,7 @@ int main(int argc, char *argv[]) { #elif defined(UNIX) printf("Enable Orca with [Super]+[Alt]+[S].\n"); #endif +#endif /* !__ANDROID__ */ if (SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SDL initialization failed: (%s)\n", SDL_GetError()); return -1; @@ -317,6 +398,11 @@ int main(int argc, char *argv[]) { SDL_Surface *screen_surface = SDL_GetWindowSurface(window); Uint32 window_id = SDL_GetWindowID(window); struct action_handler_state action_handler = {user_event, window_id}; +#if defined(__ANDROID__) + g_window_state = &state; + g_action_handler_state = &action_handler; + g_update_mutex = SDL_CreateMutex(); +#endif struct accesskit_sdl_adapter adapter; accesskit_sdl_adapter_init(&adapter, window, build_initial_tree, &state, do_action, &action_handler, @@ -380,7 +466,14 @@ int main(int argc, char *argv[]) { } accesskit_sdl_adapter_destroy(&adapter); +#if defined(__ANDROID__) + g_window_state = NULL; + g_action_handler_state = NULL; + SDL_DestroyMutex(g_update_mutex); + g_update_mutex = NULL; +#endif window_state_destroy(&state); + SDL_DestroyWindow(window); SDL_Quit(); return 0; } diff --git a/include/accesskit.h b/include/accesskit.h index 8df227f..21b5516 100644 --- a/include/accesskit.h +++ b/include/accesskit.h @@ -16,6 +16,9 @@ #ifdef _WIN32 #include #endif +#ifdef __ANDROID__ +#include +#endif /** * An action to be taken on an accessibility node. @@ -569,6 +572,24 @@ enum accesskit_vertical_offset typedef uint8_t accesskit_vertical_offset; #endif // __cplusplus +#if defined(__ANDROID__) +typedef struct accesskit_android_adapter accesskit_android_adapter; +#endif + +#if defined(__ANDROID__) +typedef struct accesskit_android_injecting_adapter + accesskit_android_injecting_adapter; +#endif + +#if defined(__ANDROID__) +typedef struct accesskit_android_platform_action + accesskit_android_platform_action; +#endif + +#if defined(__ANDROID__) +typedef struct accesskit_android_queued_events accesskit_android_queued_events; +#endif + typedef struct accesskit_custom_action accesskit_custom_action; #if defined(__APPLE__) @@ -999,13 +1020,6 @@ typedef struct accesskit_size { double height; } accesskit_size; -/** - * Ownership of `request` is transferred to the callback. `request` must - * be freed using `accesskit_action_request_free`. - */ -typedef void (*accesskit_action_handler_callback)( - struct accesskit_action_request *request, void *userdata); - typedef void *accesskit_tree_update_factory_userdata; /** @@ -1018,6 +1032,13 @@ typedef struct accesskit_tree_update *(*accesskit_tree_update_factory)( typedef struct accesskit_tree_update *(*accesskit_activation_handler_callback)( void *userdata); +/** + * Ownership of `request` is transferred to the callback. `request` must + * be freed using `accesskit_action_request_free`. + */ +typedef void (*accesskit_action_handler_callback)( + struct accesskit_action_request *request, void *userdata); + typedef void (*accesskit_deactivation_handler_callback)(void *userdata); #if defined(_WIN32) @@ -2394,6 +2415,108 @@ struct accesskit_vec2 accesskit_vec2_scale(struct accesskit_vec2 vec, struct accesskit_vec2 accesskit_vec2_neg(struct accesskit_vec2 vec); +#if defined(__ANDROID__) +struct accesskit_android_platform_action * +accesskit_android_platform_action_from_java(JNIEnv *env, jint action, + jobject arguments); +#endif + +#if defined(__ANDROID__) +void accesskit_android_platform_action_free( + struct accesskit_android_platform_action *action); +#endif + +#if defined(__ANDROID__) +/** + * Memory is also freed when calling this function. + */ +void accesskit_android_queued_events_raise( + struct accesskit_android_queued_events *events, JNIEnv *env, jobject host); +#endif + +#if defined(__ANDROID__) +struct accesskit_android_adapter *accesskit_android_adapter_new(void); +#endif + +#if defined(__ANDROID__) +void accesskit_android_adapter_free(struct accesskit_android_adapter *adapter); +#endif + +#if defined(__ANDROID__) +/** + * You must call `accesskit_android_queued_events_raise` on the returned + * pointer. It can be null if the adapter is not active. + */ +struct accesskit_android_queued_events * +accesskit_android_adapter_update_if_active( + struct accesskit_android_adapter *adapter, + accesskit_tree_update_factory update_factory, + void *update_factory_userdata); +#endif + +#if defined(__ANDROID__) +jobject accesskit_android_adapter_create_accessibility_node_info( + struct accesskit_android_adapter *adapter, + accesskit_activation_handler_callback activation_handler, + void *activation_handler_userdata, JNIEnv *env, jobject host, + jint virtual_view_id); +#endif + +#if defined(__ANDROID__) +jobject accesskit_android_adapter_find_focus( + struct accesskit_android_adapter *adapter, + accesskit_activation_handler_callback activation_handler, + void *activation_handler_userdata, JNIEnv *env, jobject host, + jint focus_type); +#endif + +#if defined(__ANDROID__) +/** + * You must call `accesskit_android_queued_events_raise` on the returned + * pointer. It can be null if the adapter is not active. + */ +struct accesskit_android_queued_events * +accesskit_android_adapter_perform_action( + struct accesskit_android_adapter *adapter, + accesskit_action_handler_callback action_handler, + void *action_handler_userdata, jint virtual_view_id, + const struct accesskit_android_platform_action *action); +#endif + +#if defined(__ANDROID__) +/** + * You must call `accesskit_android_queued_events_raise` on the returned + * pointer. It can be null if the adapter is not active. + */ +struct accesskit_android_queued_events * +accesskit_android_adapter_on_hover_event( + struct accesskit_android_adapter *adapter, + accesskit_activation_handler_callback activation_handler, + void *activation_handler_userdata, jint action, jfloat x, jfloat y); +#endif + +#if defined(__ANDROID__) +struct accesskit_android_injecting_adapter * +accesskit_android_injecting_adapter_new( + JNIEnv *env, jobject host, + accesskit_activation_handler_callback activation_handler, + void *activation_handler_userdata, + accesskit_action_handler_callback action_handler, + void *action_handler_userdata); +#endif + +#if defined(__ANDROID__) +void accesskit_android_injecting_adapter_free( + struct accesskit_android_injecting_adapter *adapter); +#endif + +#if defined(__ANDROID__) +void accesskit_android_injecting_adapter_update_if_active( + struct accesskit_android_injecting_adapter *adapter, + accesskit_tree_update_factory update_factory, + void *update_factory_userdata); +#endif + #if defined(__APPLE__) /** * Memory is also freed when calling this function. diff --git a/meson.build b/meson.build index f8dd60e..147ed01 100644 --- a/meson.build +++ b/meson.build @@ -259,10 +259,14 @@ endif if toolchain_arg != [] cargo_wrapper_args += toolchain_arg endif +if get_option('android-embedded-dex') + cargo_wrapper_args += ['--features', 'android-embedded-dex'] +endif library_sources = files( 'Cargo.lock', 'Cargo.toml', + 'src/android.rs', 'src/common.rs', 'src/geometry.rs', 'src/lib.rs', diff --git a/meson/cargo_wrapper.py b/meson/cargo_wrapper.py index dc80ae0..bee01c8 100755 --- a/meson/cargo_wrapper.py +++ b/meson/cargo_wrapper.py @@ -80,6 +80,10 @@ "--extension", required=True, help="filename extension for the library (so, a, dll, lib, dylib)", ) +parser.add_argument( + "--features", action="append", default=[], help="Cargo features to enable", +) + args = parser.parse_args() if args.toolchain_version is not None and args.target is None and args.build_triplet is None: @@ -103,7 +107,7 @@ ) env["PKG_CONFIG_PATH"] = os.pathsep.join(pkg_config_path) -features = [] +features = args.features cargo_cmd = [Path(args.cargo).as_posix()] diff --git a/meson_options.txt b/meson_options.txt index 301b954..650834a 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -23,3 +23,8 @@ option('rustc-version', type: 'string', value: '', description: 'Installed RustC version to use (if needed; currently only supported for Windows builds)') + +option('android-embedded-dex', + type: 'boolean', + value: false, + description: 'Embed the classes.dex file in the library (Android only)') diff --git a/src/android.rs b/src/android.rs new file mode 100644 index 0000000..ede4c11 --- /dev/null +++ b/src/android.rs @@ -0,0 +1,230 @@ +// Copyright 2026 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file) or the MIT license (found in +// the LICENSE-MIT file), at your option. + +use accesskit_android::*; +use std::os::raw::c_void; + +use crate::{ + box_from_ptr, mut_from_ptr, ref_from_ptr, tree_update_factory, tree_update_factory_userdata, + ActionHandlerCallback, ActivationHandlerCallback, BoxCastPtr, CastPtr, FfiActionHandler, + FfiActivationHandler, +}; + +pub struct android_platform_action { + _private: [u8; 0], +} + +impl CastPtr for android_platform_action { + type RustType = PlatformAction; +} + +impl BoxCastPtr for android_platform_action {} + +impl android_platform_action { + #[no_mangle] + pub extern "C" fn accesskit_android_platform_action_from_java( + env: *mut jni::sys::JNIEnv, + action: jni::sys::jint, + arguments: jni::sys::jobject, + ) -> *mut android_platform_action { + let mut env = unsafe { jni::JNIEnv::from_raw(env).unwrap() }; + let arguments = unsafe { jni::objects::JObject::from_raw(arguments) }; + let platform_action = PlatformAction::from_java(&mut env, action, &arguments); + BoxCastPtr::to_nullable_mut_ptr(platform_action) + } + + #[no_mangle] + pub extern "C" fn accesskit_android_platform_action_free(action: *mut android_platform_action) { + drop(box_from_ptr(action)); + } +} + +pub struct android_queued_events { + _private: [u8; 0], +} + +impl CastPtr for android_queued_events { + type RustType = QueuedEvents; +} + +impl BoxCastPtr for android_queued_events {} + +impl android_queued_events { + /// Memory is also freed when calling this function. + #[no_mangle] + pub extern "C" fn accesskit_android_queued_events_raise( + events: *mut android_queued_events, + env: *mut jni::sys::JNIEnv, + host: jni::sys::jobject, + ) { + let events = box_from_ptr(events); + let mut env = unsafe { jni::JNIEnv::from_raw(env).unwrap() }; + let host = unsafe { jni::objects::JObject::from_raw(host) }; + events.raise(&mut env, &host); + } +} + +pub struct android_adapter { + _private: [u8; 0], +} + +impl CastPtr for android_adapter { + type RustType = Adapter; +} + +impl BoxCastPtr for android_adapter {} + +impl android_adapter { + #[no_mangle] + pub extern "C" fn accesskit_android_adapter_new() -> *mut android_adapter { + let adapter = Adapter::default(); + BoxCastPtr::to_mut_ptr(adapter) + } + + #[no_mangle] + pub extern "C" fn accesskit_android_adapter_free(adapter: *mut android_adapter) { + drop(box_from_ptr(adapter)); + } + + /// You must call `accesskit_android_queued_events_raise` on the returned pointer. It can be null if the adapter is not active. + #[no_mangle] + pub extern "C" fn accesskit_android_adapter_update_if_active( + adapter: *mut android_adapter, + update_factory: tree_update_factory, + update_factory_userdata: *mut c_void, + ) -> *mut android_queued_events { + let update_factory = update_factory.unwrap(); + let update_factory_userdata = tree_update_factory_userdata(update_factory_userdata); + let adapter = mut_from_ptr(adapter); + let events = + adapter.update_if_active(|| *box_from_ptr(update_factory(update_factory_userdata))); + BoxCastPtr::to_nullable_mut_ptr(events) + } + + #[no_mangle] + pub extern "C" fn accesskit_android_adapter_create_accessibility_node_info( + adapter: *mut android_adapter, + activation_handler: ActivationHandlerCallback, + activation_handler_userdata: *mut c_void, + env: *mut jni::sys::JNIEnv, + host: jni::sys::jobject, + virtual_view_id: jni::sys::jint, + ) -> jni::sys::jobject { + let adapter = mut_from_ptr(adapter); + let mut activation_handler = + FfiActivationHandler::new(activation_handler, activation_handler_userdata); + let mut env = unsafe { jni::JNIEnv::from_raw(env).unwrap() }; + let host = unsafe { jni::objects::JObject::from_raw(host) }; + adapter + .create_accessibility_node_info( + &mut activation_handler, + &mut env, + &host, + virtual_view_id, + ) + .into_raw() + } + + #[no_mangle] + pub extern "C" fn accesskit_android_adapter_find_focus( + adapter: *mut android_adapter, + activation_handler: ActivationHandlerCallback, + activation_handler_userdata: *mut c_void, + env: *mut jni::sys::JNIEnv, + host: jni::sys::jobject, + focus_type: jni::sys::jint, + ) -> jni::sys::jobject { + let adapter = mut_from_ptr(adapter); + let mut activation_handler = + FfiActivationHandler::new(activation_handler, activation_handler_userdata); + let mut env = unsafe { jni::JNIEnv::from_raw(env).unwrap() }; + let host = unsafe { jni::objects::JObject::from_raw(host) }; + adapter + .find_focus(&mut activation_handler, &mut env, &host, focus_type) + .into_raw() + } + + /// You must call `accesskit_android_queued_events_raise` on the returned pointer. It can be null if the adapter is not active. + #[no_mangle] + pub extern "C" fn accesskit_android_adapter_perform_action( + adapter: *mut android_adapter, + action_handler: ActionHandlerCallback, + action_handler_userdata: *mut c_void, + virtual_view_id: jni::sys::jint, + action: *const android_platform_action, + ) -> *mut android_queued_events { + let adapter = mut_from_ptr(adapter); + let mut action_handler = FfiActionHandler::new(action_handler, action_handler_userdata); + let action = ref_from_ptr(action); + let events = adapter.perform_action(&mut action_handler, virtual_view_id, action); + BoxCastPtr::to_nullable_mut_ptr(events) + } + + /// You must call `accesskit_android_queued_events_raise` on the returned pointer. It can be null if the adapter is not active. + #[no_mangle] + pub extern "C" fn accesskit_android_adapter_on_hover_event( + adapter: *mut android_adapter, + activation_handler: ActivationHandlerCallback, + activation_handler_userdata: *mut c_void, + action: jni::sys::jint, + x: jni::sys::jfloat, + y: jni::sys::jfloat, + ) -> *mut android_queued_events { + let adapter = mut_from_ptr(adapter); + let mut activation_handler = + FfiActivationHandler::new(activation_handler, activation_handler_userdata); + let events = adapter.on_hover_event(&mut activation_handler, action, x, y); + BoxCastPtr::to_nullable_mut_ptr(events) + } +} + +pub struct android_injecting_adapter { + _private: [u8; 0], +} + +impl CastPtr for android_injecting_adapter { + type RustType = InjectingAdapter; +} + +impl BoxCastPtr for android_injecting_adapter {} + +impl android_injecting_adapter { + #[no_mangle] + pub extern "C" fn accesskit_android_injecting_adapter_new( + env: *mut jni::sys::JNIEnv, + host: jni::sys::jobject, + activation_handler: ActivationHandlerCallback, + activation_handler_userdata: *mut c_void, + action_handler: ActionHandlerCallback, + action_handler_userdata: *mut c_void, + ) -> *mut android_injecting_adapter { + let mut env = unsafe { jni::JNIEnv::from_raw(env).unwrap() }; + let host = unsafe { jni::objects::JObject::from_raw(host) }; + let activation_handler = + FfiActivationHandler::new(activation_handler, activation_handler_userdata); + let action_handler = FfiActionHandler::new(action_handler, action_handler_userdata); + let adapter = InjectingAdapter::new(&mut env, &host, activation_handler, action_handler); + BoxCastPtr::to_mut_ptr(adapter) + } + + #[no_mangle] + pub extern "C" fn accesskit_android_injecting_adapter_free( + adapter: *mut android_injecting_adapter, + ) { + drop(box_from_ptr(adapter)); + } + + #[no_mangle] + pub extern "C" fn accesskit_android_injecting_adapter_update_if_active( + adapter: *mut android_injecting_adapter, + update_factory: tree_update_factory, + update_factory_userdata: *mut c_void, + ) { + let update_factory = update_factory.unwrap(); + let update_factory_userdata = tree_update_factory_userdata(update_factory_userdata); + let adapter = mut_from_ptr(adapter); + adapter.update_if_active(|| *box_from_ptr(update_factory(update_factory_userdata))); + } +} diff --git a/src/lib.rs b/src/lib.rs index 22772e6..d3a80cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,8 @@ mod common; mod geometry; +#[cfg(any(target_os = "android", feature = "cbindgen"))] +mod android; #[cfg(any(target_os = "macos", feature = "cbindgen"))] mod macos; #[cfg(any( @@ -36,6 +38,8 @@ use std::{ slice, }; +#[cfg(any(target_os = "android", feature = "cbindgen"))] +pub use android::*; pub use common::*; pub use geometry::*; #[cfg(any(target_os = "macos", feature = "cbindgen"))]