diff --git a/Cargo.lock b/Cargo.lock index 38ffa0661..b53bc853c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,7 @@ version = "0.8.0" dependencies = [ "accesskit", "accesskit_consumer", + "example_common", "jni", "log", ] diff --git a/adapters/android/Cargo.toml b/adapters/android/Cargo.toml index 0e519dcfb..63850262e 100644 --- a/adapters/android/Cargo.toml +++ b/adapters/android/Cargo.toml @@ -8,6 +8,7 @@ categories.workspace = true keywords = ["gui", "ui", "accessibility"] repository.workspace = true readme = "README.md" +exclude = ["examples"] edition.workspace = true rust-version.workspace = true @@ -20,3 +21,9 @@ accesskit_consumer = { version = "0.39.0", path = "../../accesskit_consumer" } jni = "0.21.1" log = "0.4.17" +[dev-dependencies] +example_common = { path = "../../example_common" } + +[[example]] +name = "hello_world" +crate-type = ["cdylib"] diff --git a/adapters/android/README.md b/adapters/android/README.md index 2526b1466..467ec3cd9 100644 --- a/adapters/android/README.md +++ b/adapters/android/README.md @@ -8,3 +8,18 @@ This adapter is implemented in two layers: * The `InjectingAdapter` struct injects accessibility into an arbitrary Android view without requiring the view class to be modified, at the expense of depending on a specific Java class and providing less flexibility in the aspects listed above. The most convenient way to use `InjectingAdapter` is to embed a precompiled `.dex` file containing the associated Java class and its inner classes into the native code. This approach requires the `embedded-dex` Cargo feature. + +## Example + +The `examples/` directory contains a runnable example built on the low-level `Adapter`: + +### Running the example + +Install [cargo-ndk](https://github.com/bbqsrc/cargo-ndk) (version 4 or later) and the Rust target for your device, e.g. `rustup target add aarch64-linux-android`. The Android SDK must be discoverable by Gradle (through `ANDROID_HOME` or a `local.properties` file), and cargo-ndk needs an NDK, which it finds inside the SDK or through `ANDROID_NDK_HOME`. Then, with a device connected or an emulator running: + +```sh +cd examples/hello_world_app +./gradlew installDebug +``` + +By default, the Rust library is only built for `arm64-v8a`. Pass `-PrustAbis=arm64-v8a,x86_64` to build for additional ABIs, such as for an emulator on an x86-64 host. The project can also be opened in Android Studio. diff --git a/adapters/android/examples/hello_world.rs b/adapters/android/examples/hello_world.rs new file mode 100644 index 000000000..469458286 --- /dev/null +++ b/adapters/android/examples/hello_world.rs @@ -0,0 +1,374 @@ +use accesskit::{ActionHandler, ActionRequest, ActivationHandler, TreeUpdate, Vec2}; +use accesskit_android::{ + Adapter, PlatformAction, QueuedEvents, + jni::{ + JNIEnv, JavaVM, NativeMethod, + objects::{JClass, JObject}, + sys::{JNI_FALSE, JNI_TRUE, JNI_VERSION_1_6, jboolean, jfloat, jint, jlong}, + }, +}; +use example_common::{Key, KeyEvent, KeyState, Modifiers, UiState}; +use std::{ + ffi::c_void, + ops::{Deref, DerefMut}, + sync::Mutex, +}; + +const ACTION_DOWN: jint = 0; +const ACTION_UP: jint = 1; +const KEYCODE_TAB: jint = 61; +const KEYCODE_SPACE: jint = 62; +const KEYCODE_ENTER: jint = 66; + +struct Ui(UiState); + +impl ActivationHandler for Ui { + fn request_initial_tree(&mut self) -> Option { + Some(self.0.build_tree_update()) + } +} + +impl ActionHandler for Ui { + fn do_action(&mut self, request: ActionRequest) { + self.0.do_action(&request); + } +} + +impl Deref for Ui { + type Target = UiState; + + fn deref(&self) -> &UiState { + &self.0 + } +} + +impl DerefMut for Ui { + fn deref_mut(&mut self) -> &mut UiState { + &mut self.0 + } +} + +#[derive(Default)] +struct DeferredCallbacks(Vec); + +type DeferredCallback = Box; + +impl DeferredCallbacks { + fn push(&mut self, callback: impl FnOnce(&mut JNIEnv, &JObject) + 'static) { + self.0.push(Box::new(callback)); + } + + fn raise(&mut self, events: QueuedEvents) { + self.push(move |env, host| events.raise(env, host)); + } + + fn run(self, env: &mut JNIEnv, host: &JObject) { + for callback in self.0 { + callback(env, host); + } + } +} + +struct ViewState { + adapter: Adapter, + ui: Ui, +} + +impl ViewState { + fn update_accessibility_tree(&mut self, deferred: &mut DeferredCallbacks) { + let Self { adapter, ui } = self; + if let Some(events) = adapter.update_if_active(|| ui.build_tree_update()) { + deferred.raise(events); + } + } + + fn after_input(&mut self, deferred: &mut DeferredCallbacks) { + self.update_accessibility_tree(deferred); + let Some(delay) = self.ui.time_until_announcement() else { + return; + }; + let delay = delay.as_millis() as jlong; + deferred.push(move |env, host| { + env.call_method(host, "scheduleAnnouncement", "(J)V", &[delay.into()]) + .unwrap(); + }); + } +} + +type ViewHandle = Mutex; + +/// # Safety +/// +/// `handle` must have been returned by `nativeCreate` and not yet passed +/// to `nativeDestroy`. +unsafe fn with_view_state<'local, T>( + env: &mut JNIEnv<'local>, + host: &JObject, + handle: jlong, + f: impl FnOnce(&mut JNIEnv<'local>, &mut ViewState, &mut DeferredCallbacks) -> T, +) -> T { + let state = unsafe { &*(handle as *const ViewHandle) }; + let mut deferred = DeferredCallbacks::default(); + let mut guard = state.lock().unwrap(); + let result = f(env, &mut guard, &mut deferred); + drop(guard); + deferred.run(env, host); + result +} + +fn translate_key(key_code: jint) -> Option { + match key_code { + KEYCODE_ENTER => Some(Key::Enter), + KEYCODE_SPACE => Some(Key::Space), + KEYCODE_TAB => Some(Key::Tab), + _ => None, + } +} + +#[cfg(target_os = "android")] +fn install_panic_hook() { + use std::ffi::{CString, c_char, c_int}; + + #[link(name = "log")] + unsafe extern "C" { + fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int; + } + + const ANDROID_LOG_ERROR: c_int = 6; + + std::panic::set_hook(Box::new(|info| { + let tag = c"AccessKit"; + let text = CString::new(info.to_string()).unwrap_or_default(); + // SAFETY: Both strings are valid, NUL-terminated C strings. + unsafe { __android_log_write(ANDROID_LOG_ERROR, tag.as_ptr(), text.as_ptr()) }; + })); +} + +#[cfg(not(target_os = "android"))] +fn install_panic_hook() {} + +extern "system" fn create(_env: JNIEnv, _class: JClass) -> jlong { + let state = Box::new(Mutex::new(ViewState { + adapter: Adapter::default(), + ui: Ui(UiState::new()), + })); + Box::into_raw(state) as jlong +} + +extern "system" fn destroy(_env: JNIEnv, _class: JClass, handle: jlong) { + // SAFETY: The Java view calls this at most once per handle and never + // uses the handle afterwards. + drop(unsafe { Box::from_raw(handle as *mut ViewHandle) }); +} + +extern "system" fn set_viewport( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + host: JObject, + scale_factor: jfloat, + safe_area_inset_x: jfloat, + safe_area_inset_y: jfloat, +) { + unsafe { + with_view_state(&mut env, &host, handle, |_env, state, deferred| { + state.ui.set_viewport( + scale_factor.into(), + Vec2::new(safe_area_inset_x.into(), safe_area_inset_y.into()), + ); + state.update_accessibility_tree(deferred); + }) + } +} + +extern "system" fn create_accessibility_node_info<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + handle: jlong, + host: JObject<'local>, + virtual_view_id: jint, +) -> JObject<'local> { + unsafe { + with_view_state(&mut env, &host, handle, |env, state, _deferred| { + state + .adapter + .create_accessibility_node_info(&mut state.ui, env, &host, virtual_view_id) + }) + } +} + +extern "system" fn find_focus<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + handle: jlong, + host: JObject<'local>, + focus_type: jint, +) -> JObject<'local> { + unsafe { + with_view_state(&mut env, &host, handle, |env, state, _deferred| { + state + .adapter + .find_focus(&mut state.ui, env, &host, focus_type) + }) + } +} + +extern "system" fn perform_action( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + host: JObject, + virtual_view_id: jint, + action: jint, + arguments: JObject, +) -> jboolean { + unsafe { + with_view_state(&mut env, &host, handle, |env, state, deferred| { + let Some(action) = PlatformAction::from_java(env, action, &arguments) else { + return JNI_FALSE; + }; + let ViewState { adapter, ui } = state; + let Some(events) = adapter.perform_action(ui, virtual_view_id, &action) else { + return JNI_FALSE; + }; + deferred.raise(events); + state.after_input(deferred); + JNI_TRUE + }) + } +} + +extern "system" fn on_hover_event( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + host: JObject, + action: jint, + x: jfloat, + y: jfloat, +) -> jboolean { + unsafe { + with_view_state(&mut env, &host, handle, |_env, state, deferred| { + let ViewState { adapter, ui } = state; + let Some(events) = adapter.on_hover_event(ui, action, x, y) else { + return JNI_FALSE; + }; + deferred.raise(events); + JNI_TRUE + }) + } +} + +extern "system" fn on_key_event( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + host: JObject, + action: jint, + key_code: jint, + shift_pressed: jboolean, +) -> jboolean { + let Some(key) = translate_key(key_code) else { + return JNI_FALSE; + }; + let key_state = match action { + ACTION_DOWN => KeyState::Pressed, + ACTION_UP => KeyState::Released, + _ => return JNI_FALSE, + }; + unsafe { + with_view_state(&mut env, &host, handle, |_env, state, deferred| { + state.ui.handle_key(KeyEvent { + key, + state: key_state, + modifiers: Modifiers { + shift: shift_pressed != JNI_FALSE, + }, + }); + state.after_input(deferred); + JNI_TRUE + }) + } +} + +extern "system" fn flush_announcement( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + host: JObject, +) { + unsafe { + with_view_state(&mut env, &host, handle, |_env, state, deferred| { + if state.ui.flush_announcement() { + state.update_accessibility_tree(deferred); + } + }) + } +} + +/// # Safety +/// +/// `vm` must be a valid pointer to the Java VM. +#[unsafe(no_mangle)] +pub unsafe extern "system" fn JNI_OnLoad( + vm: *mut accesskit_android::jni::sys::JavaVM, + _reserved: *mut c_void, +) -> jint { + install_panic_hook(); + let vm = unsafe { JavaVM::from_raw(vm) }.unwrap(); + let mut env = vm.get_env().unwrap(); + env.register_native_methods( + "dev/accesskit/helloworld/HelloWorldView", + &[ + NativeMethod { + name: "nativeCreate".into(), + sig: "()J".into(), + fn_ptr: create as *mut c_void, + }, + NativeMethod { + name: "nativeDestroy".into(), + sig: "(J)V".into(), + fn_ptr: destroy as *mut c_void, + }, + NativeMethod { + name: "nativeSetViewport".into(), + sig: "(JLandroid/view/View;FFF)V".into(), + fn_ptr: set_viewport as *mut c_void, + }, + NativeMethod { + name: "nativeCreateAccessibilityNodeInfo".into(), + sig: "(JLandroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;" + .into(), + fn_ptr: create_accessibility_node_info as *mut c_void, + }, + NativeMethod { + name: "nativeFindFocus".into(), + sig: "(JLandroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;" + .into(), + fn_ptr: find_focus as *mut c_void, + }, + NativeMethod { + name: "nativePerformAction".into(), + sig: "(JLandroid/view/View;IILandroid/os/Bundle;)Z".into(), + fn_ptr: perform_action as *mut c_void, + }, + NativeMethod { + name: "nativeOnHoverEvent".into(), + sig: "(JLandroid/view/View;IFF)Z".into(), + fn_ptr: on_hover_event as *mut c_void, + }, + NativeMethod { + name: "nativeOnKeyEvent".into(), + sig: "(JLandroid/view/View;IIZ)Z".into(), + fn_ptr: on_key_event as *mut c_void, + }, + NativeMethod { + name: "nativeFlushAnnouncement".into(), + sig: "(JLandroid/view/View;)V".into(), + fn_ptr: flush_announcement as *mut c_void, + }, + ], + ) + .unwrap(); + JNI_VERSION_1_6 +} diff --git a/adapters/android/examples/hello_world_app/.gitignore b/adapters/android/examples/hello_world_app/.gitignore new file mode 100644 index 000000000..8eba115f9 --- /dev/null +++ b/adapters/android/examples/hello_world_app/.gitignore @@ -0,0 +1,4 @@ +.gradle/ +.kotlin/ +build/ +local.properties diff --git a/adapters/android/examples/hello_world_app/app/build.gradle.kts b/adapters/android/examples/hello_world_app/app/build.gradle.kts new file mode 100644 index 000000000..82948779f --- /dev/null +++ b/adapters/android/examples/hello_world_app/app/build.gradle.kts @@ -0,0 +1,96 @@ +import javax.inject.Inject +import org.gradle.process.ExecOperations + +plugins { + alias(libs.plugins.android.application) +} + +// Override with e.g. `-PrustAbis=arm64-v8a,x86_64`; the matching Rust +// targets must be installed. +val rustAbis = (findProperty("rustAbis") as String? ?: "arm64-v8a").split(',') + +android { + namespace = "dev.accesskit.helloworld" + compileSdk = 37 + + defaultConfig { + applicationId = "dev.accesskit.helloworld" + minSdk = 30 + targetSdk = 37 + versionCode = 1 + versionName = "1.0" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + packaging { + jniLibs { + keepDebugSymbols += "**/*.so" + } + } +} + +abstract class CargoNdkBuild @Inject constructor(private val execOperations: ExecOperations) : + DefaultTask() { + @get:Input abstract val abis: ListProperty + @get:Input abstract val apiLevel: Property + @get:Input abstract val release: Property + @get:Input abstract val sdkDir: Property + @get:InputFile abstract val cargoManifest: RegularFileProperty + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + init { + doNotTrackState("cargo tracks its own inputs") + } + + @TaskAction + fun build() { + val manifest = cargoManifest.get().asFile + val outputDir = outputDir.get().asFile + outputDir.deleteRecursively() + outputDir.mkdirs() + execOperations.exec { + workingDir = manifest.parentFile + // So cargo-ndk can find an NDK inside the SDK. + environment("ANDROID_HOME", sdkDir.get()) + commandLine = buildList { + add("cargo") + add("ndk") + add("--platform") + add(apiLevel.get().toString()) + add("--output-dir") + add(outputDir.absolutePath) + for (abi in abis.get()) { + add("--target") + add(abi) + } + add("build") + add("--example") + add("hello_world") + if (release.get()) { + add("--release") + } + } + } + } +} + +androidComponents { + onVariants { variant -> + val cargoBuild = + tasks.register( + "cargoBuild${variant.name.replaceFirstChar { it.uppercase() }}" + ) { + abis = rustAbis + apiLevel = variant.minSdk.apiLevel + release = variant.buildType == "release" + sdkDir = sdkComponents.sdkDirectory.map { it.asFile.absolutePath } + cargoManifest = layout.projectDirectory.file("../../../Cargo.toml") + outputDir = layout.buildDirectory.dir("rustJniLibs/${variant.name}") + } + variant.sources.jniLibs?.addGeneratedSourceDirectory(cargoBuild, CargoNdkBuild::outputDir) + } +} diff --git a/adapters/android/examples/hello_world_app/app/src/main/AndroidManifest.xml b/adapters/android/examples/hello_world_app/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5ac7e155 --- /dev/null +++ b/adapters/android/examples/hello_world_app/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/adapters/android/examples/hello_world_app/app/src/main/java/dev/accesskit/helloworld/HelloWorldView.java b/adapters/android/examples/hello_world_app/app/src/main/java/dev/accesskit/helloworld/HelloWorldView.java new file mode 100644 index 000000000..2dab0f303 --- /dev/null +++ b/adapters/android/examples/hello_world_app/app/src/main/java/dev/accesskit/helloworld/HelloWorldView.java @@ -0,0 +1,156 @@ +package dev.accesskit.helloworld; + +import android.content.Context; +import android.graphics.Insets; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowInsets; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +public final class HelloWorldView extends View { + static { + System.loadLibrary("hello_world"); + } + + private static final int DARK_GRAY = 0xff181818; + + private long nativeHandle; + + private final Runnable flushAnnouncement = + new Runnable() { + @Override + public void run() { + if (nativeHandle != 0) { + nativeFlushAnnouncement(nativeHandle, HelloWorldView.this); + } + } + }; + + private final AccessibilityNodeProvider nodeProvider = + new AccessibilityNodeProvider() { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + if (nativeHandle == 0) { + return null; + } + return nativeCreateAccessibilityNodeInfo( + nativeHandle, HelloWorldView.this, virtualViewId); + } + + @Override + public AccessibilityNodeInfo findFocus(int focusType) { + if (nativeHandle == 0) { + return null; + } + return nativeFindFocus(nativeHandle, HelloWorldView.this, focusType); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + if (nativeHandle == 0) { + return false; + } + return nativePerformAction( + nativeHandle, HelloWorldView.this, virtualViewId, action, arguments); + } + }; + + private static native long nativeCreate(); + + private static native void nativeDestroy(long handle); + + private static native void nativeSetViewport( + long handle, View host, float scaleFactor, float safeAreaInsetX, float safeAreaInsetY); + + private static native AccessibilityNodeInfo nativeCreateAccessibilityNodeInfo( + long handle, View host, int virtualViewId); + + private static native AccessibilityNodeInfo nativeFindFocus( + long handle, View host, int focusType); + + private static native boolean nativePerformAction( + long handle, View host, int virtualViewId, int action, Bundle arguments); + + private static native boolean nativeOnHoverEvent( + long handle, View host, int action, float x, float y); + + private static native boolean nativeOnKeyEvent( + long handle, View host, int action, int keyCode, boolean shiftPressed); + + private static native void nativeFlushAnnouncement(long handle, View host); + + public HelloWorldView(Context context) { + super(context); + setBackgroundColor(DARK_GRAY); + setFocusable(true); + setFocusableInTouchMode(true); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + nativeHandle = nativeCreate(); + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(flushAnnouncement); + nativeDestroy(nativeHandle); + nativeHandle = 0; + super.onDetachedFromWindow(); + } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + return nodeProvider; + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + Insets safeArea = + insets.getInsets( + WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); + float scaleFactor = getResources().getDisplayMetrics().density; + if (nativeHandle != 0) { + nativeSetViewport(nativeHandle, this, scaleFactor, safeArea.left, safeArea.top); + } + return super.onApplyWindowInsets(insets); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + if (nativeHandle != 0 + && nativeOnHoverEvent( + nativeHandle, this, event.getAction(), event.getX(), event.getY())) { + return true; + } + return super.onHoverEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return handleKeyEvent(event) || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return handleKeyEvent(event) || super.onKeyUp(keyCode, event); + } + + private boolean handleKeyEvent(KeyEvent event) { + if (nativeHandle == 0) { + return false; + } + return nativeOnKeyEvent( + nativeHandle, this, event.getAction(), event.getKeyCode(), event.isShiftPressed()); + } + + /** Called from Rust. */ + private void scheduleAnnouncement(long delayMillis) { + removeCallbacks(flushAnnouncement); + postDelayed(flushAnnouncement, delayMillis); + } +} diff --git a/adapters/android/examples/hello_world_app/app/src/main/java/dev/accesskit/helloworld/MainActivity.java b/adapters/android/examples/hello_world_app/app/src/main/java/dev/accesskit/helloworld/MainActivity.java new file mode 100644 index 000000000..1ca859f2e --- /dev/null +++ b/adapters/android/examples/hello_world_app/app/src/main/java/dev/accesskit/helloworld/MainActivity.java @@ -0,0 +1,14 @@ +package dev.accesskit.helloworld; + +import android.app.Activity; +import android.os.Bundle; + +public final class MainActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + HelloWorldView view = new HelloWorldView(this); + setContentView(view); + view.requestFocus(); + } +} diff --git a/adapters/android/examples/hello_world_app/build.gradle.kts b/adapters/android/examples/hello_world_app/build.gradle.kts new file mode 100644 index 000000000..ce201a578 --- /dev/null +++ b/adapters/android/examples/hello_world_app/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + alias(libs.plugins.android.application) apply false +} diff --git a/adapters/android/examples/hello_world_app/gradle.properties b/adapters/android/examples/hello_world_app/gradle.properties new file mode 100644 index 000000000..54600c0d6 --- /dev/null +++ b/adapters/android/examples/hello_world_app/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.configuration-cache=true diff --git a/adapters/android/examples/hello_world_app/gradle/libs.versions.toml b/adapters/android/examples/hello_world_app/gradle/libs.versions.toml new file mode 100644 index 000000000..a76d8ebfb --- /dev/null +++ b/adapters/android/examples/hello_world_app/gradle/libs.versions.toml @@ -0,0 +1,5 @@ +[versions] +agp = "9.4.0" + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/adapters/android/examples/hello_world_app/gradle/wrapper/gradle-wrapper.jar b/adapters/android/examples/hello_world_app/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..eddabd2ee Binary files /dev/null and b/adapters/android/examples/hello_world_app/gradle/wrapper/gradle-wrapper.jar differ diff --git a/adapters/android/examples/hello_world_app/gradle/wrapper/gradle-wrapper.properties b/adapters/android/examples/hello_world_app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ad7845be3 --- /dev/null +++ b/adapters/android/examples/hello_world_app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/adapters/android/examples/hello_world_app/gradlew b/adapters/android/examples/hello_world_app/gradlew new file mode 100755 index 000000000..249efbb03 --- /dev/null +++ b/adapters/android/examples/hello_world_app/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/adapters/android/examples/hello_world_app/gradlew.bat b/adapters/android/examples/hello_world_app/gradlew.bat new file mode 100644 index 000000000..a51ec4f58 --- /dev/null +++ b/adapters/android/examples/hello_world_app/gradlew.bat @@ -0,0 +1,82 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +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% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/adapters/android/examples/hello_world_app/settings.gradle.kts b/adapters/android/examples/hello_world_app/settings.gradle.kts new file mode 100644 index 000000000..936a5b446 --- /dev/null +++ b/adapters/android/examples/hello_world_app/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "AccessKit Hello World" +include(":app")