From 690d8d6878d7d8c03f18ca7512618154785fc042 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 31 Jul 2024 16:43:52 -0500 Subject: [PATCH 01/46] First phase of new Android adapter design --- Cargo.lock | 10 +++ Cargo.toml | 1 + platforms/android/Cargo.toml | 18 ++++++ platforms/android/README.md | 3 + platforms/android/src/adapter.rs | 61 ++++++++++++++++++ platforms/android/src/classes.rs | 85 ++++++++++++++++++++++++ platforms/android/src/filters.rs | 6 ++ platforms/android/src/lib.rs | 12 ++++ platforms/android/src/node.rs | 107 +++++++++++++++++++++++++++++++ platforms/android/src/util.rs | 54 ++++++++++++++++ release-please-config.json | 1 + 11 files changed, 358 insertions(+) create mode 100644 platforms/android/Cargo.toml create mode 100644 platforms/android/README.md create mode 100644 platforms/android/src/adapter.rs create mode 100644 platforms/android/src/classes.rs create mode 100644 platforms/android/src/filters.rs create mode 100644 platforms/android/src/lib.rs create mode 100644 platforms/android/src/node.rs create mode 100644 platforms/android/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 16aa3d38..d12d328b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,16 @@ dependencies = [ "serde", ] +[[package]] +name = "accesskit_android" +version = "0.1.0" +dependencies = [ + "accesskit", + "accesskit_consumer", + "jni", + "paste", +] + [[package]] name = "accesskit_atspi_common" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index dd1bae5e..3c4613ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "common", "consumer", + "platforms/android", "platforms/atspi-common", "platforms/macos", "platforms/unix", diff --git a/platforms/android/Cargo.toml b/platforms/android/Cargo.toml new file mode 100644 index 00000000..b82aec16 --- /dev/null +++ b/platforms/android/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "accesskit_android" +version = "0.1.0" +authors.workspace = true +license.workspace = true +description = "AccessKit UI accessibility infrastructure: Android adapter" +categories.workspace = true +keywords = ["gui", "ui", "accessibility"] +repository.workspace = true +readme = "README.md" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +accesskit = { version = "0.16.0", path = "../../common" } +accesskit_consumer = { version = "0.24.0", path = "../../consumer" } +jni = "0.21.1" +paste = "1.0.12" diff --git a/platforms/android/README.md b/platforms/android/README.md new file mode 100644 index 00000000..9628ede2 --- /dev/null +++ b/platforms/android/README.md @@ -0,0 +1,3 @@ +# AccessKit Android adapter + +This is the Android adapter for [AccessKit](https://accesskit.dev/). diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs new file mode 100644 index 00000000..e7f8d177 --- /dev/null +++ b/platforms/android/src/adapter.rs @@ -0,0 +1,61 @@ +// Copyright 2023 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::TreeUpdate; +use accesskit_consumer::Tree; +use jni::{errors::Result, objects::JObject, sys::jint, JNIEnv}; + +use crate::{classes::AccessibilityNodeInfo, node::NodeWrapper, util::NodeIdMap}; + +const HOST_VIEW_ID: jint = -1; + +pub struct Adapter { + node_id_map: NodeIdMap, + node_info_class: AccessibilityNodeInfo, + tree: Tree, +} + +impl Adapter { + pub fn new(env: &mut JNIEnv, initial_state: TreeUpdate) -> Result { + let node_info_class = AccessibilityNodeInfo::initialize_class(env)?; + let tree = Tree::new(initial_state, true); + Ok(Self { + node_id_map: NodeIdMap::default(), + node_info_class, + tree, + }) + } + + pub fn populate_node_info( + &mut self, + env: &mut JNIEnv, + host: &JObject, + virtual_view_id: jint, + jni_node: &JObject, + ) -> Result { + let tree_state = self.tree.state(); + let node = if virtual_view_id == HOST_VIEW_ID { + tree_state.root() + } else { + let Some(accesskit_id) = self.node_id_map.get_accesskit_id(virtual_view_id) else { + return Ok(false); + }; + let Some(node) = tree_state.node_by_id(accesskit_id) else { + return Ok(false); + }; + node + }; + + let wrapper = NodeWrapper(&node); + wrapper.populate_node_info( + env, + host, + &self.node_info_class, + &mut self.node_id_map, + jni_node, + )?; + Ok(true) + } +} diff --git a/platforms/android/src/classes.rs b/platforms/android/src/classes.rs new file mode 100644 index 00000000..cc3c3b64 --- /dev/null +++ b/platforms/android/src/classes.rs @@ -0,0 +1,85 @@ +// Copyright 2023 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 jni::{ + errors::Result, + objects::{GlobalRef, JMethodID, JObject, JValueOwned}, + signature::{Primitive, ReturnType}, + sys::jvalue, + JNIEnv, +}; + +macro_rules! java_class { + ( + package $package_name:literal; + class $class_name:ident { + $(method $method_return_type:literal $method_name:ident($($method_arg_type:literal $method_arg_name:ident,)*);)*} + ) => { + paste::paste! { + #[derive(Clone)] + #[allow(dead_code)] + #[allow(non_snake_case)] + pub(crate) struct $class_name { + _class: GlobalRef, + $([<$method_name _id>]: JMethodID,)* + } + #[allow(non_snake_case)] + impl $class_name { + pub(crate) fn initialize_class(env: &mut JNIEnv) -> Result { + let class = env.find_class(concat!($package_name, "/", stringify!($class_name)))?; + Ok(Self { + _class: env.new_global_ref(&class)?, + $([<$method_name _id>]: env.get_method_id( + &class, + stringify!($method_name), + concat!("(", $($method_arg_type,)* ")", $method_return_type), + )?,)* + }) + } + $(#[inline] + pub(crate) fn $method_name<'local, 'other_local, O>( + &self, + env: &mut JNIEnv<'local>, + instance: O, + $($method_arg_name: jvalue,)* + ) -> Result> + where O: AsRef> + { + unsafe { + env.call_method_unchecked( + instance, + self.[<$method_name _id>], + return_type!($method_return_type), + &[$($method_arg_name),*] + ) + } + })* + } + } + } +} + +macro_rules! return_type { + ("V") => { + ReturnType::Primitive(Primitive::Void) + }; +} + +java_class! { + package "android/view/accessibility"; + + class AccessibilityNodeInfo { + method "V" addChild("Landroid/view/View;" view, "I" virtual_descendant_id,); + method "V" setCheckable("Z" checkable,); + method "V" setChecked("Z" checked,); + method "V" setEnabled("Z" enabled,); + method "V" setFocusable("Z" focusable,); + method "V" setFocused("Z" focused,); + method "V" setParent("Landroid/view/View;" view, "I" virtual_descendant_id,); + method "V" setPassword("Z" password,); + method "V" setSelected("Z" selected,); + method "V" setText("Ljava/lang/CharSequence;" text,); + } +} diff --git a/platforms/android/src/filters.rs b/platforms/android/src/filters.rs new file mode 100644 index 00000000..d4ba0505 --- /dev/null +++ b/platforms/android/src/filters.rs @@ -0,0 +1,6 @@ +// Copyright 2024 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. + +pub(crate) use accesskit_consumer::common_filter as filter; diff --git a/platforms/android/src/lib.rs b/platforms/android/src/lib.rs new file mode 100644 index 00000000..ff9770a4 --- /dev/null +++ b/platforms/android/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2023 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. + +mod classes; +mod filters; +mod node; +mod util; + +mod adapter; +pub use adapter::Adapter; diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs new file mode 100644 index 00000000..e7d31c02 --- /dev/null +++ b/platforms/android/src/node.rs @@ -0,0 +1,107 @@ +// Copyright 2023 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::{Role, Toggled}; +use accesskit_consumer::Node; +use jni::{ + errors::Result, + objects::{JObject, JValue}, + JNIEnv, +}; + +use crate::{classes::AccessibilityNodeInfo, filters::filter, util::*}; + +pub(crate) struct NodeWrapper<'a>(pub(crate) &'a Node<'a>); + +impl<'a> NodeWrapper<'a> { + fn name(&self) -> Option { + self.0.name() + } + + fn is_enabled(&self) -> bool { + !self.0.is_disabled() + } + + fn is_focusable(&self) -> bool { + self.0.is_focusable() + } + + fn is_focused(&self) -> bool { + self.0.is_focused() + } + + fn is_checkable(&self) -> bool { + self.0.toggled().is_some() + } + + fn is_checked(&self) -> bool { + match self.0.toggled().unwrap() { + Toggled::False => false, + Toggled::True => true, + Toggled::Mixed => true, + } + } + + fn is_selected(&self) -> bool { + match self.0.role() { + // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table + // SelectionItem.IsSelected is set according to the True or False + // value of aria-checked for 'radio' and 'menuitemradio' roles. + Role::RadioButton | Role::MenuItemRadio => self.0.toggled() == Some(Toggled::True), + // https://www.w3.org/TR/wai-aria-1.1/#aria-selected + // SelectionItem.IsSelected is set according to the True or False + // value of aria-selected. + _ => self.0.is_selected().unwrap_or(false), + } + } + + pub(crate) fn populate_node_info( + &self, + env: &mut JNIEnv, + host: &JObject, + node_info_class: &AccessibilityNodeInfo, + id_map: &mut NodeIdMap, + jni_node: &JObject, + ) -> Result<()> { + for child in self.0.filtered_children(&filter) { + node_info_class.addChild( + env, + jni_node, + object_value(host), + id_value(id_map, child.id()), + )?; + } + if let Some(parent) = self.0.filtered_parent(&filter) { + if !parent.is_root() { + node_info_class.setParent( + env, + jni_node, + object_value(host), + id_value(id_map, parent.id()), + )?; + } + } + + if self.is_checkable() { + node_info_class.setCheckable(env, jni_node, bool_value(true))?; + node_info_class.setChecked(env, jni_node, bool_value(self.is_checked()))?; + } + node_info_class.setEnabled(env, jni_node, bool_value(self.is_enabled()))?; + node_info_class.setFocusable(env, jni_node, bool_value(self.is_focusable()))?; + node_info_class.setFocused(env, jni_node, bool_value(self.is_focused()))?; + node_info_class.setPassword( + env, + jni_node, + bool_value(self.0.role() == Role::PasswordInput), + )?; + node_info_class.setSelected(env, jni_node, bool_value(self.is_selected()))?; + if let Some(name) = self.name() { + let name = env.new_string(name)?; + node_info_class.setText(env, jni_node, JValue::Object(&name).as_jni())?; + } + + Ok(()) + } +} diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs new file mode 100644 index 00000000..0d9d63ec --- /dev/null +++ b/platforms/android/src/util.rs @@ -0,0 +1,54 @@ +// Copyright 2023 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::NodeId; +use jni::{ + objects::JObject, + sys::{jint, jvalue}, +}; +use std::collections::HashMap; + +#[derive(Default)] +pub(crate) struct NodeIdMap { + java_to_accesskit: HashMap, + accesskit_to_java: HashMap, + next_java_id: jint, +} + +impl NodeIdMap { + pub(crate) fn get_accesskit_id(&self, java_id: jint) -> Option { + self.java_to_accesskit.get(&java_id).copied() + } + + pub(crate) fn get_or_create_java_id(&mut self, accesskit_id: NodeId) -> jint { + if let Some(id) = self.accesskit_to_java.get(&accesskit_id) { + return *id; + } + let java_id = self.next_java_id; + self.next_java_id += 1; + self.accesskit_to_java.insert(accesskit_id, java_id); + self.java_to_accesskit.insert(java_id, accesskit_id); + java_id + } +} + +pub(crate) fn bool_value(value: bool) -> jvalue { + jvalue { z: value as u8 } +} + +pub(crate) fn id_value(id_map: &mut NodeIdMap, value: NodeId) -> jvalue { + jvalue { + i: id_map.get_or_create_java_id(value), + } +} + +pub(crate) fn object_value<'local, O>(value: O) -> jvalue +where + O: AsRef>, +{ + jvalue { + l: value.as_ref().as_raw(), + } +} diff --git a/release-please-config.json b/release-please-config.json index 2b5f5167..addf935f 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -6,6 +6,7 @@ "packages": { "common": {}, "consumer": {}, + "platforms/android": {}, "platforms/atspi-common": {}, "platforms/macos": {}, "platforms/unix": {}, From ee20c14daac4e42c4603e0593424abf04825d2cd Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Fri, 2 Aug 2024 08:19:03 -0500 Subject: [PATCH 02/46] Base native support for touch exploration --- platforms/android/src/adapter.rs | 27 ++++++++++++++++++++++----- platforms/android/src/node.rs | 4 ++-- platforms/android/src/util.rs | 13 ++++++++++--- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index e7f8d177..1d918f8b 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -3,13 +3,21 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::TreeUpdate; +use accesskit::{Point, TreeUpdate}; use accesskit_consumer::Tree; -use jni::{errors::Result, objects::JObject, sys::jint, JNIEnv}; +use jni::{ + errors::Result, + objects::JObject, + sys::{jfloat, jint}, + JNIEnv, +}; -use crate::{classes::AccessibilityNodeInfo, node::NodeWrapper, util::NodeIdMap}; - -const HOST_VIEW_ID: jint = -1; +use crate::{ + classes::AccessibilityNodeInfo, + filters::filter, + node::NodeWrapper, + util::{NodeIdMap, HOST_VIEW_ID}, +}; pub struct Adapter { node_id_map: NodeIdMap, @@ -58,4 +66,13 @@ impl Adapter { )?; Ok(true) } + + pub fn virtual_view_at_point(&mut self, x: jfloat, y: jfloat) -> jint { + let tree_state = self.tree.state(); + let root = tree_state.root(); + let point = Point::new(x.into(), y.into()); + let point = root.transform().inverse() * point; + let node = root.node_at_point(point, &filter).unwrap_or(root); + self.node_id_map.get_or_create_java_id(&node) + } } diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index e7d31c02..4cb89bde 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -70,7 +70,7 @@ impl<'a> NodeWrapper<'a> { env, jni_node, object_value(host), - id_value(id_map, child.id()), + id_value(id_map, &child), )?; } if let Some(parent) = self.0.filtered_parent(&filter) { @@ -79,7 +79,7 @@ impl<'a> NodeWrapper<'a> { env, jni_node, object_value(host), - id_value(id_map, parent.id()), + id_value(id_map, &parent), )?; } } diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 0d9d63ec..f4b73f65 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -4,12 +4,15 @@ // the LICENSE-MIT file), at your option. use accesskit::NodeId; +use accesskit_consumer::Node; use jni::{ objects::JObject, sys::{jint, jvalue}, }; use std::collections::HashMap; +pub(crate) const HOST_VIEW_ID: jint = -1; + #[derive(Default)] pub(crate) struct NodeIdMap { java_to_accesskit: HashMap, @@ -22,7 +25,11 @@ impl NodeIdMap { self.java_to_accesskit.get(&java_id).copied() } - pub(crate) fn get_or_create_java_id(&mut self, accesskit_id: NodeId) -> jint { + pub(crate) fn get_or_create_java_id(&mut self, node: &Node) -> jint { + if node.is_root() { + return HOST_VIEW_ID; + } + let accesskit_id = node.id(); if let Some(id) = self.accesskit_to_java.get(&accesskit_id) { return *id; } @@ -38,9 +45,9 @@ pub(crate) fn bool_value(value: bool) -> jvalue { jvalue { z: value as u8 } } -pub(crate) fn id_value(id_map: &mut NodeIdMap, value: NodeId) -> jvalue { +pub(crate) fn id_value(id_map: &mut NodeIdMap, node: &Node) -> jvalue { jvalue { - i: id_map.get_or_create_java_id(value), + i: id_map.get_or_create_java_id(node), } } From 4687971d8d5f593b8e5e37558f59559c6384fada Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 5 Aug 2024 04:07:34 -0500 Subject: [PATCH 03/46] Stop using unsafe method calls; work so far on injecting adapter --- Cargo.lock | 2 + platforms/android/.gitignore | 1 + platforms/android/Cargo.toml | 5 + platforms/android/build-dex.sh | 6 + platforms/android/classes.dex | Bin 0 -> 2200 bytes .../java/dev/accesskit/android/Delegate.java | 42 +++++ platforms/android/src/adapter.rs | 18 +-- platforms/android/src/classes.rs | 85 ----------- platforms/android/src/inject.rs | 143 ++++++++++++++++++ platforms/android/src/lib.rs | 4 +- platforms/android/src/node.rs | 74 +++++---- platforms/android/src/util.rs | 24 +-- 12 files changed, 255 insertions(+), 149 deletions(-) create mode 100644 platforms/android/.gitignore create mode 100755 platforms/android/build-dex.sh create mode 100644 platforms/android/classes.dex create mode 100644 platforms/android/java/dev/accesskit/android/Delegate.java delete mode 100644 platforms/android/src/classes.rs create mode 100644 platforms/android/src/inject.rs diff --git a/Cargo.lock b/Cargo.lock index d12d328b..0630e143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,8 @@ dependencies = [ "accesskit", "accesskit_consumer", "jni", + "log", + "once_cell", "paste", ] diff --git a/platforms/android/.gitignore b/platforms/android/.gitignore new file mode 100644 index 00000000..6b468b62 --- /dev/null +++ b/platforms/android/.gitignore @@ -0,0 +1 @@ +*.class diff --git a/platforms/android/Cargo.toml b/platforms/android/Cargo.toml index b82aec16..d5b5d994 100644 --- a/platforms/android/Cargo.toml +++ b/platforms/android/Cargo.toml @@ -11,8 +11,13 @@ readme = "README.md" edition.workspace = true rust-version.workspace = true +[features] +embedded-dex = [] + [dependencies] accesskit = { version = "0.16.0", path = "../../common" } accesskit_consumer = { version = "0.24.0", path = "../../consumer" } jni = "0.21.1" +log = "0.4.17" +once_cell = "1.17.1" paste = "1.0.12" diff --git a/platforms/android/build-dex.sh b/platforms/android/build-dex.sh new file mode 100755 index 00000000..62eec1d6 --- /dev/null +++ b/platforms/android/build-dex.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e +ANDROID_JAR=$ANDROID_HOME/platforms/android-30/android.jar +find java -name '*.class' -delete +javac --source 8 --target 8 --boot-class-path $ANDROID_JAR `find java -name '*.java'` +$ANDROID_HOME/build-tools/33.0.2/d8 --classpath $ANDROID_JAR --output . `find java -name '*.class'` diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex new file mode 100644 index 0000000000000000000000000000000000000000..539ecb48f6283d1fb2168cf71cda54d50d5931df GIT binary patch literal 2200 zcma)8L2MgU5S_nXukFNf-K5Yov|+mmrGX?)6DozcphP4U4Ms>LwL}VsZ~MpDWW8Rz zYm*95A}$qDB_hG0oEixZJy3}Q2M!#NI3saDLh1n(Dgg%yhXO)^18>&rD1{*S(eupC z%>VOd{_g*&sLig4snfLj;@#H|tbg;u2iF$A`{}nQF0Xz2>+J>oTKZ~|$Rnc7;$yuK z;rKE^bOIXN4**F)l!Bax)FD5F%s_qt8H1eFh<=X~?LeMIz%#%SPy!UN2D}Yi2R;Yx z0BOWe0LwrNco(< z2lM8h@mf51K9AMNucN!{yNM0r*b73?3WB_BdpDRp($X#Xxz6QTL2j03r0H?&l7(4I zbJ&ZRH=CmDR zI@r5dotbl=+I*6`F$vURAZsV44|FqlKQnJ$UO7T0PaC3)ZLd=X`k6863IoEh?a==g z*5%Ie`_7UF*W7+v+BUYctpk2eQ|#nOk&H98YX?tK&%AO}RR(H&UA82NMG^%XDM+`{ zsN0oX%T}-Ciuf6xE0>k;+a=qvgNvO>dFuOTXr6`&73s9>jhuAddLRS4?&cQUvQzhM zxB9FK*6Ni!4ek?k&UIDeDM$K#o{sI~U$U#N44MsR|6`pz9WQWea*lMXxpNM8%UMEEYGJdiJgzNIy?a8QE3KH^%Y;dHWlfRpVI~<3RjaoY#TreQ`u%@P zbFd|y;d?nH*~Hl?4y~!Ee(BQu^s81$mN%4Jv1Y7_+O$Tka=qr+PWWn!)#?=$I!etd zT%bEvUQ^`_zghFGnN{idYSgOP?wIs!Yv$Cb)lv-~eaD=s@u~62i7~tikIK65owi;h zEiQ(Sio>ME#PC3urfWFSj}3-T-K60}+BU_usc&uRJMp;q6lanU+O}!HaK%W7>o{!j zP4_0>*wSwqVrxhL(-5h55+e0+g1?`KqO*>_sWIqN0PFckzY_U7-z1+ETd-aIo1M=r zwueUy^iDj>69C`Wyl&^t=Cfz-z0K?L-Ocx4=l Result { - let node_info_class = AccessibilityNodeInfo::initialize_class(env)?; + pub fn new(initial_state: TreeUpdate) -> Self { let tree = Tree::new(initial_state, true); - Ok(Self { + Self { node_id_map: NodeIdMap::default(), - node_info_class, tree, - }) + } } pub fn populate_node_info( @@ -57,13 +53,7 @@ impl Adapter { }; let wrapper = NodeWrapper(&node); - wrapper.populate_node_info( - env, - host, - &self.node_info_class, - &mut self.node_id_map, - jni_node, - )?; + wrapper.populate_node_info(env, host, &mut self.node_id_map, jni_node)?; Ok(true) } diff --git a/platforms/android/src/classes.rs b/platforms/android/src/classes.rs deleted file mode 100644 index cc3c3b64..00000000 --- a/platforms/android/src/classes.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2023 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 jni::{ - errors::Result, - objects::{GlobalRef, JMethodID, JObject, JValueOwned}, - signature::{Primitive, ReturnType}, - sys::jvalue, - JNIEnv, -}; - -macro_rules! java_class { - ( - package $package_name:literal; - class $class_name:ident { - $(method $method_return_type:literal $method_name:ident($($method_arg_type:literal $method_arg_name:ident,)*);)*} - ) => { - paste::paste! { - #[derive(Clone)] - #[allow(dead_code)] - #[allow(non_snake_case)] - pub(crate) struct $class_name { - _class: GlobalRef, - $([<$method_name _id>]: JMethodID,)* - } - #[allow(non_snake_case)] - impl $class_name { - pub(crate) fn initialize_class(env: &mut JNIEnv) -> Result { - let class = env.find_class(concat!($package_name, "/", stringify!($class_name)))?; - Ok(Self { - _class: env.new_global_ref(&class)?, - $([<$method_name _id>]: env.get_method_id( - &class, - stringify!($method_name), - concat!("(", $($method_arg_type,)* ")", $method_return_type), - )?,)* - }) - } - $(#[inline] - pub(crate) fn $method_name<'local, 'other_local, O>( - &self, - env: &mut JNIEnv<'local>, - instance: O, - $($method_arg_name: jvalue,)* - ) -> Result> - where O: AsRef> - { - unsafe { - env.call_method_unchecked( - instance, - self.[<$method_name _id>], - return_type!($method_return_type), - &[$($method_arg_name),*] - ) - } - })* - } - } - } -} - -macro_rules! return_type { - ("V") => { - ReturnType::Primitive(Primitive::Void) - }; -} - -java_class! { - package "android/view/accessibility"; - - class AccessibilityNodeInfo { - method "V" addChild("Landroid/view/View;" view, "I" virtual_descendant_id,); - method "V" setCheckable("Z" checkable,); - method "V" setChecked("Z" checked,); - method "V" setEnabled("Z" enabled,); - method "V" setFocusable("Z" focusable,); - method "V" setFocused("Z" focused,); - method "V" setParent("Landroid/view/View;" view, "I" virtual_descendant_id,); - method "V" setPassword("Z" password,); - method "V" setSelected("Z" selected,); - method "V" setText("Ljava/lang/CharSequence;" text,); - } -} diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs new file mode 100644 index 00000000..b6791913 --- /dev/null +++ b/platforms/android/src/inject.rs @@ -0,0 +1,143 @@ +// Copyright 2024 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. + +// Derived from jni-rs +// Copyright 2016 Prevoty, Inc. and jni-rs contributors +// 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::{ActionHandler, TreeUpdate}; +use jni::{ + errors::Result, + objects::{GlobalRef, JClass, JObject, WeakRef}, + sys::jlong, + JNIEnv, JavaVM, +}; +use log::debug; +use once_cell::sync::OnceCell; +use std::{ + collections::BTreeMap, + sync::{ + atomic::{AtomicI64, Ordering}, + Arc, Mutex, Weak, + }, +}; + +use crate::adapter::Adapter; + +struct InnerInjectingAdapter { + adapter: Adapter, + action_handler: Box, +} + +static NEXT_HANDLE: AtomicI64 = AtomicI64::new(0); +static HANDLE_MAP: Mutex>>> = + Mutex::new(BTreeMap::new()); + +fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { + static CLASS: OnceCell = OnceCell::new(); + let global = CLASS.get_or_try_init(|| { + #[cfg(feature = "embedded-dex")] + let class = { + let dex_class_loader_class = env.find_class("dalvik/system/InMemoryDexClassLoader")?; + let dex_bytes = include_bytes!("../classes.dex"); + let dex_buffer = unsafe { + env.new_direct_byte_buffer(dex_bytes.as_ptr() as *mut u8, dex_bytes.len()) + }?; + let dex_class_loader = env.new_object( + &dex_class_loader_class, + "(Ljava/nio/ByteBUffer;Ljava/lang/ClassLoader;)V", + &[(&dex_buffer).into(), (&JObject::null()).into()], + )?; + let class_name = env.new_string("dev.accesskit.android.Delegate")?; + let class_obj = env + .call_method( + &dex_class_loader, + "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;", + &[(&class_name).into()], + )? + .l()?; + JClass::from(class_obj) + }; + #[cfg(not(feature = "embedded-dex"))] + let class = env.find_class("dev/accesskit/android/Delegate")?; + // TODO: register JNI methods + env.new_global_ref(class) + })?; + Ok(global.as_obj().into()) +} + +pub struct InjectingAdapter { + vm: JavaVM, + host: WeakRef, + handle: jlong, + inner: Arc>, +} + +impl InjectingAdapter { + pub fn new( + env: &mut JNIEnv, + host_view: &JObject, + initial_state: TreeUpdate, + action_handler: impl 'static + ActionHandler + Send, + ) -> Result { + let inner = Arc::new(Mutex::new(InnerInjectingAdapter { + adapter: Adapter::new(initial_state), + action_handler: Box::new(action_handler), + })); + let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed); + HANDLE_MAP + .lock() + .unwrap() + .insert(handle, Arc::downgrade(&inner)); + let delegate_class = delegate_class(env)?; + env.call_static_method( + delegate_class, + "inject", + "(Landroid/view/View;J)V", + &[host_view.into(), handle.into()], + )?; + Ok(Self { + vm: env.get_java_vm()?, + host: env.new_weak_ref(host_view)?.unwrap(), + handle, + inner, + }) + } +} + +impl Drop for InjectingAdapter { + fn drop(&mut self) { + fn drop_impl(env: &mut JNIEnv, host: &WeakRef) -> Result<()> { + let Some(host) = host.upgrade_local(env)? else { + return Ok(()); + }; + let delegate_class = delegate_class(env)?; + env.call_static_method( + delegate_class, + "remove", + "(Landroid/view/View;)V", + &[(&host).into()], + )?; + Ok(()) + } + + let res = match self.vm.get_env() { + Ok(mut env) => drop_impl(&mut env, &self.host), + Err(_) => self + .vm + .attach_current_thread() + .and_then(|mut env| drop_impl(&mut env, &self.host)), + }; + + if let Err(err) = res { + debug!("error dropping InjectingAdapter: {:#?}", err); + } + + HANDLE_MAP.lock().unwrap().remove(&self.handle); + } +} diff --git a/platforms/android/src/lib.rs b/platforms/android/src/lib.rs index ff9770a4..3c36564a 100644 --- a/platforms/android/src/lib.rs +++ b/platforms/android/src/lib.rs @@ -3,10 +3,12 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -mod classes; mod filters; mod node; mod util; mod adapter; pub use adapter::Adapter; + +mod inject; +pub use inject::InjectingAdapter; diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index 4cb89bde..6b224f91 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -5,13 +5,9 @@ use accesskit::{Role, Toggled}; use accesskit_consumer::Node; -use jni::{ - errors::Result, - objects::{JObject, JValue}, - JNIEnv, -}; +use jni::{errors::Result, objects::JObject, JNIEnv}; -use crate::{classes::AccessibilityNodeInfo, filters::filter, util::*}; +use crate::{filters::filter, util::*}; pub(crate) struct NodeWrapper<'a>(pub(crate) &'a Node<'a>); @@ -32,6 +28,10 @@ impl<'a> NodeWrapper<'a> { self.0.is_focused() } + fn is_password(&self) -> bool { + self.0.role() == Role::PasswordInput + } + fn is_checkable(&self) -> bool { self.0.toggled().is_some() } @@ -61,45 +61,67 @@ impl<'a> NodeWrapper<'a> { &self, env: &mut JNIEnv, host: &JObject, - node_info_class: &AccessibilityNodeInfo, id_map: &mut NodeIdMap, jni_node: &JObject, ) -> Result<()> { for child in self.0.filtered_children(&filter) { - node_info_class.addChild( - env, + env.call_method( jni_node, - object_value(host), - id_value(id_map, &child), + "addChild", + "(Landroid/view/View;I)V", + &[host.into(), id_map.get_or_create_java_id(&child).into()], )?; } if let Some(parent) = self.0.filtered_parent(&filter) { - if !parent.is_root() { - node_info_class.setParent( - env, + if parent.is_root() { + env.call_method( jni_node, - object_value(host), - id_value(id_map, &parent), + "setParent", + "(Landroid/view/View;)V", + &[host.into()], + )?; + } else { + env.call_method( + jni_node, + "setParent", + "(Landroid/view/View;I)V", + &[host.into(), id_map.get_or_create_java_id(&parent).into()], )?; } } if self.is_checkable() { - node_info_class.setCheckable(env, jni_node, bool_value(true))?; - node_info_class.setChecked(env, jni_node, bool_value(self.is_checked()))?; + env.call_method(jni_node, "setCheckable", "(Z)V", &[true.into()])?; + env.call_method(jni_node, "setChecked", "(Z)V", &[self.is_checked().into()])?; } - node_info_class.setEnabled(env, jni_node, bool_value(self.is_enabled()))?; - node_info_class.setFocusable(env, jni_node, bool_value(self.is_focusable()))?; - node_info_class.setFocused(env, jni_node, bool_value(self.is_focused()))?; - node_info_class.setPassword( - env, + env.call_method(jni_node, "setEnabled", "(Z)V", &[self.is_enabled().into()])?; + env.call_method( + jni_node, + "setFocusable", + "(Z)V", + &[self.is_focusable().into()], + )?; + env.call_method(jni_node, "setFocused", "(Z)V", &[self.is_focused().into()])?; + env.call_method( + jni_node, + "setPassword", + "(Z)V", + &[self.is_password().into()], + )?; + env.call_method( jni_node, - bool_value(self.0.role() == Role::PasswordInput), + "setSelected", + "(Z)V", + &[self.is_selected().into()], )?; - node_info_class.setSelected(env, jni_node, bool_value(self.is_selected()))?; if let Some(name) = self.name() { let name = env.new_string(name)?; - node_info_class.setText(env, jni_node, JValue::Object(&name).as_jni())?; + env.call_method( + jni_node, + "setText", + "(Ljava/lang/String;)V", + &[(&name).into()], + )?; } Ok(()) diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index f4b73f65..b0f6e9ad 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -5,10 +5,7 @@ use accesskit::NodeId; use accesskit_consumer::Node; -use jni::{ - objects::JObject, - sys::{jint, jvalue}, -}; +use jni::sys::jint; use std::collections::HashMap; pub(crate) const HOST_VIEW_ID: jint = -1; @@ -40,22 +37,3 @@ impl NodeIdMap { java_id } } - -pub(crate) fn bool_value(value: bool) -> jvalue { - jvalue { z: value as u8 } -} - -pub(crate) fn id_value(id_map: &mut NodeIdMap, node: &Node) -> jvalue { - jvalue { - i: id_map.get_or_create_java_id(node), - } -} - -pub(crate) fn object_value<'local, O>(value: O) -> jvalue -where - O: AsRef>, -{ - jvalue { - l: value.as_ref().as_raw(), - } -} From abdb5b78b19d966eebbe37e4aaed07224f062c7d Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 5 Aug 2024 05:08:36 -0500 Subject: [PATCH 04/46] Implement proper lazy initialization, partial support for updates --- platforms/android/src/adapter.rs | 83 +++++++++++++++++++++++++++----- platforms/android/src/inject.rs | 23 +++++++-- 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 4635c0ff..d036f2e1 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -3,7 +3,9 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::{Point, TreeUpdate}; +use accesskit::{ + ActivationHandler, NodeBuilder, NodeId, Point, Role, Tree as TreeData, TreeUpdate, +}; use accesskit_consumer::Tree; use jni::{ errors::Result, @@ -18,28 +20,81 @@ use crate::{ util::{NodeIdMap, HOST_VIEW_ID}, }; +const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0); + +#[derive(Default)] +enum State { + #[default] + Inactive, + Placeholder(Tree), + Active(Tree), +} + +impl State { + fn get_or_init_tree( + &mut self, + activation_handler: &mut H, + ) -> &Tree { + match self { + Self::Inactive => { + *self = match activation_handler.request_initial_tree() { + Some(initial_state) => Self::Active(Tree::new(initial_state, true)), + None => { + let placeholder_update = TreeUpdate { + nodes: vec![( + PLACEHOLDER_ROOT_ID, + NodeBuilder::new(Role::Window).build(), + )], + tree: Some(TreeData::new(PLACEHOLDER_ROOT_ID)), + focus: PLACEHOLDER_ROOT_ID, + }; + Self::Placeholder(Tree::new(placeholder_update, true)) + } + }; + self.get_or_init_tree(activation_handler) + } + Self::Placeholder(tree) => tree, + Self::Active(tree) => tree, + } + } +} + +#[derive(Default)] pub struct Adapter { node_id_map: NodeIdMap, - tree: Tree, + state: State, } impl Adapter { - pub fn new(initial_state: TreeUpdate) -> Self { - let tree = Tree::new(initial_state, true); - Self { - node_id_map: NodeIdMap::default(), - tree, + /// If and only if the tree has been initialized, call the provided function + /// and apply the resulting update. Note: If the caller's implementation of + /// [`ActivationHandler::request_initial_tree`] initially returned `None`, + /// the [`TreeUpdate`] returned by the provided function must contain + /// a full tree. + /// + /// TODO: dispatch events + pub fn update_if_active(&mut self, update_factory: impl FnOnce() -> TreeUpdate) { + match &mut self.state { + State::Inactive => (), + State::Placeholder(_) => { + self.state = State::Active(Tree::new(update_factory(), true)); + } + State::Active(tree) => { + tree.update(update_factory()); + } } } - pub fn populate_node_info( + pub fn populate_node_info( &mut self, + activation_handler: &mut H, env: &mut JNIEnv, host: &JObject, virtual_view_id: jint, jni_node: &JObject, ) -> Result { - let tree_state = self.tree.state(); + let tree = self.state.get_or_init_tree(activation_handler); + let tree_state = tree.state(); let node = if virtual_view_id == HOST_VIEW_ID { tree_state.root() } else { @@ -57,8 +112,14 @@ impl Adapter { Ok(true) } - pub fn virtual_view_at_point(&mut self, x: jfloat, y: jfloat) -> jint { - let tree_state = self.tree.state(); + pub fn virtual_view_at_point( + &mut self, + activation_handler: &mut H, + x: jfloat, + y: jfloat, + ) -> jint { + let tree = self.state.get_or_init_tree(activation_handler); + let tree_state = tree.state(); let root = tree_state.root(); let point = Point::new(x.into(), y.into()); let point = root.transform().inverse() * point; diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index b6791913..cf11c297 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -9,7 +9,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::{ActionHandler, TreeUpdate}; +use accesskit::{ActionHandler, ActivationHandler, TreeUpdate}; use jni::{ errors::Result, objects::{GlobalRef, JClass, JObject, WeakRef}, @@ -30,6 +30,7 @@ use crate::adapter::Adapter; struct InnerInjectingAdapter { adapter: Adapter, + activation_handler: Box, action_handler: Box, } @@ -82,11 +83,12 @@ impl InjectingAdapter { pub fn new( env: &mut JNIEnv, host_view: &JObject, - initial_state: TreeUpdate, + activation_handler: impl 'static + ActivationHandler + Send, action_handler: impl 'static + ActionHandler + Send, ) -> Result { let inner = Arc::new(Mutex::new(InnerInjectingAdapter { - adapter: Adapter::new(initial_state), + adapter: Adapter::default(), + activation_handler: Box::new(activation_handler), action_handler: Box::new(action_handler), })); let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed); @@ -108,6 +110,21 @@ impl InjectingAdapter { inner, }) } + + /// If and only if the tree has been initialized, call the provided function + /// and apply the resulting update. Note: If the caller's implementation of + /// [`ActivationHandler::request_initial_tree`] initially returned `None`, + /// the [`TreeUpdate`] returned by the provided function must contain + /// a full tree. + /// + /// TODO: dispatch events + pub fn update_if_active(&mut self, update_factory: impl FnOnce() -> TreeUpdate) { + self.inner + .lock() + .unwrap() + .adapter + .update_if_active(update_factory); + } } impl Drop for InjectingAdapter { From 2a701f2fa168df490fbc9197dee348793d90863b Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 5 Aug 2024 08:34:30 -0500 Subject: [PATCH 05/46] Add the necessary boilerplate to build the winit examples for Android --- platforms/winit/Cargo.toml | 22 ++++++++++- platforms/winit/examples/mixed_handlers.rs | 45 ++++++++++++++++++++-- platforms/winit/examples/simple.rs | 45 ++++++++++++++++++++-- 3 files changed, 103 insertions(+), 9 deletions(-) diff --git a/platforms/winit/Cargo.toml b/platforms/winit/Cargo.toml index d3310fe0..bc57497b 100644 --- a/platforms/winit/Cargo.toml +++ b/platforms/winit/Cargo.toml @@ -36,4 +36,24 @@ accesskit_unix = { version = "0.13.1", path = "../unix", optional = true, defaul [dev-dependencies.winit] version = "0.30" default-features = false -features = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] +features = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita", "android-native-activity"] + +[[example]] +name = "simple" + +[[example]] +# A custom example target which uses the same `simple.rs` file but for android +name = "simple_android" +path = "examples/simple.rs" +# cdylib is required for cargo-apk +crate-type = ["cdylib"] + +[[example]] +name = "mixed_handlers" + +[[example]] +# A custom example target which uses the same `mixed_handlers.rs` file but for android +name = "mixed_handlers_android" +path = "examples/mixed_handlers.rs" +# cdylib is required for cargo-apk +crate-type = ["cdylib"] diff --git a/platforms/winit/examples/mixed_handlers.rs b/platforms/winit/examples/mixed_handlers.rs index ef5246f5..731be625 100644 --- a/platforms/winit/examples/mixed_handlers.rs +++ b/platforms/winit/examples/mixed_handlers.rs @@ -276,7 +276,17 @@ impl ApplicationHandler for Application { } } -fn main() -> Result<(), Box> { +fn run(event_loop: EventLoop) { + let mut app = Application::new(event_loop.create_proxy()); + event_loop.run_app(&mut app).unwrap(); +} + +#[cfg(not(target_os = "android"))] +#[allow(dead_code)] +// This is treated as dead code by the Android version of the example, but is actually live +// This hackery is required because Cargo doesn't care to support this use case, of one +// example which works across Android and desktop +fn main() { println!("This example has no visible GUI, and a keyboard interface:"); println!("- [Tab] switches focus between two logical buttons."); println!("- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed."); @@ -294,7 +304,34 @@ fn main() -> Result<(), Box> { ))] println!("Enable Orca with [Super]+[Alt]+[S]."); - let event_loop = EventLoop::with_user_event().build()?; - let mut state = Application::new(event_loop.create_proxy()); - event_loop.run_app(&mut state).map_err(Into::into) + let event_loop = EventLoop::with_user_event().build().unwrap(); + run(event_loop); +} + +// Boilerplate code for android: Identical across all applications + +#[cfg(target_os = "android")] +use winit::platform::android::activity::AndroidApp; + +#[cfg(target_os = "android")] +// Safety: We are following `android_activity`'s docs here +// We believe that there are no other declarations using this name in the compiled objects here +#[allow(unsafe_code)] +#[no_mangle] +fn android_main(app: AndroidApp) { + use winit::platform::android::EventLoopBuilderExtAndroid; + + let event_loop = EventLoop::with_user_event() + .with_android_app(app) + .build() + .unwrap(); + run(event_loop); +} + +// TODO: This is a hack because of how we handle our examples in Cargo.toml +// Ideally, we change Cargo to be more sensible here? +#[cfg(target_os = "android")] +#[allow(dead_code)] +fn main() { + unreachable!() } diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs index 39ccd28e..1d131832 100644 --- a/platforms/winit/examples/simple.rs +++ b/platforms/winit/examples/simple.rs @@ -252,7 +252,17 @@ impl ApplicationHandler for Application { } } -fn main() -> Result<(), Box> { +fn run(event_loop: EventLoop) { + let mut app = Application::new(event_loop.create_proxy()); + event_loop.run_app(&mut app).unwrap(); +} + +#[cfg(not(target_os = "android"))] +#[allow(dead_code)] +// This is treated as dead code by the Android version of the example, but is actually live +// This hackery is required because Cargo doesn't care to support this use case, of one +// example which works across Android and desktop +fn main() { println!("This example has no visible GUI, and a keyboard interface:"); println!("- [Tab] switches focus between two logical buttons."); println!("- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed."); @@ -270,7 +280,34 @@ fn main() -> Result<(), Box> { ))] println!("Enable Orca with [Super]+[Alt]+[S]."); - let event_loop = EventLoop::with_user_event().build()?; - let mut state = Application::new(event_loop.create_proxy()); - event_loop.run_app(&mut state).map_err(Into::into) + let event_loop = EventLoop::with_user_event().build().unwrap(); + run(event_loop); +} + +// Boilerplate code for android: Identical across all applications + +#[cfg(target_os = "android")] +use winit::platform::android::activity::AndroidApp; + +#[cfg(target_os = "android")] +// Safety: We are following `android_activity`'s docs here +// We believe that there are no other declarations using this name in the compiled objects here +#[allow(unsafe_code)] +#[no_mangle] +fn android_main(app: AndroidApp) { + use winit::platform::android::EventLoopBuilderExtAndroid; + + let event_loop = EventLoop::with_user_event() + .with_android_app(app) + .build() + .unwrap(); + run(event_loop); +} + +// TODO: This is a hack because of how we handle our examples in Cargo.toml +// Ideally, we change Cargo to be more sensible here? +#[cfg(target_os = "android")] +#[allow(dead_code)] +fn main() { + unreachable!() } From f4438b7379691bad08664476989614d825b8ffd0 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 7 Aug 2024 08:34:09 -0500 Subject: [PATCH 06/46] Partial adapter initialization in a winit example --- Cargo.lock | 1 + platforms/android/src/inject.rs | 2 +- platforms/android/src/lib.rs | 2 + platforms/winit/Cargo.toml | 3 ++ platforms/winit/examples/simple.rs | 1 + platforms/winit/src/platform_impl/android.rs | 50 ++++++++++++++++++++ platforms/winit/src/platform_impl/mod.rs | 7 ++- 7 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 platforms/winit/src/platform_impl/android.rs diff --git a/Cargo.lock b/Cargo.lock index 0630e143..1b059700 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,7 @@ name = "accesskit_winit" version = "0.23.1" dependencies = [ "accesskit", + "accesskit_android", "accesskit_macos", "accesskit_unix", "accesskit_windows", diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index cf11c297..d40ebfe6 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -50,7 +50,7 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { }?; let dex_class_loader = env.new_object( &dex_class_loader_class, - "(Ljava/nio/ByteBUffer;Ljava/lang/ClassLoader;)V", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V", &[(&dex_buffer).into(), (&JObject::null()).into()], )?; let class_name = env.new_string("dev.accesskit.android.Delegate")?; diff --git a/platforms/android/src/lib.rs b/platforms/android/src/lib.rs index 3c36564a..264d0d39 100644 --- a/platforms/android/src/lib.rs +++ b/platforms/android/src/lib.rs @@ -12,3 +12,5 @@ pub use adapter::Adapter; mod inject; pub use inject::InjectingAdapter; + +pub use jni; diff --git a/platforms/winit/Cargo.toml b/platforms/winit/Cargo.toml index bc57497b..6a7f40c5 100644 --- a/platforms/winit/Cargo.toml +++ b/platforms/winit/Cargo.toml @@ -33,6 +33,9 @@ accesskit_macos = { version = "0.18.1", path = "../macos" } [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] accesskit_unix = { version = "0.13.1", path = "../unix", optional = true, default-features = false } +[target.'cfg(target_os = "android")'.dependencies] +accesskit_android = { version = "0.1.0", path = "../android", features = ["embedded-dex"] } + [dev-dependencies.winit] version = "0.30" default-features = false diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs index 1d131832..877f008e 100644 --- a/platforms/winit/examples/simple.rs +++ b/platforms/winit/examples/simple.rs @@ -245,6 +245,7 @@ impl ApplicationHandler for Application { .expect("failed to create initial window"); } + #[cfg(not(target_os = "android"))] fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { if self.window.is_none() { event_loop.exit(); diff --git a/platforms/winit/src/platform_impl/android.rs b/platforms/winit/src/platform_impl/android.rs new file mode 100644 index 00000000..abc53f8a --- /dev/null +++ b/platforms/winit/src/platform_impl/android.rs @@ -0,0 +1,50 @@ +// Copyright 2024 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file). + +use accesskit::{ActionHandler, ActivationHandler, DeactivationHandler, TreeUpdate}; +use accesskit_android::{ + jni::{objects::JObject, JavaVM}, + InjectingAdapter, +}; +use winit::{ + event::WindowEvent, event_loop::ActiveEventLoop, platform::android::ActiveEventLoopExtAndroid, + window::Window, +}; + +pub struct Adapter { + adapter: InjectingAdapter, +} + +impl Adapter { + pub fn new( + event_loop: &ActiveEventLoop, + _window: &Window, + activation_handler: impl 'static + ActivationHandler + Send, + action_handler: impl 'static + ActionHandler + Send, + _deactivation_handler: impl 'static + DeactivationHandler, + ) -> Self { + let app = event_loop.android_app(); + let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr() as *mut _) }.unwrap(); + let mut env = vm.get_env().unwrap(); + let activity = unsafe { JObject::from_raw(app.activity_as_ptr() as *mut _) }; + let view = env + .get_field( + &activity, + "mNativeContentView", + "Landroid/app/NativeActivity$NativeContentView;", + ) + .unwrap() + .l() + .unwrap(); + let adapter = + InjectingAdapter::new(&mut env, &view, activation_handler, action_handler).unwrap(); + Self { adapter } + } + + pub fn update_if_active(&mut self, updater: impl FnOnce() -> TreeUpdate) { + self.adapter.update_if_active(updater); + } + + pub fn process_event(&mut self, _window: &Window, _event: &WindowEvent) {} +} diff --git a/platforms/winit/src/platform_impl/mod.rs b/platforms/winit/src/platform_impl/mod.rs index 76197cbd..38bb1a8d 100644 --- a/platforms/winit/src/platform_impl/mod.rs +++ b/platforms/winit/src/platform_impl/mod.rs @@ -27,6 +27,10 @@ mod platform; #[path = "unix.rs"] mod platform; +#[cfg(target_os = "android")] +#[path = "android.rs"] +mod platform; + #[cfg(not(any( target_os = "windows", target_os = "macos", @@ -39,7 +43,8 @@ mod platform; target_os = "netbsd", target_os = "openbsd" ) - ) + ), + target_os = "android" )))] #[path = "null.rs"] mod platform; From 932550c2a9f7c7f0f770ecee349a372b8e68856c Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 8 Aug 2024 09:18:21 -0500 Subject: [PATCH 07/46] Finish minimal proof-of-concept adapter --- platforms/android/build-dex.sh | 2 +- platforms/android/classes.dex | Bin 2200 -> 5372 bytes .../java/dev/accesskit/android/Delegate.java | 82 +++++++++++ platforms/android/src/adapter.rs | 92 ++++++++++-- platforms/android/src/inject.rs | 139 +++++++++++++++++- platforms/android/src/node.rs | 69 ++++++++- platforms/android/src/util.rs | 5 + platforms/winit/examples/simple.rs | 8 + 8 files changed, 373 insertions(+), 24 deletions(-) diff --git a/platforms/android/build-dex.sh b/platforms/android/build-dex.sh index 62eec1d6..b21c9b5c 100755 --- a/platforms/android/build-dex.sh +++ b/platforms/android/build-dex.sh @@ -2,5 +2,5 @@ set -e ANDROID_JAR=$ANDROID_HOME/platforms/android-30/android.jar find java -name '*.class' -delete -javac --source 8 --target 8 --boot-class-path $ANDROID_JAR `find java -name '*.java'` +javac --source 8 --target 8 --boot-class-path $ANDROID_JAR -Xlint:deprecation `find java -name '*.java'` $ANDROID_HOME/build-tools/33.0.2/d8 --classpath $ANDROID_JAR --output . `find java -name '*.class'` diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index 539ecb48f6283d1fb2168cf71cda54d50d5931df..61e8675cb509f8ec26b60e62bd677f4c7802f0fa 100644 GIT binary patch literal 5372 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 tree, } } + + fn get_full_tree(&mut self) -> Option<&Tree> { + match self { + Self::Inactive => None, + Self::Placeholder(_) => None, + Self::Active(tree) => Some(tree), + } + } } #[derive(Default)] @@ -71,11 +76,17 @@ impl Adapter { /// [`ActivationHandler::request_initial_tree`] initially returned `None`, /// the [`TreeUpdate`] returned by the provided function must contain /// a full tree. - /// - /// TODO: dispatch events - pub fn update_if_active(&mut self, update_factory: impl FnOnce() -> TreeUpdate) { + pub fn update_if_active( + &mut self, + update_factory: impl FnOnce() -> TreeUpdate, + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + ) { match &mut self.state { - State::Inactive => (), + State::Inactive => { + return; + } State::Placeholder(_) => { self.state = State::Active(Tree::new(update_factory(), true)); } @@ -83,6 +94,19 @@ impl Adapter { tree.update(update_factory()); } } + // TODO: Send other events; only send a change event if the tree + // actually changed. + env.call_static_method( + callback_class, + "sendEvent", + "(Landroid/view/View;II)V", + &[ + host.into(), + HOST_VIEW_ID.into(), + EVENT_WINDOW_CONTENT_CHANGED.into(), + ], + ) + .unwrap(); } pub fn populate_node_info( @@ -90,6 +114,8 @@ impl Adapter { activation_handler: &mut H, env: &mut JNIEnv, host: &JObject, + host_screen_x: jint, + host_screen_y: jint, virtual_view_id: jint, jni_node: &JObject, ) -> Result { @@ -108,7 +134,14 @@ impl Adapter { }; let wrapper = NodeWrapper(&node); - wrapper.populate_node_info(env, host, &mut self.node_id_map, jni_node)?; + wrapper.populate_node_info( + env, + host, + host_screen_x, + host_screen_y, + &mut self.node_id_map, + jni_node, + )?; Ok(true) } @@ -126,4 +159,39 @@ impl Adapter { let node = root.node_at_point(point, &filter).unwrap_or(root); self.node_id_map.get_or_create_java_id(&node) } + + pub fn perform_action( + &mut self, + action_handler: &mut H, + _env: &mut JNIEnv, + _host: &JObject, + virtual_view_id: jint, + action: jint, + _arguments: &JObject, + ) -> Result { + let Some(tree) = self.state.get_full_tree() else { + return Ok(false); + }; + let tree_state = tree.state(); + let target = if virtual_view_id == HOST_VIEW_ID { + tree_state.root_id() + } else { + let Some(accesskit_id) = self.node_id_map.get_accesskit_id(virtual_view_id) else { + return Ok(false); + }; + accesskit_id + }; + let request = match action { + ACTION_CLICK => ActionRequest { + action: Action::Default, + target, + data: None, + }, + _ => { + return Ok(false); + } + }; + action_handler.do_action(request); + Ok(true) + } } diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index d40ebfe6..be6d1530 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -13,13 +13,14 @@ use accesskit::{ActionHandler, ActivationHandler, TreeUpdate}; use jni::{ errors::Result, objects::{GlobalRef, JClass, JObject, WeakRef}, - sys::jlong, - JNIEnv, JavaVM, + sys::{jboolean, jint, jlong, JNI_FALSE, JNI_TRUE}, + JNIEnv, JavaVM, NativeMethod, }; use log::debug; use once_cell::sync::OnceCell; use std::{ collections::BTreeMap, + ffi::c_void, sync::{ atomic::{AtomicI64, Ordering}, Arc, Mutex, Weak, @@ -34,10 +35,109 @@ struct InnerInjectingAdapter { action_handler: Box, } +impl InnerInjectingAdapter { + fn populate_node_info( + &mut self, + env: &mut JNIEnv, + host: &JObject, + host_screen_x: jint, + host_screen_y: jint, + virtual_view_id: jint, + jni_node: &JObject, + ) -> Result { + self.adapter.populate_node_info( + &mut *self.activation_handler, + env, + host, + host_screen_x, + host_screen_y, + virtual_view_id, + jni_node, + ) + } + + pub fn perform_action( + &mut self, + env: &mut JNIEnv, + host: &JObject, + virtual_view_id: jint, + action: jint, + arguments: &JObject, + ) -> Result { + self.adapter.perform_action( + &mut *self.action_handler, + env, + host, + virtual_view_id, + action, + arguments, + ) + } +} + static NEXT_HANDLE: AtomicI64 = AtomicI64::new(0); static HANDLE_MAP: Mutex>>> = Mutex::new(BTreeMap::new()); +fn inner_adapter_from_handle(handle: jlong) -> Option>> { + let handle_map_guard = HANDLE_MAP.lock().unwrap(); + handle_map_guard.get(&handle).and_then(Weak::upgrade) +} + +extern "system" fn populate_node_info( + mut env: JNIEnv, + _class: JClass, + adapter_handle: jlong, + host: JObject, + host_screen_x: jint, + host_screen_y: jint, + virtual_view_id: jint, + node_info: JObject, +) -> jboolean { + let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { + return JNI_FALSE; + }; + let mut inner_adapter = inner_adapter.lock().unwrap(); + if inner_adapter + .populate_node_info( + &mut env, + &host, + host_screen_x, + host_screen_y, + virtual_view_id, + &node_info, + ) + .unwrap() + { + JNI_TRUE + } else { + JNI_FALSE + } +} + +extern "system" fn perform_action( + mut env: JNIEnv, + _class: JClass, + adapter_handle: jlong, + host: JObject, + virtual_view_id: jint, + action: jint, + arguments: JObject, +) -> jboolean { + let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { + return JNI_FALSE; + }; + let mut inner_adapter = inner_adapter.lock().unwrap(); + if inner_adapter + .perform_action(&mut env, &host, virtual_view_id, action, &arguments) + .unwrap() + { + JNI_TRUE + } else { + JNI_FALSE + } +} + fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { static CLASS: OnceCell = OnceCell::new(); let global = CLASS.get_or_try_init(|| { @@ -66,7 +166,23 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { }; #[cfg(not(feature = "embedded-dex"))] let class = env.find_class("dev/accesskit/android/Delegate")?; - // TODO: register JNI methods + env.register_native_methods( + &class, + &[ + NativeMethod { + name: "populateNodeInfo".into(), + sig: "(JLandroid/view/View;IIILandroid/view/accessibility/AccessibilityNodeInfo;)Z" + .into(), + fn_ptr: populate_node_info as *mut c_void, + }, + NativeMethod { + name: "performAction".into(), + sig: "(JLandroid/view/View;IILandroid/os/Bundle;)Z" + .into(), + fn_ptr: perform_action as *mut c_void, + }, + ], + )?; env.new_global_ref(class) })?; Ok(global.as_obj().into()) @@ -74,6 +190,7 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { pub struct InjectingAdapter { vm: JavaVM, + delegate_class: &'static JClass<'static>, host: WeakRef, handle: jlong, inner: Arc>, @@ -105,6 +222,7 @@ impl InjectingAdapter { )?; Ok(Self { vm: env.get_java_vm()?, + delegate_class, host: env.new_weak_ref(host_view)?.unwrap(), handle, inner, @@ -119,11 +237,16 @@ impl InjectingAdapter { /// /// TODO: dispatch events pub fn update_if_active(&mut self, update_factory: impl FnOnce() -> TreeUpdate) { - self.inner - .lock() - .unwrap() - .adapter - .update_if_active(update_factory); + let mut env = self.vm.get_env().unwrap(); + let Some(host) = self.host.upgrade_local(&env).unwrap() else { + return; + }; + self.inner.lock().unwrap().adapter.update_if_active( + update_factory, + &mut env, + &self.delegate_class, + &host, + ); } } diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index 6b224f91..ba75d04e 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -3,9 +3,9 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::{Role, Toggled}; +use accesskit::{Live, Role, Toggled}; use accesskit_consumer::Node; -use jni::{errors::Result, objects::JObject, JNIEnv}; +use jni::{errors::Result, objects::JObject, sys::jint, JNIEnv}; use crate::{filters::filter, util::*}; @@ -57,10 +57,20 @@ impl<'a> NodeWrapper<'a> { } } + fn class_name(&self) -> &str { + match self.0.role() { + Role::Button => "android.widget.Button", + Role::Label => "android.widget.TextView", + _ => "android.view.View", + } + } + pub(crate) fn populate_node_info( &self, env: &mut JNIEnv, host: &JObject, + host_screen_x: jint, + host_screen_y: jint, id_map: &mut NodeIdMap, jni_node: &JObject, ) -> Result<()> { @@ -90,6 +100,26 @@ impl<'a> NodeWrapper<'a> { } } + if let Some(rect) = self.0.bounding_box() { + let android_rect_class = env.find_class("android/graphics/Rect")?; + let android_rect = env.new_object( + &android_rect_class, + "(IIII)V", + &[ + ((rect.x0 as jint) + host_screen_x).into(), + ((rect.y0 as jint) + host_screen_y).into(), + ((rect.x1 as jint) + host_screen_x).into(), + ((rect.y1 as jint) + host_screen_y).into(), + ], + )?; + env.call_method( + jni_node, + "setBoundsInScreen", + "(Landroid/graphics/Rect;)V", + &[(&android_rect).into()], + )?; + } + if self.is_checkable() { env.call_method(jni_node, "setCheckable", "(Z)V", &[true.into()])?; env.call_method(jni_node, "setChecked", "(Z)V", &[self.is_checked().into()])?; @@ -119,10 +149,43 @@ impl<'a> NodeWrapper<'a> { env.call_method( jni_node, "setText", - "(Ljava/lang/String;)V", + "(Ljava/lang/CharSequence;)V", &[(&name).into()], )?; } + let class_name = env.new_string(self.class_name())?; + env.call_method( + jni_node, + "setClassName", + "(Ljava/lang/CharSequence;)V", + &[(&class_name).into()], + )?; + + fn add_action(env: &mut JNIEnv, jni_node: &JObject, action: jint) -> Result<()> { + // Note: We're using the deprecated addAction signature. + // But this one is much easier to call from JNI since it uses + // a simple integer constant. Revisit if Android ever gets strict + // about prohibiting deprecated methods for applications targeting + // newer SDKs. + env.call_method(jni_node, "addAction", "(I)V", &[action.into()])?; + Ok(()) + } + + if self.0.default_action_verb().is_some() { + add_action(env, jni_node, ACTION_CLICK)?; + } + + let live = match self.0.live() { + Live::Off => LIVE_REGION_NONE, + Live::Polite => LIVE_REGION_POLITE, + Live::Assertive => LIVE_REGION_ASSERTIVE, + }; + env.call_method( + jni_node, + "setLiveRegion", + "(I)V", + &[live.into()], + )?; Ok(()) } diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index b0f6e9ad..d9aef0a4 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -8,7 +8,12 @@ use accesskit_consumer::Node; use jni::sys::jint; use std::collections::HashMap; +pub(crate) const ACTION_CLICK: jint = 1 << 4; +pub(crate) const EVENT_WINDOW_CONTENT_CHANGED: jint = 1 << 11; pub(crate) const HOST_VIEW_ID: jint = -1; +pub(crate) const LIVE_REGION_NONE: jint = 0; +pub(crate) const LIVE_REGION_POLITE: jint = 1; +pub(crate) const LIVE_REGION_ASSERTIVE: jint = 2; #[derive(Default)] pub(crate) struct NodeIdMap { diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs index 877f008e..145044bc 100644 --- a/platforms/winit/examples/simple.rs +++ b/platforms/winit/examples/simple.rs @@ -31,6 +31,13 @@ const BUTTON_2_RECT: Rect = Rect { y1: 100.0, }; +const ANNOUNCEMENT_RECT: Rect = Rect { + x0: 20.0, + y0: 100.0, + x1: 100.0, + y1: 140.0, +}; + fn build_button(id: NodeId, label: &str) -> Node { let rect = match id { BUTTON_1_ID => BUTTON_1_RECT, @@ -48,6 +55,7 @@ fn build_button(id: NodeId, label: &str) -> Node { fn build_announcement(text: &str) -> Node { let mut node = Node::new(Role::Label); + node.set_bounds(ANNOUNCEMENT_RECT); node.set_value(text); node.set_live(Live::Polite); node From 13570a0400068fdc4882155f7e843cb1f0ebb85c Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 29 Aug 2024 07:01:49 -0500 Subject: [PATCH 08/46] Update to winit 0.30.5, since it has an API required by the accesskit_winit Android backend --- platforms/winit/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platforms/winit/Cargo.toml b/platforms/winit/Cargo.toml index 6a7f40c5..b0ccb3fd 100644 --- a/platforms/winit/Cargo.toml +++ b/platforms/winit/Cargo.toml @@ -20,7 +20,7 @@ tokio = ["accesskit_unix/tokio"] [dependencies] accesskit = { version = "0.17.1", path = "../../common" } -winit = { version = "0.30", default-features = false } +winit = { version = "0.30.5", default-features = false } rwh_05 = { package = "raw-window-handle", version = "0.5", features = ["std"], optional = true } rwh_06 = { package = "raw-window-handle", version = "0.6.2", features = ["std"], optional = true } @@ -37,7 +37,7 @@ accesskit_unix = { version = "0.13.1", path = "../unix", optional = true, defaul accesskit_android = { version = "0.1.0", path = "../android", features = ["embedded-dex"] } [dev-dependencies.winit] -version = "0.30" +version = "0.30.5" default-features = false features = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita", "android-native-activity"] From 4c5df21744a423a9d80adf28f49c233311b1fbda Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Tue, 3 Sep 2024 10:20:21 -0500 Subject: [PATCH 09/46] Reformat; factor out a utility function to send events from native --- platforms/android/src/adapter.rs | 16 ++++++---------- platforms/android/src/node.rs | 7 +------ platforms/android/src/util.rs | 22 +++++++++++++++++++++- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index f990613b..e4c873db 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -96,17 +96,13 @@ impl Adapter { } // TODO: Send other events; only send a change event if the tree // actually changed. - env.call_static_method( + send_event( + env, callback_class, - "sendEvent", - "(Landroid/view/View;II)V", - &[ - host.into(), - HOST_VIEW_ID.into(), - EVENT_WINDOW_CONTENT_CHANGED.into(), - ], - ) - .unwrap(); + host, + HOST_VIEW_ID, + EVENT_WINDOW_CONTENT_CHANGED, + ); } pub fn populate_node_info( diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index ba75d04e..2b2a8f16 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -180,12 +180,7 @@ impl<'a> NodeWrapper<'a> { Live::Polite => LIVE_REGION_POLITE, Live::Assertive => LIVE_REGION_ASSERTIVE, }; - env.call_method( - jni_node, - "setLiveRegion", - "(I)V", - &[live.into()], - )?; + env.call_method(jni_node, "setLiveRegion", "(I)V", &[live.into()])?; Ok(()) } diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index d9aef0a4..b419eab5 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -5,7 +5,11 @@ use accesskit::NodeId; use accesskit_consumer::Node; -use jni::sys::jint; +use jni::{ + objects::{JClass, JObject}, + sys::jint, + JNIEnv, +}; use std::collections::HashMap; pub(crate) const ACTION_CLICK: jint = 1 << 4; @@ -42,3 +46,19 @@ impl NodeIdMap { java_id } } + +pub(crate) fn send_event( + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + virtual_view_id: jint, + event_type: jint, +) { + env.call_static_method( + callback_class, + "sendEvent", + "(Landroid/view/View;II)V", + &[host.into(), virtual_view_id.into(), event_type.into()], + ) + .unwrap(); +} From 1b2a379272fec92aa2215658b49abf63e1697b85 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Tue, 3 Sep 2024 12:07:54 -0500 Subject: [PATCH 10/46] Attempt to implement explore-by-touch --- platforms/android/src/inject.rs | 63 +++++++++++++++++++- platforms/android/src/util.rs | 2 + platforms/winit/src/platform_impl/android.rs | 12 +++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index be6d1530..7d309d5e 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -13,7 +13,7 @@ use accesskit::{ActionHandler, ActivationHandler, TreeUpdate}; use jni::{ errors::Result, objects::{GlobalRef, JClass, JObject, WeakRef}, - sys::{jboolean, jint, jlong, JNI_FALSE, JNI_TRUE}, + sys::{jboolean, jfloat, jint, jlong, JNI_FALSE, JNI_TRUE}, JNIEnv, JavaVM, NativeMethod, }; use log::debug; @@ -27,7 +27,7 @@ use std::{ }, }; -use crate::adapter::Adapter; +use crate::{adapter::Adapter, util::*}; struct InnerInjectingAdapter { adapter: Adapter, @@ -56,7 +56,12 @@ impl InnerInjectingAdapter { ) } - pub fn perform_action( + fn virtual_view_at_point(&mut self, x: jfloat, y: jfloat) -> jint { + self.adapter + .virtual_view_at_point(&mut *self.activation_handler, x, y) + } + + fn perform_action( &mut self, env: &mut JNIEnv, host: &JObject, @@ -194,6 +199,7 @@ pub struct InjectingAdapter { host: WeakRef, handle: jlong, inner: Arc>, + hover_view_id: jint, } impl InjectingAdapter { @@ -226,6 +232,7 @@ impl InjectingAdapter { host: env.new_weak_ref(host_view)?.unwrap(), handle, inner, + hover_view_id: HOST_VIEW_ID, }) } @@ -248,6 +255,56 @@ impl InjectingAdapter { &host, ); } + + pub fn handle_hover_enter_or_move(&mut self, x: jfloat, y: jfloat) { + let old_id = self.hover_view_id; + let new_id = self.inner.lock().unwrap().virtual_view_at_point(x, y); + if new_id == old_id { + return; + } + let mut env = self.vm.get_env().unwrap(); + let Some(host) = self.host.upgrade_local(&env).unwrap() else { + return; + }; + self.hover_view_id = new_id; + if new_id != HOST_VIEW_ID { + send_event( + &mut env, + &self.delegate_class, + &host, + new_id, + EVENT_VIEW_HOVER_ENTER, + ); + } + if old_id != HOST_VIEW_ID { + send_event( + &mut env, + &self.delegate_class, + &host, + old_id, + EVENT_VIEW_HOVER_EXIT, + ); + } + } + + pub fn handle_hover_exit(&mut self) { + if self.hover_view_id == HOST_VIEW_ID { + return; + } + let old_id = self.hover_view_id; + self.hover_view_id = HOST_VIEW_ID; + let mut env = self.vm.get_env().unwrap(); + let Some(host) = self.host.upgrade_local(&env).unwrap() else { + return; + }; + send_event( + &mut env, + &self.delegate_class, + &host, + old_id, + EVENT_VIEW_HOVER_EXIT, + ); + } } impl Drop for InjectingAdapter { diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index b419eab5..807083a1 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -13,6 +13,8 @@ use jni::{ use std::collections::HashMap; pub(crate) const ACTION_CLICK: jint = 1 << 4; +pub(crate) const EVENT_VIEW_HOVER_ENTER: jint = 1 << 7; +pub(crate) const EVENT_VIEW_HOVER_EXIT: jint = 1 << 8; pub(crate) const EVENT_WINDOW_CONTENT_CHANGED: jint = 1 << 11; pub(crate) const HOST_VIEW_ID: jint = -1; pub(crate) const LIVE_REGION_NONE: jint = 0; diff --git a/platforms/winit/src/platform_impl/android.rs b/platforms/winit/src/platform_impl/android.rs index abc53f8a..dad0988e 100644 --- a/platforms/winit/src/platform_impl/android.rs +++ b/platforms/winit/src/platform_impl/android.rs @@ -46,5 +46,15 @@ impl Adapter { self.adapter.update_if_active(updater); } - pub fn process_event(&mut self, _window: &Window, _event: &WindowEvent) {} + pub fn process_event(&mut self, _window: &Window, event: &WindowEvent) { + match event { + WindowEvent::CursorMoved { position, .. } => { + self.adapter.handle_hover_enter_or_move(position.x as _, position.y as _); + } + WindowEvent::CursorLeft { .. } => { + self.adapter.handle_hover_exit(); + } + _ => (), + } + } } From 742a174393fa1547d87fb05abb94ad4e924efb0e Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 4 Sep 2024 08:37:45 -0500 Subject: [PATCH 11/46] Try to add the missing pieces to let the framework draw the focus rectangle. But NativeActivity seems to be sabotaging this. --- platforms/android/classes.dex | Bin 5372 -> 5504 bytes .../java/dev/accesskit/android/Delegate.java | 10 ++++++++++ 2 files changed, 10 insertions(+) diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index 61e8675cb509f8ec26b60e62bd677f4c7802f0fa..fa38eb39c9d3547e888ca686fe8c3de06e6f1e80 100644 GIT binary patch literal 5504 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 delta 2757 zcmZ9OYitx%6oAj2*`4j|Zg*#QOG`_kLqRB%>b6BA!53QbReY3Hd5LJMQKFmH7{O?= z^?^~MNp&J22nLKH8jTh-5e+7gn20}&#u(A032NkzuSoQV4-);(?JQQO{q8yU+~=Ho z?%5gZKE0?me8<*wx&K^w;KBEI?OF3z?@P|2755##yJ_>D=9S;h==uC~lgNmOY`tk| z5n-S6v28+A`z-jyh=j-ujz|_c>57z*7bHZI$iozSbF_OR2|D;tfS#<>D)_!<5LFAyoh9O#CN;aXS=55PFQ z0q@l)K7kYPJv0{>1oL4DEQeLF8aBW%JO)of1pDC#dK1DCb`XqX9tpAAKAM2;l`(k|{`o?WpwvJj3Pp(JiV;P*0oE3JC zQ*MC*hqBg6`bWKqmf8Td1ddcEC($mk(v?fCY?I5ZOv>d}cF3TWfviS)GDogMrqHgk zvQcidvPEvPvLH8GS+vrZ8FCSIpEK9~zWAN7l9g4~&?*%xrzY@m%bMs^qGj3jSo3UO zx0z?VS`VaT>p@&ENVBaM*wDqa7sv=#M^`1U=v_grC{1#Om9yn)E8FE7D+_Y1l_j|$ zmP66eWXGxjTus`LuS+6-6Ri4hq3kn*vflnX?IqAh9j(vw_JABUKkD&jCA-j;N2|P! zzSVL61S{w}?I2wBBCI#AtaBV02{b75SdHDo?sW8`*SRv3ps%vRm1>xxAt#-@RGV2T zBjMRBy-*zfbh?+Bjts|ii8E8Vw2&3$)pX>g7OYD#K(AeW2NMk8pCL(E>!f6@>zx^@ zdnA#{J8_>%IVnTEDH-bXWVAe20w>4)bMQMEHXS{Y>a67%k~Lka$^lM@o}&QN)G2b*1?y#rq(yEg+$M(^3kG1P4y(_b^`^ZjSYUGk;!nGQD%%_p2p{Xm+ zKhq~q;Lc>#fBV$uSv=LJ#zX-Vwf$$JWW0$!I_dk420n{t>#=1twlw;x_LYjK`b!>5 zWF?fYM77)~p<8mLp?|3-#Zom)lXI&9(;`mBY;5atvd;2^TsgIZe_vCNUn)#Bi*!)qK||t}#j5iGFREcjsoABRR%84c6R_?NDL1`7US7 zL?BMiq@9{6c04+k_^oKh_9k|g%#-1S7L!Ga-wG73tM-8w<3-0mRx;Z;3B~`hWc=|a zlYYBtq4`pOvi0DP$Ei+_>IC{pQ2nQA`mfXUp+DlvM4Z#6!j2eW(975k|IeUZcr9|JFn diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 3715d872..4a85aafe 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -106,6 +106,7 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { switch (action) { case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: accessibilityFocus = virtualViewId; + host.invalidate(); sendEventInternal(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); return true; } @@ -119,6 +120,15 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { } return true; } + + @Override + public AccessibilityNodeInfo findFocus(int focusType) { + if (focusType != AccessibilityNodeInfo.FOCUS_ACCESSIBILITY + || accessibilityFocus == HOST_VIEW_ID) { + return null; + } + return createAccessibilityNodeInfo(accessibilityFocus); + } }; } } From 4fd6611b2616e076ae5ad51b626f0c609fd737ca Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 4 Sep 2024 08:43:22 -0500 Subject: [PATCH 12/46] Resolve clippy warnings --- platforms/android/src/adapter.rs | 1 + platforms/android/src/inject.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index e4c873db..e4de559c 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -105,6 +105,7 @@ impl Adapter { ); } + #[allow(clippy::too_many_arguments)] pub fn populate_node_info( &mut self, activation_handler: &mut H, diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index 7d309d5e..d4df3344 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -251,7 +251,7 @@ impl InjectingAdapter { self.inner.lock().unwrap().adapter.update_if_active( update_factory, &mut env, - &self.delegate_class, + self.delegate_class, &host, ); } @@ -270,7 +270,7 @@ impl InjectingAdapter { if new_id != HOST_VIEW_ID { send_event( &mut env, - &self.delegate_class, + self.delegate_class, &host, new_id, EVENT_VIEW_HOVER_ENTER, @@ -279,7 +279,7 @@ impl InjectingAdapter { if old_id != HOST_VIEW_ID { send_event( &mut env, - &self.delegate_class, + self.delegate_class, &host, old_id, EVENT_VIEW_HOVER_EXIT, @@ -299,7 +299,7 @@ impl InjectingAdapter { }; send_event( &mut env, - &self.delegate_class, + self.delegate_class, &host, old_id, EVENT_VIEW_HOVER_EXIT, From fd94ce054c1d31f4ed49870ded3335683418ba2e Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 4 Sep 2024 08:45:48 -0500 Subject: [PATCH 13/46] Fix copyright year --- platforms/android/src/adapter.rs | 2 +- platforms/android/src/lib.rs | 2 +- platforms/android/src/node.rs | 2 +- platforms/android/src/util.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index e4de559c..e429520c 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The AccessKit Authors. All rights reserved. +// Copyright 2024 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. diff --git a/platforms/android/src/lib.rs b/platforms/android/src/lib.rs index 264d0d39..d1cbe8b2 100644 --- a/platforms/android/src/lib.rs +++ b/platforms/android/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The AccessKit Authors. All rights reserved. +// Copyright 2024 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. diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index 2b2a8f16..89c98e71 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The AccessKit Authors. All rights reserved. +// Copyright 2024 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. diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 807083a1..f159d37f 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The AccessKit Authors. All rights reserved. +// Copyright 2024 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. From 3615903713814c6b9554123eb16539d708f06a75 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 4 Sep 2024 09:40:50 -0500 Subject: [PATCH 14/46] Map roles to Android classes --- platforms/android/src/node.rs | 45 ++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index 89c98e71..dd01c8c8 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -3,6 +3,11 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. +// Derived from Chromium's accessibility abstraction. +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE.chromium file. + use accesskit::{Live, Role, Toggled}; use accesskit_consumer::Node; use jni::{errors::Result, objects::JObject, sys::jint, JNIEnv}; @@ -59,7 +64,45 @@ impl<'a> NodeWrapper<'a> { fn class_name(&self) -> &str { match self.0.role() { - Role::Button => "android.widget.Button", + Role::TextInput + | Role::MultilineTextInput + | Role::SearchInput + | Role::EmailInput + | Role::NumberInput + | Role::PasswordInput + | Role::PhoneNumberInput + | Role::UrlInput => "android.widget.EditText", + Role::Slider => "android.widget.SeekBar", + Role::ColorWell + | Role::ComboBox + | Role::EditableComboBox + | Role::DateInput + | Role::DateTimeInput + | Role::WeekInput + | Role::MonthInput + | Role::TimeInput => "android.widget.Spinner", + Role::Button => { + if self.0.supports_toggle() { + "android.widget.ToggleButton" + } else { + "android.widget.Button" + } + } + Role::PdfActionableHighlight => "android.widget.Button", + Role::CheckBox => "android.widget.CheckBox", + Role::RadioButton => "android.widget.RadioButton", + Role::RadioGroup => "android.widget.RadioGroup", + Role::Switch => "android.widget.ToggleButton", + Role::Canvas | Role::Image | Role::SvgRoot => "android.widget.ImageView", + Role::Meter | Role::ProgressIndicator => "android.widget.ProgressBar", + Role::TabList => "android.widget.TabWidget", + Role::Grid | Role::Table | Role::TreeGrid => "android.widget.GridView", + Role::DescriptionList | Role::List | Role::ListBox => "android.widget.ListView", + Role::Dialog => "android.app.Dialog", + Role::RootWebArea => "android.webkit.WebView", + Role::MenuItem | Role::MenuItemCheckBox | Role::MenuItemRadio => { + "android.view.MenuItem" + } Role::Label => "android.widget.TextView", _ => "android.view.View", } From ef753e7b03e8e4aff4cd46dc8f7e009af14c9805 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 5 Sep 2024 14:32:09 -0500 Subject: [PATCH 15/46] Better mapping of the AccessKit name property to the appropriate Android equivalent --- platforms/android/src/node.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index dd01c8c8..8975080e 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -144,6 +144,9 @@ impl<'a> NodeWrapper<'a> { } if let Some(rect) = self.0.bounding_box() { + if self.0.role() == Role::TextInput { + println!("text input rect {rect:?}"); + } let android_rect_class = env.find_class("android/graphics/Rect")?; let android_rect = env.new_object( &android_rect_class, @@ -191,7 +194,11 @@ impl<'a> NodeWrapper<'a> { let name = env.new_string(name)?; env.call_method( jni_node, - "setText", + if self.0.role() == Role::Label { + "setText" + } else { + "setContentDescription" + }, "(Ljava/lang/CharSequence;)V", &[(&name).into()], )?; From 90330886f18b037fc3ca2c1509d845b3a7ca377f Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 5 Sep 2024 17:29:50 -0500 Subject: [PATCH 16/46] Expose more properties --- platforms/android/src/node.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index 8975080e..b368d5c6 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -21,6 +21,14 @@ impl<'a> NodeWrapper<'a> { self.0.name() } + fn value(&self) -> Option { + self.0.value() + } + + fn is_editable(&self) -> bool { + self.0.is_text_input() && !self.0.is_read_only() + } + fn is_enabled(&self) -> bool { !self.0.is_disabled() } @@ -170,6 +178,12 @@ impl<'a> NodeWrapper<'a> { env.call_method(jni_node, "setCheckable", "(Z)V", &[true.into()])?; env.call_method(jni_node, "setChecked", "(Z)V", &[self.is_checked().into()])?; } + env.call_method( + jni_node, + "setEditable", + "(Z)V", + &[self.is_editable().into()], + )?; env.call_method(jni_node, "setEnabled", "(Z)V", &[self.is_enabled().into()])?; env.call_method( jni_node, @@ -203,6 +217,15 @@ impl<'a> NodeWrapper<'a> { &[(&name).into()], )?; } + if let Some(value) = self.value() { + let value = env.new_string(value)?; + env.call_method( + jni_node, + "setText", + "(Ljava/lang/CharSequence;)V", + &[(&value).into()], + )?; + } let class_name = env.new_string(self.class_name())?; env.call_method( jni_node, From dd9b1547da997bcd357d17eeeaa0eb5046472c29 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 23 Sep 2024 12:02:39 -0500 Subject: [PATCH 17/46] Support the focus action; drop a leftover bit of println debugging --- platforms/android/src/adapter.rs | 14 +++++++++++++- platforms/android/src/node.rs | 9 +++++---- platforms/android/src/util.rs | 1 + 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index e429520c..34e4e020 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -180,7 +180,19 @@ impl Adapter { }; let request = match action { ACTION_CLICK => ActionRequest { - action: Action::Default, + action: { + let node = tree_state.node_by_id(target).unwrap(); + if node.is_focusable() && !node.is_focused() && !node.is_clickable() { + Action::Focus + } else { + Action::Default + } + }, + target, + data: None, + }, + ACTION_FOCUS => ActionRequest { + action: Action::Focus, target, data: None, }, diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index b368d5c6..c497b244 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -152,9 +152,6 @@ impl<'a> NodeWrapper<'a> { } if let Some(rect) = self.0.bounding_box() { - if self.0.role() == Role::TextInput { - println!("text input rect {rect:?}"); - } let android_rect_class = env.find_class("android/graphics/Rect")?; let android_rect = env.new_object( &android_rect_class, @@ -244,9 +241,13 @@ impl<'a> NodeWrapper<'a> { Ok(()) } - if self.0.default_action_verb().is_some() { + let can_focus = self.0.is_focusable() && !self.0.is_focused(); + if self.0.is_clickable() || can_focus { add_action(env, jni_node, ACTION_CLICK)?; } + if can_focus { + add_action(env, jni_node, ACTION_FOCUS)?; + } let live = match self.0.live() { Live::Off => LIVE_REGION_NONE, diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index f159d37f..e71a196c 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -12,6 +12,7 @@ use jni::{ }; use std::collections::HashMap; +pub(crate) const ACTION_FOCUS: jint = 1 << 0; pub(crate) const ACTION_CLICK: jint = 1 << 4; pub(crate) const EVENT_VIEW_HOVER_ENTER: jint = 1 << 7; pub(crate) const EVENT_VIEW_HOVER_EXIT: jint = 1 << 8; From 87510d030698a8b8271419e9bdec1fca3f798f79 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 23 Sep 2024 14:53:57 -0500 Subject: [PATCH 18/46] More complete event dispatching, including the input focus event --- platforms/android/src/adapter.rs | 131 ++++++++++++++++++++++++++----- platforms/android/src/inject.rs | 2 - platforms/android/src/util.rs | 1 + 3 files changed, 112 insertions(+), 22 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 34e4e020..b06dcc58 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -7,7 +7,7 @@ use accesskit::{ Action, ActionHandler, ActionRequest, ActivationHandler, NodeBuilder, NodeId, Point, Role, Tree as TreeData, TreeUpdate, }; -use accesskit_consumer::Tree; +use accesskit_consumer::{Node, Tree, TreeChangeHandler}; use jni::{ errors::Result, objects::{JClass, JObject}, @@ -17,6 +17,94 @@ use jni::{ use crate::{filters::filter, node::NodeWrapper, util::*}; +fn send_window_content_changed(env: &mut JNIEnv, callback_class: &JClass, host: &JObject) { + send_event( + env, + callback_class, + host, + HOST_VIEW_ID, + EVENT_WINDOW_CONTENT_CHANGED, + ); +} + +fn send_focus_event_if_applicable( + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + node_id_map: &mut NodeIdMap, + node: &Node, +) { + if node.is_root() && node.role() == Role::Window { + return; + } + let id = node_id_map.get_or_create_java_id(node); + send_event(env, callback_class, host, id, EVENT_VIEW_FOCUSED); +} + +struct AdapterChangeHandler<'a> { + env: &'a mut JNIEnv<'a>, + callback_class: &'a JClass<'a>, + host: &'a JObject<'a>, + node_id_map: &'a mut NodeIdMap, + sent_window_content_changed: bool, +} + +impl<'a> AdapterChangeHandler<'a> { + fn new( + env: &'a mut JNIEnv<'a>, + callback_class: &'a JClass<'a>, + host: &'a JObject<'a>, + node_id_map: &'a mut NodeIdMap, + ) -> Self { + Self { + env, + callback_class, + host, + node_id_map, + sent_window_content_changed: false, + } + } +} + +impl AdapterChangeHandler<'_> { + fn send_window_content_changed_if_needed(&mut self) { + if self.sent_window_content_changed { + return; + } + send_window_content_changed(self.env, self.callback_class, self.host); + self.sent_window_content_changed = true; + } +} + +impl TreeChangeHandler for AdapterChangeHandler<'_> { + fn node_added(&mut self, _node: &Node) { + self.send_window_content_changed_if_needed(); + // TODO: live regions? + } + + fn node_updated(&mut self, _old_node: &Node, _new_node: &Node) { + self.send_window_content_changed_if_needed(); + // TODO: other events + } + + fn focus_moved(&mut self, _old_node: Option<&Node>, new_node: Option<&Node>) { + if let Some(new_node) = new_node { + send_focus_event_if_applicable( + self.env, + self.callback_class, + self.host, + self.node_id_map, + new_node, + ); + } + } + + fn node_removed(&mut self, _node: &Node) { + self.send_window_content_changed_if_needed(); + // TODO: other events? + } +} + const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0); #[derive(Default)] @@ -76,33 +164,36 @@ impl Adapter { /// [`ActivationHandler::request_initial_tree`] initially returned `None`, /// the [`TreeUpdate`] returned by the provided function must contain /// a full tree. - pub fn update_if_active( - &mut self, + pub fn update_if_active<'a>( + &'a mut self, update_factory: impl FnOnce() -> TreeUpdate, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, + env: &'a mut JNIEnv<'a>, + callback_class: &'a JClass<'a>, + host: &'a JObject<'a>, ) { match &mut self.state { - State::Inactive => { - return; - } + State::Inactive => (), State::Placeholder(_) => { - self.state = State::Active(Tree::new(update_factory(), true)); + let tree = Tree::new(update_factory(), true); + send_window_content_changed(env, callback_class, host); + let state = tree.state(); + if let Some(focus) = state.focus() { + send_focus_event_if_applicable( + env, + callback_class, + host, + &mut self.node_id_map, + &focus, + ); + } + self.state = State::Active(tree); } State::Active(tree) => { - tree.update(update_factory()); + let mut handler = + AdapterChangeHandler::new(env, callback_class, host, &mut self.node_id_map); + tree.update_and_process_changes(update_factory(), &mut handler); } } - // TODO: Send other events; only send a change event if the tree - // actually changed. - send_event( - env, - callback_class, - host, - HOST_VIEW_ID, - EVENT_WINDOW_CONTENT_CHANGED, - ); } #[allow(clippy::too_many_arguments)] diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index d4df3344..74524a65 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -241,8 +241,6 @@ impl InjectingAdapter { /// [`ActivationHandler::request_initial_tree`] initially returned `None`, /// the [`TreeUpdate`] returned by the provided function must contain /// a full tree. - /// - /// TODO: dispatch events pub fn update_if_active(&mut self, update_factory: impl FnOnce() -> TreeUpdate) { let mut env = self.vm.get_env().unwrap(); let Some(host) = self.host.upgrade_local(&env).unwrap() else { diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index e71a196c..170e490f 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -14,6 +14,7 @@ use std::collections::HashMap; pub(crate) const ACTION_FOCUS: jint = 1 << 0; pub(crate) const ACTION_CLICK: jint = 1 << 4; +pub(crate) const EVENT_VIEW_FOCUSED: jint = 1 << 3; pub(crate) const EVENT_VIEW_HOVER_ENTER: jint = 1 << 7; pub(crate) const EVENT_VIEW_HOVER_EXIT: jint = 1 << 8; pub(crate) const EVENT_WINDOW_CONTENT_CHANGED: jint = 1 << 11; From 7f959397ab020318d2a738f2e7ef05dcec890e48 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Tue, 24 Sep 2024 10:05:02 -0500 Subject: [PATCH 19/46] Send text change events --- platforms/android/classes.dex | Bin 5504 -> 6752 bytes .../java/dev/accesskit/android/Delegate.java | 66 ++++++++++++++++-- platforms/android/src/adapter.rs | 35 +++++++++- platforms/android/src/node.rs | 42 ++++++----- 4 files changed, 115 insertions(+), 28 deletions(-) diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index fa38eb39c9d3547e888ca686fe8c3de06e6f1e80..efd4c2db4bee7ea4d16e1adddb3cc10a07aed1f2 100644 GIT binary patch literal 6752 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 diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 4a85aafe..09204258 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -3,6 +3,11 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. +// Derived from the Flutter engine. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE.chromium file. + package dev.accesskit.android; import android.os.Bundle; @@ -46,18 +51,27 @@ public void run() { }); } - private static void sendEventInternal(View host, int virtualViewId, int type) { - AccessibilityEvent event = AccessibilityEvent.obtain(type); - event.setPackageName(host.getContext().getPackageName()); + private static AccessibilityEvent newEvent(View host, int virtualViewId, int type) { + AccessibilityEvent e = AccessibilityEvent.obtain(type); + e.setPackageName(host.getContext().getPackageName()); if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) { - event.setSource(host); + e.setSource(host); } else { - event.setSource(host, virtualViewId); + e.setSource(host, virtualViewId); } + return e; + } + + private static void sendCompletedEvent(View host, AccessibilityEvent e) { + host.getParent().requestSendAccessibilityEvent(host, e); + } + + private static void sendEventInternal(View host, int virtualViewId, int type) { + AccessibilityEvent e = newEvent(host, virtualViewId, type); if (type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { - event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); + e.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); } - host.getParent().requestSendAccessibilityEvent(host, event); + sendCompletedEvent(host, e); } public static void sendEvent(final View host, final int virtualViewId, final int type) { @@ -69,6 +83,44 @@ public void run() { }); } + private static void sendTextChangedInternal(View host, int virtualViewId, String oldValue, String newValue) { + int i; + for (i = 0; i < oldValue.length() && i < newValue.length(); ++i) { + if (oldValue.charAt(i) != newValue.charAt(i)) { + break; + } + } + if (i >= oldValue.length() && i >= newValue.length()) { + return; // Text did not change + } + AccessibilityEvent e = newEvent(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); + e.setBeforeText(oldValue); + e.getText().add(newValue); + int firstDifference = i; + e.setFromIndex(firstDifference); + int oldIndex = oldValue.length() - 1; + int newIndex = newValue.length() - 1; + while (oldIndex >= firstDifference && newIndex >= firstDifference) { + if (oldValue.charAt(oldIndex) != newValue.charAt(newIndex)) { + break; + } + --oldIndex; + --newIndex; + } + e.setRemovedCount(oldIndex - firstDifference + 1); + e.setAddedCount(newIndex - firstDifference + 1); + sendCompletedEvent(host, e); + } + + public static void sendTextChanged(final View host, final int virtualViewId, final String oldValue, final String newValue) { + host.post(new Runnable() { + @Override + public void run() { + sendTextChangedInternal(host, virtualViewId, oldValue, newValue); + } + }); + } + private static native boolean populateNodeInfo(long adapterHandle, View host, int screenX, int screenY, int virtualViewId, AccessibilityNodeInfo nodeInfo); private static native boolean performAction(long adapterHandle, View host, int virtualViewId, int action, Bundle arguments); diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index b06dcc58..49e5b2b6 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -7,7 +7,7 @@ use accesskit::{ Action, ActionHandler, ActionRequest, ActivationHandler, NodeBuilder, NodeId, Point, Role, Tree as TreeData, TreeUpdate, }; -use accesskit_consumer::{Node, Tree, TreeChangeHandler}; +use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler}; use jni::{ errors::Result, objects::{JClass, JObject}, @@ -82,8 +82,39 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { // TODO: live regions? } - fn node_updated(&mut self, _old_node: &Node, _new_node: &Node) { + fn node_updated(&mut self, old_node: &Node, new_node: &Node) { self.send_window_content_changed_if_needed(); + if filter(new_node) != FilterResult::Include { + return; + } + let old_wrapper = NodeWrapper(old_node); + let new_wrapper = NodeWrapper(new_node); + let old_text = old_wrapper.text(); + let new_text = new_wrapper.text(); + if old_text != new_text { + let id = self.node_id_map.get_or_create_java_id(new_node); + let old_text = self + .env + .new_string(old_text.unwrap_or_else(String::new)) + .unwrap(); + let new_text = self + .env + .new_string(new_text.unwrap_or_else(String::new)) + .unwrap(); + self.env + .call_static_method( + self.callback_class, + "sendTextChanged", + "(Landroid/view/View;ILjava/lang/String;Ljava/lang/String;)V", + &[ + self.host.into(), + id.into(), + (&old_text).into(), + (&new_text).into(), + ], + ) + .unwrap(); + } // TODO: other events } diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index c497b244..03640bf6 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -17,14 +17,6 @@ use crate::{filters::filter, util::*}; pub(crate) struct NodeWrapper<'a>(pub(crate) &'a Node<'a>); impl<'a> NodeWrapper<'a> { - fn name(&self) -> Option { - self.0.name() - } - - fn value(&self) -> Option { - self.0.value() - } - fn is_editable(&self) -> bool { self.0.is_text_input() && !self.0.is_read_only() } @@ -70,6 +62,22 @@ impl<'a> NodeWrapper<'a> { } } + fn content_description(&self) -> Option { + if self.0.role() == Role::Label { + return None; + } + self.0.name() + } + + pub(crate) fn text(&self) -> Option { + self.0.value().or_else(|| { + if self.0.role() != Role::Label { + return None; + } + self.0.name() + }) + } + fn class_name(&self) -> &str { match self.0.role() { Role::TextInput @@ -201,26 +209,22 @@ impl<'a> NodeWrapper<'a> { "(Z)V", &[self.is_selected().into()], )?; - if let Some(name) = self.name() { - let name = env.new_string(name)?; + if let Some(desc) = self.content_description() { + let desc = env.new_string(desc)?; env.call_method( jni_node, - if self.0.role() == Role::Label { - "setText" - } else { - "setContentDescription" - }, + "setContentDescription", "(Ljava/lang/CharSequence;)V", - &[(&name).into()], + &[(&desc).into()], )?; } - if let Some(value) = self.value() { - let value = env.new_string(value)?; + if let Some(text) = self.text() { + let text = env.new_string(text)?; env.call_method( jni_node, "setText", "(Ljava/lang/CharSequence;)V", - &[(&value).into()], + &[(&text).into()], )?; } let class_name = env.new_string(self.class_name())?; From cdfee0a33d935e264add0b0874003cc3325363bc Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Tue, 1 Oct 2024 18:07:55 -0500 Subject: [PATCH 20/46] Expose the text selection and send text selection change events --- platforms/android/classes.dex | Bin 6752 -> 7488 bytes .../java/dev/accesskit/android/Delegate.java | 18 +++++++++++++ platforms/android/src/adapter.rs | 24 +++++++++++++++++- platforms/android/src/node.rs | 19 ++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index efd4c2db4bee7ea4d16e1adddb3cc10a07aed1f2..c4468fe94adec6d1192ca607ceed84f83fa20156 100644 GIT binary patch literal 7488 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 { .unwrap(); let new_text = self .env - .new_string(new_text.unwrap_or_else(String::new)) + .new_string(new_text.clone().unwrap_or_else(String::new)) .unwrap(); self.env .call_static_method( @@ -115,6 +115,28 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { ) .unwrap(); } + if old_node.raw_text_selection() != new_node.raw_text_selection() { + if let Some((start, end)) = new_wrapper.text_selection() { + if let Some(text) = new_text { + let id = self.node_id_map.get_or_create_java_id(new_node); + let text = self.env.new_string(text).unwrap(); + self.env + .call_static_method( + self.callback_class, + "sendTextSelectionChanged", + "(Landroid/view/View;ILjava/lang/String;II)V", + &[ + self.host.into(), + id.into(), + (&text).into(), + (start as jint).into(), + (end as jint).into(), + ], + ) + .unwrap(); + } + } + } // TODO: other events } diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index 03640bf6..aaa49e0c 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -78,6 +78,15 @@ impl<'a> NodeWrapper<'a> { }) } + pub(crate) fn text_selection(&self) -> Option<(usize, usize)> { + self.0.text_selection().map(|range| { + ( + range.start().to_global_utf16_index(), + range.end().to_global_utf16_index(), + ) + }) + } + fn class_name(&self) -> &str { match self.0.role() { Role::TextInput @@ -218,6 +227,7 @@ impl<'a> NodeWrapper<'a> { &[(&desc).into()], )?; } + if let Some(text) = self.text() { let text = env.new_string(text)?; env.call_method( @@ -227,6 +237,15 @@ impl<'a> NodeWrapper<'a> { &[(&text).into()], )?; } + if let Some((start, end)) = self.text_selection() { + env.call_method( + jni_node, + "setTextSelection", + "(II)V", + &[(start as jint).into(), (end as jint).into()], + )?; + } + let class_name = env.new_string(self.class_name())?; env.call_method( jni_node, From c5edb2f8c1c1907708dd76aab06dccb31abf84ed Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 2 Oct 2024 16:32:26 -0500 Subject: [PATCH 21/46] Implement ACTION_SET_SELECTION --- consumer/src/node.rs | 6 +- consumer/src/tree.rs | 4 + platforms/android/classes.dex | Bin 7488 -> 8012 bytes .../java/dev/accesskit/android/Delegate.java | 14 +- platforms/android/src/adapter.rs | 189 +++++++++++++++--- platforms/android/src/inject.rs | 97 +++++++-- platforms/android/src/node.rs | 5 +- platforms/android/src/util.rs | 1 + 8 files changed, 263 insertions(+), 53 deletions(-) diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 2a14a998..780285a3 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -43,7 +43,7 @@ pub struct Node<'a> { } impl<'a> Node<'a> { - pub(crate) fn data(&self) -> &NodeData { + pub fn data(&self) -> &NodeData { &self.state.data } @@ -446,9 +446,7 @@ impl<'a> Node<'a> { && self.is_selected().is_none() } - // The future of the `Action` enum is undecided, so keep the following - // function private for now. - fn supports_action(&self, action: Action) -> bool { + pub fn supports_action(&self, action: Action) -> bool { self.data().supports_action(action) } diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 5047aa1e..c71e739a 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -205,6 +205,10 @@ impl State { self.is_host_focused } + pub fn focus_id_in_tree(&self) -> NodeId { + self.focus + } + pub fn focus_id(&self) -> Option { self.is_host_focused.then_some(self.focus) } diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index c4468fe94adec6d1192ca607ceed84f83fa20156..d73db035db9911ecae38abd5e3c524a86f99c8cb 100644 GIT binary patch literal 8012 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 { - env: &'a mut JNIEnv<'a>, - callback_class: &'a JClass<'a>, - host: &'a JObject<'a>, +struct AdapterChangeHandler<'a, 'b, 'c, 'd> { + env: &'a mut JNIEnv<'b>, + callback_class: &'a JClass<'c>, + host: &'a JObject<'d>, node_id_map: &'a mut NodeIdMap, sent_window_content_changed: bool, } -impl<'a> AdapterChangeHandler<'a> { +impl<'a, 'b, 'c, 'd> AdapterChangeHandler<'a, 'b, 'c, 'd> { fn new( - env: &'a mut JNIEnv<'a>, - callback_class: &'a JClass<'a>, - host: &'a JObject<'a>, + env: &'a mut JNIEnv<'b>, + callback_class: &'a JClass<'c>, + host: &'a JObject<'d>, node_id_map: &'a mut NodeIdMap, ) -> Self { Self { @@ -66,7 +71,7 @@ impl<'a> AdapterChangeHandler<'a> { } } -impl AdapterChangeHandler<'_> { +impl AdapterChangeHandler<'_, '_, '_, '_> { fn send_window_content_changed_if_needed(&mut self) { if self.sent_window_content_changed { return; @@ -76,7 +81,7 @@ impl AdapterChangeHandler<'_> { } } -impl TreeChangeHandler for AdapterChangeHandler<'_> { +impl TreeChangeHandler for AdapterChangeHandler<'_, '_, '_, '_> { fn node_added(&mut self, _node: &Node) { self.send_window_content_changed_if_needed(); // TODO: live regions? @@ -196,7 +201,7 @@ impl State { } } - fn get_full_tree(&mut self) -> Option<&Tree> { + fn get_full_tree(&mut self) -> Option<&mut Tree> { match self { Self::Inactive => None, Self::Placeholder(_) => None, @@ -205,6 +210,18 @@ impl State { } } +fn update_tree( + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + node_id_map: &mut NodeIdMap, + tree: &mut Tree, + update: TreeUpdate, +) { + let mut handler = AdapterChangeHandler::new(env, callback_class, host, node_id_map); + tree.update_and_process_changes(update, &mut handler); +} + #[derive(Default)] pub struct Adapter { node_id_map: NodeIdMap, @@ -217,12 +234,12 @@ impl Adapter { /// [`ActivationHandler::request_initial_tree`] initially returned `None`, /// the [`TreeUpdate`] returned by the provided function must contain /// a full tree. - pub fn update_if_active<'a>( - &'a mut self, + pub fn update_if_active( + &mut self, update_factory: impl FnOnce() -> TreeUpdate, - env: &'a mut JNIEnv<'a>, - callback_class: &'a JClass<'a>, - host: &'a JObject<'a>, + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, ) { match &mut self.state { State::Inactive => (), @@ -242,9 +259,14 @@ impl Adapter { self.state = State::Active(tree); } State::Active(tree) => { - let mut handler = - AdapterChangeHandler::new(env, callback_class, host, &mut self.node_id_map); - tree.update_and_process_changes(update_factory(), &mut handler); + update_tree( + env, + callback_class, + host, + &mut self.node_id_map, + tree, + update_factory(), + ); } } } @@ -304,21 +326,18 @@ impl Adapter { pub fn perform_action( &mut self, action_handler: &mut H, - _env: &mut JNIEnv, - _host: &JObject, virtual_view_id: jint, action: jint, - _arguments: &JObject, - ) -> Result { + ) -> bool { let Some(tree) = self.state.get_full_tree() else { - return Ok(false); + return false; }; let tree_state = tree.state(); let target = if virtual_view_id == HOST_VIEW_ID { tree_state.root_id() } else { let Some(accesskit_id) = self.node_id_map.get_accesskit_id(virtual_view_id) else { - return Ok(false); + return false; }; accesskit_id }; @@ -341,10 +360,120 @@ impl Adapter { data: None, }, _ => { - return Ok(false); + return false; } }; action_handler.do_action(request); - Ok(true) + true + } + + fn set_text_selection_common( + &mut self, + action_handler: &mut H, + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + virtual_view_id: jint, + selection_factory: F, + ) -> bool + where + for<'a> F: FnOnce(&'a Node<'a>) -> Option>, + { + let Some(tree) = self.state.get_full_tree() else { + return false; + }; + let tree_state = tree.state(); + let node = if virtual_view_id == HOST_VIEW_ID { + tree_state.root() + } else { + let Some(id) = self.node_id_map.get_accesskit_id(virtual_view_id) else { + return false; + }; + tree_state.node_by_id(id).unwrap() + }; + let target = node.id(); + // TalkBack expects the text selection change to take effect + // immediately, so we optimistically update the node. + // But don't be *too* optimistic. + if !node.supports_action(Action::SetTextSelection) { + return false; + } + let Some(range) = selection_factory(&node) else { + return false; + }; + let selection = range.to_text_selection(); + let mut builder = NodeBuilder::from(node.data()); + builder.set_text_selection(selection); + let new_node = builder.build(); + let update = TreeUpdate { + nodes: vec![(node.id(), new_node)], + tree: None, + focus: tree_state.focus_id_in_tree(), + }; + update_tree( + env, + callback_class, + host, + &mut self.node_id_map, + tree, + update, + ); + let request = ActionRequest { + target, + action: Action::SetTextSelection, + data: Some(ActionData::SetTextSelection(selection)), + }; + action_handler.do_action(request); + true + } + + #[allow(clippy::too_many_arguments)] + pub fn set_text_selection( + &mut self, + action_handler: &mut H, + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + virtual_view_id: jint, + start: jint, + end: jint, + ) -> bool { + self.set_text_selection_common( + action_handler, + env, + callback_class, + host, + virtual_view_id, + |node| { + let start = usize::try_from(start).ok()?; + let start = node.text_position_from_global_utf16_index(start)?; + let mut range = start.to_degenerate_range(); + let end = usize::try_from(end).ok()?; + let end = node.text_position_from_global_utf16_index(end)?; + range.set_end(end); + Some(range) + }, + ) + } + + pub fn collapse_text_selection( + &mut self, + action_handler: &mut H, + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + virtual_view_id: jint, + ) -> bool { + self.set_text_selection_common( + action_handler, + env, + callback_class, + host, + virtual_view_id, + |node| { + node.text_selection_focus() + .map(|pos| pos.to_degenerate_range()) + }, + ) } } diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index 74524a65..b07ee32f 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -61,21 +61,44 @@ impl InnerInjectingAdapter { .virtual_view_at_point(&mut *self.activation_handler, x, y) } - fn perform_action( + fn perform_action(&mut self, virtual_view_id: jint, action: jint) -> bool { + self.adapter + .perform_action(&mut *self.action_handler, virtual_view_id, action) + } + + fn set_text_selection( &mut self, env: &mut JNIEnv, + callback_class: &JClass, host: &JObject, virtual_view_id: jint, - action: jint, - arguments: &JObject, - ) -> Result { - self.adapter.perform_action( + start: jint, + end: jint, + ) -> bool { + self.adapter.set_text_selection( + &mut *self.action_handler, + env, + callback_class, + host, + virtual_view_id, + start, + end, + ) + } + + fn collapse_text_selection( + &mut self, + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + virtual_view_id: jint, + ) -> bool { + self.adapter.collapse_text_selection( &mut *self.action_handler, env, + callback_class, host, virtual_view_id, - action, - arguments, ) } } @@ -121,22 +144,55 @@ extern "system" fn populate_node_info( } extern "system" fn perform_action( - mut env: JNIEnv, + _env: JNIEnv, _class: JClass, adapter_handle: jlong, - host: JObject, virtual_view_id: jint, action: jint, - arguments: JObject, ) -> jboolean { let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { return JNI_FALSE; }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if inner_adapter - .perform_action(&mut env, &host, virtual_view_id, action, &arguments) - .unwrap() - { + if inner_adapter.perform_action(virtual_view_id, action) { + JNI_TRUE + } else { + JNI_FALSE + } +} + +extern "system" fn set_text_selection( + mut env: JNIEnv, + class: JClass, + adapter_handle: jlong, + host: JObject, + virtual_view_id: jint, + start: jint, + end: jint, +) -> jboolean { + let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { + return JNI_FALSE; + }; + let mut inner_adapter = inner_adapter.lock().unwrap(); + if inner_adapter.set_text_selection(&mut env, &class, &host, virtual_view_id, start, end) { + JNI_TRUE + } else { + JNI_FALSE + } +} + +extern "system" fn collapse_text_selection( + mut env: JNIEnv, + class: JClass, + adapter_handle: jlong, + host: JObject, + virtual_view_id: jint, +) -> jboolean { + let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { + return JNI_FALSE; + }; + let mut inner_adapter = inner_adapter.lock().unwrap(); + if inner_adapter.collapse_text_selection(&mut env, &class, &host, virtual_view_id) { JNI_TRUE } else { JNI_FALSE @@ -182,10 +238,19 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { }, NativeMethod { name: "performAction".into(), - sig: "(JLandroid/view/View;IILandroid/os/Bundle;)Z" - .into(), + sig: "(JII)Z".into(), fn_ptr: perform_action as *mut c_void, }, + NativeMethod { + name: "setTextSelection".into(), + sig: "(JLandroid/view/View;III)Z".into(), + fn_ptr: set_text_selection as *mut c_void, + }, + NativeMethod { + name: "collapseTextSelection".into(), + sig: "(JLandroid/view/View;I)Z".into(), + fn_ptr: collapse_text_selection as *mut c_void, + }, ], )?; env.new_global_ref(class) diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index aaa49e0c..511b86a9 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -8,7 +8,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE.chromium file. -use accesskit::{Live, Role, Toggled}; +use accesskit::{Action, Live, Role, Toggled}; use accesskit_consumer::Node; use jni::{errors::Result, objects::JObject, sys::jint, JNIEnv}; @@ -271,6 +271,9 @@ impl<'a> NodeWrapper<'a> { if can_focus { add_action(env, jni_node, ACTION_FOCUS)?; } + if self.0.supports_action(Action::SetTextSelection) { + add_action(env, jni_node, ACTION_SET_SELECTION)?; + } let live = match self.0.live() { Live::Off => LIVE_REGION_NONE, diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 170e490f..57759272 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -14,6 +14,7 @@ use std::collections::HashMap; pub(crate) const ACTION_FOCUS: jint = 1 << 0; pub(crate) const ACTION_CLICK: jint = 1 << 4; +pub(crate) const ACTION_SET_SELECTION: jint = 1 << 17; pub(crate) const EVENT_VIEW_FOCUSED: jint = 1 << 3; pub(crate) const EVENT_VIEW_HOVER_ENTER: jint = 1 << 7; pub(crate) const EVENT_VIEW_HOVER_EXIT: jint = 1 << 8; From 5263e253e80a72399441f8446deccfa513d9eb19 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 3 Oct 2024 13:45:09 -0500 Subject: [PATCH 22/46] First attempt at traversing text by movement granularity --- consumer/src/text.rs | 36 +++ platforms/android/classes.dex | Bin 8012 -> 9136 bytes .../java/dev/accesskit/android/Delegate.java | 36 ++- platforms/android/src/adapter.rs | 219 +++++++++++++++--- platforms/android/src/inject.rs | 71 +++++- platforms/android/src/node.rs | 16 +- platforms/android/src/util.rs | 6 + 7 files changed, 342 insertions(+), 42 deletions(-) diff --git a/consumer/src/text.rs b/consumer/src/text.rs index 06863f99..77d38989 100644 --- a/consumer/src/text.rs +++ b/consumer/src/text.rs @@ -192,6 +192,10 @@ pub struct Position<'a> { } impl<'a> Position<'a> { + pub fn to_raw(self) -> WeakPosition { + self.inner.downgrade() + } + pub fn inner_node(&self) -> &Node { &self.inner.node } @@ -223,6 +227,14 @@ impl<'a> Position<'a> { self.is_document_end() || self.inner.is_paragraph_end() } + pub fn is_paragraph_separator(&self) -> bool { + if self.is_document_end() { + return false; + } + let next = self.forward_to_character_end(); + !next.is_document_end() && next.is_paragraph_end() + } + pub fn is_page_start(&self) -> bool { self.is_document_start() } @@ -292,6 +304,20 @@ impl<'a> Position<'a> { lines_before_current } + pub fn biased_to_start(&self) -> Self { + Self { + root_node: self.root_node, + inner: self.inner.biased_to_start(&self.root_node), + } + } + + pub fn biased_to_end(&self) -> Self { + Self { + root_node: self.root_node, + inner: self.inner.biased_to_end(&self.root_node), + } + } + pub fn forward_to_character_start(&self) -> Self { let pos = self.inner.biased_to_start(&self.root_node); Self { @@ -905,6 +931,16 @@ impl<'a> Node<'a> { }) } + pub fn text_selection_anchor(&self) -> Option { + self.data().text_selection().map(|selection| { + let anchor = InnerPosition::clamped_upgrade(self.tree_state, selection.anchor).unwrap(); + Position { + root_node: *self, + inner: anchor, + } + }) + } + pub fn text_selection_focus(&self) -> Option { self.data().text_selection().map(|selection| { let focus = InnerPosition::clamped_upgrade(self.tree_state, selection.focus).unwrap(); diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index d73db035db9911ecae38abd5e3c524a86f99c8cb..b2c3445fb7856cc3e24abdf561dc2c36664ea191 100644 GIT binary patch literal 9136 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 literal 8012 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 + fn set_text_selection_common( &mut self, action_handler: &mut H, env: &mut JNIEnv, @@ -375,20 +375,16 @@ impl Adapter { host: &JObject, virtual_view_id: jint, selection_factory: F, - ) -> bool + ) -> Option where - for<'a> F: FnOnce(&'a Node<'a>) -> Option>, + for<'a> F: FnOnce(&'a Node<'a>) -> Option<(TextPosition<'a>, TextPosition<'a>, Extra)>, { - let Some(tree) = self.state.get_full_tree() else { - return false; - }; + let tree = self.state.get_full_tree()?; let tree_state = tree.state(); let node = if virtual_view_id == HOST_VIEW_ID { tree_state.root() } else { - let Some(id) = self.node_id_map.get_accesskit_id(virtual_view_id) else { - return false; - }; + let id = self.node_id_map.get_accesskit_id(virtual_view_id)?; tree_state.node_by_id(id).unwrap() }; let target = node.id(); @@ -396,12 +392,13 @@ impl Adapter { // immediately, so we optimistically update the node. // But don't be *too* optimistic. if !node.supports_action(Action::SetTextSelection) { - return false; + return None; } - let Some(range) = selection_factory(&node) else { - return false; + let (anchor, focus, extra) = selection_factory(&node)?; + let selection = TextSelection { + anchor: anchor.to_raw(), + focus: focus.to_raw(), }; - let selection = range.to_text_selection(); let mut builder = NodeBuilder::from(node.data()); builder.set_text_selection(selection); let new_node = builder.build(); @@ -424,7 +421,7 @@ impl Adapter { data: Some(ActionData::SetTextSelection(selection)), }; action_handler.do_action(request); - true + Some(extra) } #[allow(clippy::too_many_arguments)] @@ -435,8 +432,8 @@ impl Adapter { callback_class: &JClass, host: &JObject, virtual_view_id: jint, - start: jint, - end: jint, + anchor: jint, + focus: jint, ) -> bool { self.set_text_selection_common( action_handler, @@ -445,15 +442,14 @@ impl Adapter { host, virtual_view_id, |node| { - let start = usize::try_from(start).ok()?; - let start = node.text_position_from_global_utf16_index(start)?; - let mut range = start.to_degenerate_range(); - let end = usize::try_from(end).ok()?; - let end = node.text_position_from_global_utf16_index(end)?; - range.set_end(end); - Some(range) + let anchor = usize::try_from(anchor).ok()?; + let anchor = node.text_position_from_global_utf16_index(anchor)?; + let focus = usize::try_from(focus).ok()?; + let focus = node.text_position_from_global_utf16_index(focus)?; + Some((anchor, focus, ())) }, ) + .is_some() } pub fn collapse_text_selection( @@ -465,15 +461,184 @@ impl Adapter { virtual_view_id: jint, ) -> bool { self.set_text_selection_common( + action_handler, + env, + callback_class, + host, + virtual_view_id, + |node| node.text_selection_focus().map(|pos| (pos, pos, ())), + ) + .is_some() + } + + #[allow(clippy::too_many_arguments)] + pub fn traverse_text( + &mut self, + action_handler: &mut H, + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + virtual_view_id: jint, + granularity: jint, + forward: bool, + extend_selection: bool, + ) -> bool { + let Some((segment_start, segment_end)) = self.set_text_selection_common( action_handler, env, callback_class, host, virtual_view_id, |node| { - node.text_selection_focus() - .map(|pos| pos.to_degenerate_range()) + let current = node.text_selection_focus().unwrap_or_else(|| { + let range = node.document_range(); + if forward { + range.start() + } else { + range.end() + } + }); + if (forward && current.is_document_end()) + || (!forward && current.is_document_start()) + { + return None; + } + let current = if forward { + current.biased_to_start() + } else { + current.biased_to_end() + }; + let (segment_start, segment_end) = match granularity { + MOVEMENT_GRANULARITY_CHARACTER => { + if forward { + (current, current.forward_to_character_end()) + } else { + (current.backward_to_character_start(), current) + } + } + MOVEMENT_GRANULARITY_WORD => { + if forward { + let start = if current.is_word_start() { + current + } else { + let start = current.forward_to_word_start(); + if start.is_document_end() { + return None; + } + start + }; + (start, start.forward_to_word_end()) + } else { + let end = if current.is_line_end() || current.is_word_start() { + current + } else { + let end = current.backward_to_word_start().biased_to_end(); + if end.is_document_start() { + return None; + } + end + }; + (end.backward_to_word_start(), end) + } + } + MOVEMENT_GRANULARITY_LINE => { + if forward { + let start = if current.is_line_start() { + current + } else { + let start = current.forward_to_line_start(); + if start.is_document_end() { + return None; + } + start + }; + (start, start.forward_to_line_end()) + } else { + let end = if current.is_line_end() { + current + } else { + let end = current.backward_to_line_start().biased_to_end(); + if end.is_document_start() { + return None; + } + end + }; + (end.backward_to_line_start(), end) + } + } + MOVEMENT_GRANULARITY_PARAGRAPH => { + if forward { + let mut start = current; + while start.is_paragraph_separator() { + start = start.forward_to_paragraph_start(); + } + if start.is_document_end() { + return None; + } + let mut end = start.forward_to_paragraph_end(); + let prev = end.backward_to_character_start(); + if prev.is_paragraph_separator() { + end = prev; + } + (start, end) + } else { + let mut end = current; + while !end.is_document_start() + && end.backward_to_character_start().is_paragraph_separator() + { + end = end.backward_to_character_start(); + } + if end.is_document_start() { + return None; + } + (end.backward_to_paragraph_start(), end) + } + } + _ => { + return None; + } + }; + if segment_start == segment_end { + return None; + } + let focus = if forward { segment_end } else { segment_start }; + let anchor = if extend_selection { + node.text_selection_anchor().unwrap_or({ + if forward { + segment_start + } else { + segment_end + } + }) + } else { + focus + }; + Some(( + anchor, + focus, + ( + segment_start.to_global_utf16_index(), + segment_end.to_global_utf16_index(), + ), + )) }, + ) else { + return false; + }; + env.call_static_method( + callback_class, + "sendTextTraversed", + "(Landroid/view/View;IIZII)V", + &[ + host.into(), + virtual_view_id.into(), + granularity.into(), + forward.into(), + (segment_start as jint).into(), + (segment_end as jint).into(), + ], ) + .unwrap(); + true } } diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index b07ee32f..930e7197 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -72,8 +72,8 @@ impl InnerInjectingAdapter { callback_class: &JClass, host: &JObject, virtual_view_id: jint, - start: jint, - end: jint, + anchor: jint, + focus: jint, ) -> bool { self.adapter.set_text_selection( &mut *self.action_handler, @@ -81,8 +81,8 @@ impl InnerInjectingAdapter { callback_class, host, virtual_view_id, - start, - end, + anchor, + focus, ) } @@ -101,6 +101,29 @@ impl InnerInjectingAdapter { virtual_view_id, ) } + + #[allow(clippy::too_many_arguments)] + fn traverse_text( + &mut self, + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + virtual_view_id: jint, + granularity: jint, + forward: bool, + extend_selection: bool, + ) -> bool { + self.adapter.traverse_text( + &mut *self.action_handler, + env, + callback_class, + host, + virtual_view_id, + granularity, + forward, + extend_selection, + ) + } } static NEXT_HANDLE: AtomicI64 = AtomicI64::new(0); @@ -167,14 +190,14 @@ extern "system" fn set_text_selection( adapter_handle: jlong, host: JObject, virtual_view_id: jint, - start: jint, - end: jint, + anchor: jint, + focus: jint, ) -> jboolean { let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { return JNI_FALSE; }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if inner_adapter.set_text_selection(&mut env, &class, &host, virtual_view_id, start, end) { + if inner_adapter.set_text_selection(&mut env, &class, &host, virtual_view_id, anchor, focus) { JNI_TRUE } else { JNI_FALSE @@ -199,6 +222,35 @@ extern "system" fn collapse_text_selection( } } +extern "system" fn traverse_text( + mut env: JNIEnv, + class: JClass, + adapter_handle: jlong, + host: JObject, + virtual_view_id: jint, + granularity: jint, + forward: jboolean, + extend_selection: jboolean, +) -> jboolean { + let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { + return JNI_FALSE; + }; + let mut inner_adapter = inner_adapter.lock().unwrap(); + if inner_adapter.traverse_text( + &mut env, + &class, + &host, + virtual_view_id, + granularity, + forward == JNI_TRUE, + extend_selection == JNI_TRUE, + ) { + JNI_TRUE + } else { + JNI_FALSE + } +} + fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { static CLASS: OnceCell = OnceCell::new(); let global = CLASS.get_or_try_init(|| { @@ -251,6 +303,11 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { sig: "(JLandroid/view/View;I)Z".into(), fn_ptr: collapse_text_selection as *mut c_void, }, + NativeMethod { + name: "traverseText".into(), + sig: "(JLandroid/view/View;IIZZ)Z".into(), + fn_ptr: traverse_text as *mut c_void, + }, ], )?; env.new_global_ref(class) diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index 511b86a9..1452c799 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -8,7 +8,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE.chromium file. -use accesskit::{Action, Live, Role, Toggled}; +use accesskit::{Live, Role, Toggled}; use accesskit_consumer::Node; use jni::{errors::Result, objects::JObject, sys::jint, JNIEnv}; @@ -271,8 +271,20 @@ impl<'a> NodeWrapper<'a> { if can_focus { add_action(env, jni_node, ACTION_FOCUS)?; } - if self.0.supports_action(Action::SetTextSelection) { + if self.0.supports_text_ranges() { add_action(env, jni_node, ACTION_SET_SELECTION)?; + add_action(env, jni_node, ACTION_NEXT_AT_MOVEMENT_GRANULARITY)?; + add_action(env, jni_node, ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY)?; + env.call_method( + jni_node, + "setMovementGranularities", + "(I)V", + &[(MOVEMENT_GRANULARITY_CHARACTER + | MOVEMENT_GRANULARITY_WORD + | MOVEMENT_GRANULARITY_LINE + | MOVEMENT_GRANULARITY_PARAGRAPH) + .into()], + )?; } let live = match self.0.live() { diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 57759272..262a27b4 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -14,6 +14,8 @@ use std::collections::HashMap; pub(crate) const ACTION_FOCUS: jint = 1 << 0; pub(crate) const ACTION_CLICK: jint = 1 << 4; +pub(crate) const ACTION_NEXT_AT_MOVEMENT_GRANULARITY: jint = 1 << 8; +pub(crate) const ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: jint = 1 << 9; pub(crate) const ACTION_SET_SELECTION: jint = 1 << 17; pub(crate) const EVENT_VIEW_FOCUSED: jint = 1 << 3; pub(crate) const EVENT_VIEW_HOVER_ENTER: jint = 1 << 7; @@ -23,6 +25,10 @@ pub(crate) const HOST_VIEW_ID: jint = -1; pub(crate) const LIVE_REGION_NONE: jint = 0; pub(crate) const LIVE_REGION_POLITE: jint = 1; pub(crate) const LIVE_REGION_ASSERTIVE: jint = 2; +pub(crate) const MOVEMENT_GRANULARITY_CHARACTER: jint = 1 << 0; +pub(crate) const MOVEMENT_GRANULARITY_WORD: jint = 1 << 1; +pub(crate) const MOVEMENT_GRANULARITY_LINE: jint = 1 << 2; +pub(crate) const MOVEMENT_GRANULARITY_PARAGRAPH: jint = 1 << 3; #[derive(Default)] pub(crate) struct NodeIdMap { From 3729d8c4e44ec335b6e420f5602cf8218fa299be Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Tue, 22 Oct 2024 19:19:07 -0500 Subject: [PATCH 23/46] Implement ACTION_CLEAR_ACCESSIBILITY_FOCUS, in case it helps --- platforms/android/classes.dex | Bin 9136 -> 9208 bytes .../java/dev/accesskit/android/Delegate.java | 7 +++++++ 2 files changed, 7 insertions(+) diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index b2c3445fb7856cc3e24abdf561dc2c36664ea191..eda97a333703ec2a097933c832c2b0af347a78c8 100644 GIT binary patch delta 2450 zcmZA3e@v8h90%~v_u)YA;eK;)9C+Npg<_%{KNL_ggd8U3CQ%Bu{0En^hEmR)T1SdW zrY`n%R!WQ1tPC59Q&%Kvv}u^>+6s-#Y^HAN*rZL$toP^Pi{5zP^}YA=`8+(&_j#Us z?r1N&SC(3{WVIg3olH~4Y>TZ7CEWbW^`IcMqw9lb7Muw8zwLPEUUO=kmKSWEk`wOr z5(RXkm;|C&h=VMsf(@__df+zn!vI8ius!Ht2NxuO7krQkV<8jD;Ca{tEpQZWKq!%@ zCXsY%K{yG&HxDKov$>BMr6ds*z#>=<8{vKU9KL`84G3jaWS z3Q<1Hfh7=z_h1hkf|Jk-f5HG*{6u4(#V`>HU=BP7OJN1P4$ZI=K7nI!3eLl2xC0cx zn+W2;4>?d6ke9X1MFVcMh)4i07T|mzb*svz_(ydMvK@7ew~-%5WzFC{;Fm^&8(k+} z@x~>tyHpOK9;b2!GS~U2fq3aMO$zg-*;vK(p%B_RDi1WLX}q{PsKej4!jO4!v|{|Y8dq`5%uj6nXkk&wWF4Z`d;KB)N%8u z4S9yj$B>^;xf{7y<@52giDs&mD`=Fc+>iXE$~?JJl?jjfaK42rH=Hf;9dAbOiwbj% zZ@~66JZJ#~ERLZU!ZS?6{WkO|b*RBNjhD!NH~LQ1=NraLWWNvn4AtkG%M1FG9v)x- z8}8QK z(F#W;l8vIUm4>uc9@O*2c6n0|ie1tg8x*ZFJ9eD-R90a;Am50cAdbod7{8VmW3$9* zX|`mEbF$FVBd*D4Yw7qq*!ywp$we+_rll3B@_>ix9norb zOJ>5_bk(abo<)wFq24IsA3RxZInSP?O?S&$dy=nocrP*YAMQmMb0C^4KeBtg^RYz~ zdR4+pJXyZnyv&ss>>jZ|-n0kBd}(!fynIwXdVOdPIW(Iw7hYd1iyWRLK9bR`oQFBt za2xr@m`|M}LA)q8I)b83eui=PWPp>s_{bnO1jYQ*HHXZI!RO zdXwgr=i+|q*yG-937_?aM0=tT_q{@#NFvQIyqa+3u5U2421G-vsXHK=0)0H3O~G)3 zhXG#Ql8Rw34^6z<=*Q5@Lq89#JaqcSW{os+XR6R|`8XTQSFqlgCd_-&L|8wKa4${h zEkR*!Oc(m$bYX57EA;lU!+%gB+Ea`RzdwL;!@_TojsVe@!1+`}J`<78N93O)^5sW4 zOxM&(aT*!R&>hCt9oF(tn>(VOuIdX%)W@lM&4~JVRbMfpKEbHti+SCM#zgf8P4d^Y zwF}qaxfJ;P!Z)$;$)W}D9}6=7XJkS)BOBKRnZJYhBQFZubDgIjgLQuE^T#274)W>c H;oSZQartXQ delta 2349 zcmZA33v5$W7zgn0+;xNAt=(;JS@-IC*KQdLY=beF8?QM~zySg_QHHM|8UfJ_6E|L| z6T$}KWFEsyL`)c2q(cY)=Qsng}n#dT50<2*Foy3ci6e@I9P|AK?OY!&T^ohv0G(O@#ow z1$*HJWTujn0;xo+U?+4yPs{C$;6<#eAM|vhJeUmg;T3odmcg6wHq^mdcn{tO4!htp z*avNJ01m@(I0>C_9xlNZh(Is&!-I56AvJ?21$q(stMn2jCc-f(!5) zJOnk9Cy!}sDnoM2tI)`@B>_d-{B_w4gZ1a!3hE%jDm5nz(YZL7GXKO1DjwM z9D#H27bsao9w>!*un3mHD%c8pAOvCf8LmM;C|)8v7%&nhz${n*FF_5g_T~^ZA#8*F z&;h6699)7w;kK7wRW?r!xrh>ph=JIWK;)yS+hjh09Ic0ulTla1F}yXdXo?sC|7bXz zMR4E~wVD^JwI9!qpNJmtZG4*`&b^vpzkhK079hGci#P6o{!mk31Q5 zTr66GJVoXj3AB3lsqWw$I-(d`Ph>Iq+*#5QX|IRSW=7k*E|1tU_vR_6YiGKDnADfuRUgrzZuHX%cqgWkpN4t)9 zCl;|LZcQp;&AcqBi?#B2+qC?H7<~^$a+1?AI%xw^pxsTi+C)4DiArrnDHV6Awldoa zUTZtXU$Fag@%+=sgg_{jYR8PH6D%1^u}}r7=F-KoNa0`FbCsWTe$k$#bnE=Cy;8ZR z^YY~M@>`fN9*5KoTYy$IA1m>-E!evw`)VtmAgy??Zy1RmqFXv|PEJ?u==_W1bkF_h zC<*f)jl#_NF*^S>*`221euzWgLY78YqIt@&i(ZH3W*X1e{7mI@GT`dedR*WMaK`#kKIXveLtJM^#nN z<)!sYm6}L(b)(|sBU~4c^tnDyVq17~c3LpvW$*5_Y%o}pv0F4-ve2}N=BU^`E1F)> zeCtIM79Bf8IwYEi*u9^{HY=o9?;A`Fc|SFl`*cbP=cyEapmiQWd$o~UO`euFr diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 1263340f..0189a337 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -200,6 +200,13 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { host.invalidate(); sendEventInternal(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); return true; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + if (accessibilityFocus == virtualViewId) { + accessibilityFocus = AccessibilityNodeProvider.HOST_VIEW_ID; + } + host.invalidate(); + sendEventInternal(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + return true; case AccessibilityNodeInfo.ACTION_SET_SELECTION: if (!(arguments != null && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT) && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT))) { return Delegate.collapseTextSelection(adapterHandle, host, virtualViewId); From c4c6443265c98454f3054e97da0464e1ca4efb37 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 23 Oct 2024 11:14:28 -0500 Subject: [PATCH 24/46] Attempts to make TalkBack movement by granularity work reliably when editing --- consumer/src/tree.rs | 18 ++++++++------ platforms/android/classes.dex | Bin 9208 -> 9456 bytes .../java/dev/accesskit/android/Delegate.java | 22 ++++++++++++++---- platforms/android/src/adapter.rs | 15 +++++++++++- platforms/android/src/inject.rs | 17 ++++++++++++++ 5 files changed, 60 insertions(+), 12 deletions(-) diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index c71e739a..cbe411a7 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -209,6 +209,10 @@ impl State { self.focus } + pub fn focus_in_tree(&self) -> Node<'_> { + self.node_by_id(self.focus_id_in_tree()).unwrap() + } + pub fn focus_id(&self) -> Option { self.is_host_focused.then_some(self.focus) } @@ -295,13 +299,10 @@ impl Tree { let node = self.state.node_by_id(*id).unwrap(); handler.node_added(&node); } - for id in &changes.updated_node_ids { - let old_node = old_state.node_by_id(*id).unwrap(); - let new_node = self.state.node_by_id(*id).unwrap(); - handler.node_updated(&old_node, &new_node); - } if old_state.focus_id() != self.state.focus_id() { let old_node = old_state.focus(); + let new_node = self.state.focus(); + handler.focus_moved(old_node.as_ref(), new_node.as_ref()); if let Some(old_node) = &old_node { let id = old_node.id(); if !changes.updated_node_ids.contains(&id) @@ -312,7 +313,6 @@ impl Tree { } } } - let new_node = self.state.focus(); if let Some(new_node) = &new_node { let id = new_node.id(); if !changes.added_node_ids.contains(&id) && !changes.updated_node_ids.contains(&id) @@ -322,7 +322,11 @@ impl Tree { } } } - handler.focus_moved(old_node.as_ref(), new_node.as_ref()); + } + for id in &changes.updated_node_ids { + let old_node = old_state.node_by_id(*id).unwrap(); + let new_node = self.state.node_by_id(*id).unwrap(); + handler.node_updated(&old_node, &new_node); } for id in &changes.removed_node_ids { let node = old_state.node_by_id(*id).unwrap(); diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index eda97a333703ec2a097933c832c2b0af347a78c8..d0fe2cda207ce3431a38da27772d9025c2bd98af 100644 GIT binary patch literal 9456 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 diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 0189a337..ce5b6035 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -158,6 +158,7 @@ public void run() { } private static native boolean populateNodeInfo(long adapterHandle, View host, int screenX, int screenY, int virtualViewId, AccessibilityNodeInfo nodeInfo); + private static native int getInputFocus(long adapterHandle); private static native boolean performAction(long adapterHandle, int virtualViewId, int action); private static native boolean setTextSelection(long adapterHandle, View host, int virtualViewId, int anchor, int focus); private static native boolean collapseTextSelection(long adapterHandle, View host, int virtualViewId); @@ -182,6 +183,7 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { nodeInfo.recycle(); return null; } + nodeInfo.setVisibleToUser(true); if (virtualViewId == accessibilityFocus) { nodeInfo.setAccessibilityFocused(true); nodeInfo.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); @@ -234,11 +236,23 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { @Override public AccessibilityNodeInfo findFocus(int focusType) { - if (focusType != AccessibilityNodeInfo.FOCUS_ACCESSIBILITY - || accessibilityFocus == HOST_VIEW_ID) { - return null; + switch (focusType) { + case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: { + AccessibilityNodeInfo result = createAccessibilityNodeInfo(accessibilityFocus); + if (result != null && result.isAccessibilityFocused()) { + return result; + } + break; + } + case AccessibilityNodeInfo.FOCUS_INPUT: { + AccessibilityNodeInfo result = createAccessibilityNodeInfo(getInputFocus(adapterHandle)); + if (result != null && result.isFocused()) { + return result; + } + break; + } } - return createAccessibilityNodeInfo(accessibilityFocus); + return null; } }; } diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 975dcc22..8bb494ba 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -120,7 +120,10 @@ impl TreeChangeHandler for AdapterChangeHandler<'_, '_, '_, '_> { ) .unwrap(); } - if old_node.raw_text_selection() != new_node.raw_text_selection() { + if old_node.raw_text_selection() != new_node.raw_text_selection() + || (new_node.raw_text_selection().is_some() + && old_node.is_focused() != new_node.is_focused()) + { if let Some((start, end)) = new_wrapper.text_selection() { if let Some(text) = new_text { let id = self.node_id_map.get_or_create_java_id(new_node); @@ -308,6 +311,16 @@ impl Adapter { Ok(true) } + pub fn input_focus( + &mut self, + activation_handler: &mut H, + ) -> jint { + let tree = self.state.get_or_init_tree(activation_handler); + let tree_state = tree.state(); + let node = tree_state.focus_in_tree(); + self.node_id_map.get_or_create_java_id(&node) + } + pub fn virtual_view_at_point( &mut self, activation_handler: &mut H, diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index 930e7197..c4672af4 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -61,6 +61,10 @@ impl InnerInjectingAdapter { .virtual_view_at_point(&mut *self.activation_handler, x, y) } + fn input_focus(&mut self) -> jint { + self.adapter.input_focus(&mut *self.activation_handler) + } + fn perform_action(&mut self, virtual_view_id: jint, action: jint) -> bool { self.adapter .perform_action(&mut *self.action_handler, virtual_view_id, action) @@ -166,6 +170,14 @@ extern "system" fn populate_node_info( } } +extern "system" fn get_input_focus(_env: JNIEnv, _class: JClass, adapter_handle: jlong) -> jint { + let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { + return HOST_VIEW_ID; + }; + let mut inner_adapter = inner_adapter.lock().unwrap(); + inner_adapter.input_focus() +} + extern "system" fn perform_action( _env: JNIEnv, _class: JClass, @@ -288,6 +300,11 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { .into(), fn_ptr: populate_node_info as *mut c_void, }, + NativeMethod { + name: "getInputFocus".into(), + sig: "(J)I".into(), + fn_ptr: get_input_focus as *mut c_void, + }, NativeMethod { name: "performAction".into(), sig: "(JII)Z".into(), From f740f7a8645a9d72381472ba89457d56da9c0c8d Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 21 Nov 2024 06:10:15 -0600 Subject: [PATCH 25/46] Switch to assuming GameActivity in the winit Android backend --- platforms/winit/src/platform_impl/android.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platforms/winit/src/platform_impl/android.rs b/platforms/winit/src/platform_impl/android.rs index dad0988e..667554a7 100644 --- a/platforms/winit/src/platform_impl/android.rs +++ b/platforms/winit/src/platform_impl/android.rs @@ -31,8 +31,8 @@ impl Adapter { let view = env .get_field( &activity, - "mNativeContentView", - "Landroid/app/NativeActivity$NativeContentView;", + "mSurfaceView", + "Lcom/google/androidgamesdk/GameActivity$InputEnabledSurfaceView;", ) .unwrap() .l() From 6ad5a1fcc5a4cadf857a1500f688281dbe2a251f Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Tue, 24 Dec 2024 11:09:33 -0600 Subject: [PATCH 26/46] fixes after rebase --- common/src/lib.rs | 14 ++++++++++++++ platforms/android/Cargo.toml | 4 ++-- platforms/android/src/adapter.rs | 14 +++++--------- platforms/android/src/node.rs | 12 ++---------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index 93b8e167..e023c715 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1382,6 +1382,20 @@ impl From for FrozenNode { } } +impl From<&FrozenNode> for Node { + fn from(node: &FrozenNode) -> Self { + Self { + role: node.role, + actions: node.actions, + flags: node.flags, + properties: Properties { + indices: node.properties.indices, + values: node.properties.values.to_vec(), + }, + } + } +} + impl FrozenNode { #[inline] pub fn role(&self) -> Role { diff --git a/platforms/android/Cargo.toml b/platforms/android/Cargo.toml index d5b5d994..9192d749 100644 --- a/platforms/android/Cargo.toml +++ b/platforms/android/Cargo.toml @@ -15,8 +15,8 @@ rust-version.workspace = true embedded-dex = [] [dependencies] -accesskit = { version = "0.16.0", path = "../../common" } -accesskit_consumer = { version = "0.24.0", path = "../../consumer" } +accesskit = { version = "0.17.1", path = "../../common" } +accesskit_consumer = { version = "0.26.0", path = "../../consumer" } jni = "0.21.1" log = "0.4.17" once_cell = "1.17.1" diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 8bb494ba..ea450a41 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -9,7 +9,7 @@ // found in the LICENSE.chromium file. use accesskit::{ - Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, NodeBuilder, NodeId, + Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, Node as NodeData, NodeId, Point, Role, TextSelection, Tree as TreeData, TreeUpdate, }; use accesskit_consumer::{FilterResult, Node, TextPosition, Tree, TreeChangeHandler}; @@ -187,10 +187,7 @@ impl State { Some(initial_state) => Self::Active(Tree::new(initial_state, true)), None => { let placeholder_update = TreeUpdate { - nodes: vec![( - PLACEHOLDER_ROOT_ID, - NodeBuilder::new(Role::Window).build(), - )], + nodes: vec![(PLACEHOLDER_ROOT_ID, NodeData::new(Role::Window))], tree: Some(TreeData::new(PLACEHOLDER_ROOT_ID)), focus: PLACEHOLDER_ROOT_ID, }; @@ -361,7 +358,7 @@ impl Adapter { if node.is_focusable() && !node.is_focused() && !node.is_clickable() { Action::Focus } else { - Action::Default + Action::Click } }, target, @@ -412,9 +409,8 @@ impl Adapter { anchor: anchor.to_raw(), focus: focus.to_raw(), }; - let mut builder = NodeBuilder::from(node.data()); - builder.set_text_selection(selection); - let new_node = builder.build(); + let mut new_node = NodeData::from(node.data()); + new_node.set_text_selection(selection); let update = TreeUpdate { nodes: vec![(node.id(), new_node)], tree: None, diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index 1452c799..dca3231d 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -63,19 +63,11 @@ impl<'a> NodeWrapper<'a> { } fn content_description(&self) -> Option { - if self.0.role() == Role::Label { - return None; - } - self.0.name() + self.0.label() } pub(crate) fn text(&self) -> Option { - self.0.value().or_else(|| { - if self.0.role() != Role::Label { - return None; - } - self.0.name() - }) + self.0.value() } pub(crate) fn text_selection(&self) -> Option<(usize, usize)> { From 202d8428c1a4d26829a24cf8bcf7a9e52b7956d4 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 29 Dec 2024 18:28:55 -0600 Subject: [PATCH 27/46] Clean up after some of the more desperate attempts to fix the TalkBack text traversal problem, which now appears to be a bug in TalkBack. But also be more consistent about only exposing text selection when the node is focused, because that does seem to be what TalkBack expects. --- consumer/src/tree.rs | 14 +++++++------- platforms/android/classes.dex | Bin 9456 -> 9532 bytes .../java/dev/accesskit/android/Delegate.java | 1 - platforms/android/src/node.rs | 3 +++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index cbe411a7..c45ad7fd 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -299,10 +299,13 @@ impl Tree { let node = self.state.node_by_id(*id).unwrap(); handler.node_added(&node); } + for id in &changes.updated_node_ids { + let old_node = old_state.node_by_id(*id).unwrap(); + let new_node = self.state.node_by_id(*id).unwrap(); + handler.node_updated(&old_node, &new_node); + } if old_state.focus_id() != self.state.focus_id() { let old_node = old_state.focus(); - let new_node = self.state.focus(); - handler.focus_moved(old_node.as_ref(), new_node.as_ref()); if let Some(old_node) = &old_node { let id = old_node.id(); if !changes.updated_node_ids.contains(&id) @@ -313,6 +316,7 @@ impl Tree { } } } + let new_node = self.state.focus(); if let Some(new_node) = &new_node { let id = new_node.id(); if !changes.added_node_ids.contains(&id) && !changes.updated_node_ids.contains(&id) @@ -322,11 +326,7 @@ impl Tree { } } } - } - for id in &changes.updated_node_ids { - let old_node = old_state.node_by_id(*id).unwrap(); - let new_node = self.state.node_by_id(*id).unwrap(); - handler.node_updated(&old_node, &new_node); + handler.focus_moved(old_node.as_ref(), new_node.as_ref()); } for id in &changes.removed_node_ids { let node = old_state.node_by_id(*id).unwrap(); diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index d0fe2cda207ce3431a38da27772d9025c2bd98af..7f61cd72c8d9110fff33cd86fb3cbddf9b17472f 100644 GIT binary patch literal 9532 zcmb7K4|J2)b-(YEELpN_%kqC4%xB?%`De;H_;bk&p&ti_*c$Oz4zpwfBu=jKXv8o z`cGYmAO3dWE}|3>&5dmIfymcf3(*B|EZ2b#G@^FUM{Gn(Kra;$JqUWvK~xNS(na(} z1E`zmhc!e$0p10iwL}|$G2k)a0`LvsE#M09kH8Os9|QjkybJsr@E^eYz<&e30<=1e z11tx+fG98rd=j_}c6O|6gLrhfhE93;9g)Suot)=I1HG;Jn%8#Dd0Kab>L0lZ-IXR-UI#@s9lB_fMMVW z@Hp@ya1r<_@NM7+z`KB^V{D)a*bEE+_XFcV7MKT~20jhE0=xy;wgNkVLEs>897qGaH%owW0NatUl?dB~)Yc%iG=!88ra=ggbO2q+4=e{z3WO4& z6~IcM6<7tV2G#&;fpx%opbg+Uxf>V&P!{rarP_)^sI~```R_qqs`zW*%M}090{M0D zUdT~`#5NV6C`0*T9;KM)or>o|=DG8@_$T@Q1jBNasFYVKo)c50_+s!Z=bW*e{d11^ z#XPp0eDNGPXZ+d#FwbLkD4yrlsdz8^Y*c(*0nh$7DLIe3S@9g_7RBq3cPYLd{8q&W zz;9E02zJm)r``eqB{kAUYj3Mu!1pVj zmVFiMUsUpQik=6p#v*@0Qp>Ieb_dX|!8}GlZJ2=pYDeXaDf*D4MHB@s!VI`h9N59X zC8?7llD1GxQWqVO)JMl8T~8TFYv^%MC#rJ>v=|wgkkl>jv|`NocV)YhPDC&UrxpFUqGu#+rr%fXvy#@*A4poKXeB*`wi_qPK}qkTS z<@AK4mVY-+parx`R36GvX`GL8v}ZxRIMKN6RqYC#Y}~F;ZC-b7`&7FU70m5Q)vm$? z$?YoDt|nc!tI0zTK+g0*NmtMmXbmjcz6KhL*3(YeUPHSiT}eSnHQFs{BkhrN356tW zpuLhhXi(DCv`u^hO9_o=lrj5WM zNd--UF2T)kKWI6?uX=!MoX_B^%W<#xS!$i{Obhilpx>(W6|BUoP+#0av|H(YmU@w1 zhd!e8mABD%LI1GQSKUUxANm)S9wEz@ycfW6{70dGQ|WPp$@`&5T3LE#|4!-csBq5D zZVbft(wMN(sHRcM%h@VJoeNkH?T4Pv*>I1IOfP%$;LP_T%QZCItC7!^>e)ixj&LvZ zdI@@!L03l&;wYu`gRoqQlSPwb9fU6jJd6o@ejFP#8e|Vb$2ke>_WU?q7)!@EU5Jq7 zKEWe+<(-_1KZbrh_c2jKhcqW0vN`g;d4@LVjw_pRdl3z7aMDnRgQnWM$gA-@d8T+@ z4I6$BFeZxOr-a6AZW^<@^1gEUm{WG)o?;s6EurvsH--CLG}RM87RqqGIiib#e5|?aGd6%K>5v@+vPU$-RC0gMx!P4u17msEl0g}Q;Gx_RG z6u>7a{G~dY^4<$gi59+yI(6?TBKHmlxx`;N>t>xj(73V3obc1kBYG-4s^_cRvlmgh zsnb(}i2U$3?h)XYVDwfOOFjn)-YZK<1$G+mKB7OveE5CK8e2P_t!u*g;KIIa-+r@E5E+lp+sPJ=>_`Z``F@OHbou>MC7D4IT&PAn02%mS#j#B-^Fr z7F$uPt+l3ZW37WJq|Wf?Eq@-)>r1$GzC7#L!MQ9cFDcM@!soGCJ$-cTRn|0Us5f|6 z2M?79y>l(j+8!J4 zXY6#$1VW~IE#$r%of@xEJ4Am0E?(hB(-Vh69@G+K+oV)!u=#rULtb|1dUm ze3H&O`6hD9yNJ&-j?t%dQ$4$Jo>;mM_v0J-7*#-vt;RbED|s@}B$Txtp6S=a%hf9sDk( zOlE*{a8rIj%6U|_yBqc#n;$bw^{jv|Q+Fduj*TmsbF?FG(@P#cPdoU*1Or##y~+oZP-U1RLIBQ|&=&ws|z%hsB+^&wQ1pUWpTGO5clf zlqBH?*nMuX2_OHMFP1#dWmL+ULZ)>z<8-lK zMdh^qORmpG)NroPMz0MqR0^+*(-0n-NDvu)%xCR0zB^hI=l~fjeCOk;*&q;!ay9n z!dz2lRPOxFN^+*0kBg_B7n~QxH^ld6oa>?I6`O;#6ZV{!XJOwAiX7`Q-+W)`K4h8a zfm_SQ^XNlA+?RMZ;@vIBZ_t?G;%T+viMYx+u7XH%JD~~NvyAt|$4`yc(;D8k>!~arKSqN*$uhikFMF1< zywTB^rLv23N^m*I_8HjUpcifm&Pl-+aAcMR#sVjfeP@rTE)T2^oC+KZjL{92hh9GA z|EE)adJA_Dd}@t{zOJR0ipq*Td#+uL&&4(KN2iXB&S^7CM@LU-^-Dt!(4XK+#OPaO zhdmbqp}Qf1E2_#zQe{P3#it_MR!Oz&Ru8u8-gQ)^CLyQ1Am{oNZD*4?HQBQ?U=6V+ z6v)l_EFHA^edp!Ip06*92j-dr@%WyXm#w=y9uE{G<9&KG7Y&R8_*ama_h})Yq0?0eeii^jnBQT@7S2~`*^0y0FHmMEtZV!fn;X{W9_VpbYrq-Np zXy<{w{e$7d{RhMSgF6ln_lNqeUfcKW3-$L7e%NyFzL9>}W#>@u;DJ!@5PJ;{hN<~h zpEsScYc^T_(I&&;-l4EG(v?vk#vi(E^rDZnz95zEFcQW@Bx|fc7MY2V2$B}u4TQ*s zxsm|CJ47Nx8d_Qi;t&`+9zm)PMa)<_8IQF^lV;X1vu%A$=d#^YnO7wapQ^#-Yr<*qaMt%0DlQZ#{k?yADp;#m_6F=S-G0kK) z!U44Po6$rv6E`RJ8rh@CST{A@#wci-M!GK%$z-}|>237G@d-1Molawo{GpgJ(`M!G zcsz@ttQF2JP0NNmu<5u1o6bA1*?0#wo9@77^Bvf1=_dXEnR_?Yg}6%E5)pHvt?y_g zJ#0)(8)g)FYc9|P6I@b>VJwl+KNmGp)>iQqnCu%rW<;@Xs|sX8)2102=Rzs89L}b( zy7=g3Sx#r;iMCKYgT7*fv=J2L$ZoQYph&e5RObj+Cmum^@^chS5f~IMw`JMFhI>Gy zjf|2QB|EG_c@WI$)OT zQpk4}$Tt@7n+o{N1^kvpd`I4ASAl#hcoot>A~KO7pB0}vX$F$f=?s-dVv$tVNbkZf zPZ(s2#A4*a!zylaM6txZS*ngE6NyMFV}xP6yaW{+`#8l!0g%iUXe z@Z?Soavh1AF>A=W2_w76C|6jt!L&lNwpq1~QLCX9$Fd3DlKDda&jHJ!EGJ7hCU zm7t+yRGtz0%wYs)m{bPg{z&wAWWpGXOd8~pZ9YKB36^I^tB)o#Sv``#Y+@($qmhgr zG4(~M(_`w!AV=KfT}7>$>kv@`4PpVb$rjn`Y1bpYyX-^?Wa;A;wi71xMkv1liGX^b9b2VnLLMB$Q z;99ZV+tSk}`7%)UB_~q}BWuJgg^OhtTajmjxX99GBthjY;?$tt%?Zq;_7*|jvM#5{ zZ;@OV{i8JbzH(q5C7h1T80iegsn2!Fi}KbO=_57~f?KS~B^{H5f)64pWi}QwkcVk3 zJvX--NVweI^JIbIZEit;vK>q>PL-8b~K6gJ#T_ldWLZn6&)WpvzvY$0YXE z&UD1Y+NE)~QyJuiyAO%Y4#|~MfpCb!$?0^|ATQX(#pY&7(i)GoBXPWGNEqSdfsBzR zNA_qu)6!0!tV*BU%N5v_EsIOMltod#3FY*h+{F4)2{XvSY$P3{a&Al%gkH=7E)4Q8 zhaEc-NlY77E18H{tuoeSj0v82KYV%%+PDkSS#n8Ll8XDcZCLB)ZHA8M)49HLeMkE`T>NVy$xLdK|54J)#OK5k zQA?T!Ov5g%R14#awkE3?vdExC_FH7gB4LXRkS2t-po;~4#r(YeWc5n%68?ur2>XKG z3c-!)RpM+7*@P&PMzC8A@#UH|;+-0@3sIaCL$el|v$gBQ#aeO*;VIBSiGS;%zgF8O z&epNs_aS{d^q1;3i0gHT%2p@!9gzK^sZ*S*7vg#Y&`4UdXwZaj^Aq#-cbdigdHY$v zIIX|Lk6$dq;~5=~lg)TsV%hijah)G;v+PyYo@ee2eq7<^JGwZ7&o+Q?om?*5&oAS) zgzFWwuPhg?>!1rQ!u7Ql;r=P;l@-GM!3yDed!=yCx902z#IC@y#;)20pFzflgrt&^%;xl3SbVhgsJ8=>_c0wF1f*r>RN)#u^iorH(kfkq<68V!X z$$wG`ODSyvWaUpc3MK7A3tKr$4}lXp&c;SJI$&q8wv6>;Yj)P1a@KB#9kL#J*8S~% z_kB-7PGL(w`}f`Z?!E86`~KYbzE4upSa$dJZ>5)R{i*NE=fAUK;7i$`e)jFXqdN|N z_g?KQZ$5aXgD6Wx%M)AtAoAJ(*;C+Ht^*%vM7^K|8&NCh#R{T3K+iacDnZjOqWfAv z-9#@m5dA*z3h)DlpwXMhKRM}aQ_XMuCT*MMh%=YSW1KLTC@z61Ofa0$2!`~>(JpsgaR z0a}4hpdYvy*azGOB!CQX7jPOl3;Z5%0r*4URp4)d9|QK)@CB>`_5kC+VZa0yfd_#n zf!_h11AYMf2zVcGwh{S(7C;Bq1AV|SFb+%tGr%$6e&F-KSAlN;-vYh|{4-!*Lv%f` z9f$!Ya0+-7cnY`xybk;|@FU!EaIgdu2TP->T%??hT6PIJYVO1MmZiw_^gg zD?R}JM#byk2NmB1zSvf8nS7v3K2*l@*oIWyTgv1I!SfvQn&4{(!2Ioy->mo~_#wq- z!4E6`Q{ZqFpvORcWYO9Z z74&7uYq7e%0_sy#arpOocRIxDGV7eMcS6XgK(za^>F zE|;HB@-HdM?bajP9+uRyYk=J$lp7HL1gH&>8=!V%%(S9+N?Jh)&q>Xe&(k4ag=<_JMv4h+$>2-8m(X^!1vVAu)JS)q7DoDDP?v>Q? z@5YYv5Xx1m|24=g9)}u~mq2~k!MN;G^xIxExUB zdeUXNo;>m#XZmZ<*JHPv2W^lu&a__IHK4scQr=CsNZLtZNj2Il=_=YMX)8q}ZK3^= zI%rJN^>jc|8y%F?PPa`r0eK6B<-e0K$~zX@c1=j{Ft@^ladOW z16_ra;5N`2fUjnNbDHj9;=BeQvY zf@p}(p=n{GDNUoSkF&#z{1&jlSqgm{Xl%$vrjNaOu*dt*%?27D*2r(m4&6Y$zSuDI zdKGGUp=+WRag?(9ZLsXbj-tu7dg03f57WY4YR3kR2H95VcuZotz0}SC+S1X^03u|$ zUvLXPc?K8b51}6SZCX^&q~@eao1^5LduW4hMA^i8D=50zNzpzB&G&92pT_;RB5q`P2_j)sv!3+2f4)S&ZZ^j&Cs~9ww&<8d!FA@>rpi? zaLpk^<)(g56(VYfzZs7J*NWD=Tr7DC60CnIsl^K8RflI_^k)FH+t!PFS9ff!<;zBH zo2mEVdB|fMu<2-X71oLywk=d^v#fSNQiUGgh!HQhcENhJvbMq6Z>z;Jw$^wfTH(C$ z;?BMp0YC4HGLS!Xdb$i+|Qe6(NEoGvH7r$0P zLm(#sED-B~8$G=jEI&i+R^a>~A!MHLT-fl(=C5Hcti7(0rnM&IN(ZDoQ*42E73f<) z=Y3w3JvLs?Sn22qgiQ4Y$o*IPHJ+m;>{nfOv$vV*v|5`Daj=iSfe+5RAkO1#?`G^V zwO9xINA&m5alS7(`3cJ{&mi8%IL3ccy7{3n^4-$?a=pF(N4;I8db|g53|ETnIrz!W z^2u4jdGYVX_VnGnx@9avj*_M0zC>|4@i}SLAo?HFVfmzh90O{N>WB8@dW#x!OJm0& z<$`_I$2+nA%A~KEru;2(U*ul1FZRj(&-QMW`!UZ1%XvIlKHobg<;8LGz^9ZC9hCbb z@83M8+*c`&MWwtr20Z30Uvk87x8|D50To*iJ`UQ?#8616;(>-DI3ZL;02l8;)nvu;H@#dX&U8(w1by|+-i&7tM;JpR%#;6+T_C;qluA8-A|!BeE&1KDXHFYmj`U%!{!w5QhgN!D~E@e)1X(I_k)Ir*mH1>->UvTD-XE*a#`l*Q}gV2kd!V(y-^} z8H}+m$6KCPeEMPT(1Tvosl(f$iM1S+y|7M-K-oO-DbM+Q7ku%&1)TwMW}TlFkBYB} zi{j73_ZOW#kaO$HXx#~WZu=DMSA*80wNl%9Jv7j0(KKQCK4HMfw~bjI84QFn^mK7d&jZ zy!0dlSLi%;N^fX7bmG|C`$T_vc_Rf>A7qaomR)^_cCyK`>gyTWZ8fnk5-N824Bc+kd;6Kzp;yjQ)j62UET)o1ZjjbSlJWFn>UdAwG&6-b2hcNWCeoRF%ADPA6pm(+ zgVgpBMq$%5awF+@K0ipSKSDpAnlDYV?HhtG% z(|-*%Tdu)o>owTia1A!w21);a`aVca5zdmHbljZn895ryjT`d|hMB;)wU=qaX-=v1 zIHt%LT}~KTYpM9lOb*N(GZI+0fihWi!8GGDoG9g%?PX-l2n^Oc2011;ua<+->1Mhz_6f#sWjmV;IxS;5>sS{Q3UtBu)!S;9DHXN z7L6!oYKpWeIGEzP(_wO3%cNs-Z!cAS$d?sXX!>AQtWwVRm&v!3@mtII8_M`?EBL;W z&w(=ecJL~s-Rbyjp8QsPYNOemNi5{4I-ZPY3r6l1Eb_ELws|Ol$o@eY??I+D~yVH zMXRpXaP?yDlN;KdnM~SEmcDvHnR%_%e_Mu3seUh$t2_!aljl$aE3`<2oJ^+ z$K$icSbWYPmn`%CNlvibJ6eABGm8P*Iw`6i;W_UQMI&0*PWO8$A7JS)E zb|DSlrQx(^5hFQsM&d*Q)8@z-bD2ehR_8bw^OzwMGgx-2Snh4P1(W=FC`U4L*|bqG zl9s~7GK;M(v0D5sVNRRItc~uLsZRdGHGBO7BKZ(+-YFIGcbphv?mugFGuw!LXJ6VJ& zq$+06&82uQNi|%UEsM311-$mj!yFdnL_EDfhAYyAelwcRZ8NZVlc|npga`SS1=s^MGmDqg87$e0c?0@sT(_`XI6`@?!C1efZ&1ineN2~i=9V7Ct9^9}38cN@qqL}gJ7 zO*b?jG;9#~X-0(*o-z%T8=?Gm<0f&XksLzQu2e$b1O3^iUh!rVLb5eV{btB6we^Yr zY7*kj767jr?V?!|{;l_(w7=0VPM)*B*Dg-$_XTmgv<9~i`1Y=j+gX-f?30O5K!DBM4931ib=!u0y!L-634(uNUsi z*9+IzJB9mFXVHFCyu3!fe>~BSmk?b1{_|Dv&jQRpU&g;w#=la=zgEV-RmQ)g_<^^lNrDrq z%=9TB3-B1|0RI-q2HFeo?}7OJkKa%EH$nWqcL%_~3*vV@{tSzM8zgkJ$2`7Nv4QUZ z&)*T@V;}k30{*T{gPhyq-w)Zr^E;@V3-IiRzeBUZp5+|719JYn NodeWrapper<'a> { } pub(crate) fn text_selection(&self) -> Option<(usize, usize)> { + if !self.is_focused() { + return None; + } self.0.text_selection().map(|range| { ( range.start().to_global_utf16_index(), From 14aad89bf59eb91eb35111c33eeaa472912b48eb Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 29 Dec 2024 18:33:13 -0600 Subject: [PATCH 28/46] Forgot to rebuild the dex --- platforms/android/classes.dex | Bin 9532 -> 9452 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index 7f61cd72c8d9110fff33cd86fb3cbddf9b17472f..9e0d02acc7d12cd203ec0f193a77d43a2fd5e455 100644 GIT binary patch literal 9452 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 literal 9532 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%0DlQZ#{k?yADp;#m_6F=S-G0kK) z!U44Po6$rv6E`RJ8rh@CST{A@#wci-M!GK%$z-}|>237G@d-1Molawo{GpgJ(`M!G zcsz@ttQF2JP0NNmu<5u1o6bA1*?0#wo9@77^Bvf1=_dXEnR_?Yg}6%E5)pHvt?y_g zJ#0)(8)g)FYc9|P6I@b>VJwl+KNmGp)>iQqnCu%rW<;@Xs|sX8)2102=Rzs89L}b( zy7=g3Sx#r;iMCKYgT7*fv=J2L$ZoQYph&e5RObj+Cmum^@^chS5f~IMw`JMFhI>Gy zjf|2QB|EG_c@WI$)OT zQpk4}$Tt@7n+o{N1^kvpd`I4ASAl#hcoot>A~KO7pB0}vX$F$f=?s-dVv$tVNbkZf zPZ(s2#A4*a!zylaM6txZS*ngE6NyMFV}xP6yaW{+`#8l!0g%iUXe z@Z?Soavh1AF>A=W2_w76C|6jt!L&lNwpq1~QLCX9$Fd3DlKDda&jHJ!EGJ7hCU zm7t+yRGtz0%wYs)m{bPg{z&wAWWpGXOd8~pZ9YKB36^I^tB)o#Sv``#Y+@($qmhgr zG4(~M(_`w!AV=KfT}7>$>kv@`4PpVb$rjn`Y1bpYyX-^?Wa;A;wi71xMkv1liGX^b9b2VnLLMB$Q z;99ZV+tSk}`7%)UB_~q}BWuJgg^OhtTajmjxX99GBthjY;?$tt%?Zq;_7*|jvM#5{ zZ;@OV{i8JbzH(q5C7h1T80iegsn2!Fi}KbO=_57~f?KS~B^{H5f)64pWi}QwkcVk3 zJvX--NVweI^JIbIZEit;vK>q>PL-8b~K6gJ#T_ldWLZn6&)WpvzvY$0YXE z&UD1Y+NE)~QyJuiyAO%Y4#|~MfpCb!$?0^|ATQX(#pY&7(i)GoBXPWGNEqSdfsBzR zNA_qu)6!0!tV*BU%N5v_EsIOMltod#3FY*h+{F4)2{XvSY$P3{a&Al%gkH=7E)4Q8 zhaEc-NlY77E18H{tuoeSj0v82KYV%%+PDkSS#n8Ll8XDcZCLB)ZHA8M)49HLeMkE`T>NVy$xLdK|54J)#OK5k zQA?T!Ov5g%R14#awkE3?vdExC_FH7gB4LXRkS2t-po;~4#r(YeWc5n%68?ur2>XKG z3c-!)RpM+7*@P&PMzC8A@#UH|;+-0@3sIaCL$el|v$gBQ#aeO*;VIBSiGS;%zgF8O z&epNs_aS{d^q1;3i0gHT%2p@!9gzK^sZ*S*7vg#Y&`4UdXwZaj^Aq#-cbdigdHY$v zIIX|Lk6$dq;~5=~lg)TsV%hijah)G;v+PyYo@ee2eq7<^JGwZ7&o+Q?om?*5&oAS) zgzFWwuPhg?>!1rQ!u7Ql;r=P;l@-GM!3yDed!=yCx902z#IC@y#;)20pFzflgrt&^%;xl3SbVhg Date: Mon, 30 Dec 2024 13:28:51 -0600 Subject: [PATCH 29/46] fmt --- platforms/winit/src/platform_impl/android.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platforms/winit/src/platform_impl/android.rs b/platforms/winit/src/platform_impl/android.rs index 667554a7..7b6b09a2 100644 --- a/platforms/winit/src/platform_impl/android.rs +++ b/platforms/winit/src/platform_impl/android.rs @@ -49,7 +49,8 @@ impl Adapter { pub fn process_event(&mut self, _window: &Window, event: &WindowEvent) { match event { WindowEvent::CursorMoved { position, .. } => { - self.adapter.handle_hover_enter_or_move(position.x as _, position.y as _); + self.adapter + .handle_hover_enter_or_move(position.x as _, position.y as _); } WindowEvent::CursorLeft { .. } => { self.adapter.handle_hover_exit(); From d7ca5a6fd1269047c602244a322bba96258eec4a Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 30 Dec 2024 14:43:14 -0600 Subject: [PATCH 30/46] Add some documentation to the two adapter types; at least hint at the design rationale --- platforms/android/src/adapter.rs | 20 ++++++++++++++++++++ platforms/android/src/inject.rs | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index ea450a41..a6de7812 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -228,6 +228,26 @@ pub struct Adapter { state: State, } +/// Low-level AccessKit adapter for Android. This layer provides maximum +/// flexibility in the application threading model, the interface between +/// Java and native code, and the implementation of action callbacks, +/// at the expense of requiring its caller to provide glue code. For a +/// higher-level implementation built on this type, see [`InjectingAdapter`]. +/// +/// Several of this type's functions have a `callback_class` parameter. +/// The reference implementation of the duck-typed contract for this Java class +/// is `dev.accesskit.android.Delegate`, the source code for which is in the +/// `java` directory of this crate. The methods that are called from native +/// code are all marked `public static`, and so far, all of them that are +/// called by this type (rather than [`InjectingAdapter`]) are for sending +/// events. Other implementations may differ by, for example, sending those +/// events synchronously rather than posting them to the UI thread for +/// asynchronous handling. +/// +/// Several of this type's functions have a `host` parameter. This is always +/// a Java object whose class must derive from `android.view.View`. +/// +/// [`InjectingAdapter`]: crate::InjectingAdapter impl Adapter { /// If and only if the tree has been initialized, call the provided function /// and apply the resulting update. Note: If the caller's implementation of diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index c4672af4..7cccb0bb 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -332,6 +332,16 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { Ok(global.as_obj().into()) } +/// High-level AccessKit Android adapter that injects itself into an Android +/// view without requiring the view class to be modified for accessibility. +/// This depends on the Java `dev.accesskit.android.Delegate` class, the source +/// code for which is in this crate's `java` directory. If the `embedded-dex` +/// feature is enabled, then that class is loaded from a prebuilt `.dex` file +/// that this crate embeds. Otherwise, it's simply assumed that the class +/// is in the application package. None of this type's public functions +/// make assumptions about whether they're called from the Android UI thread. +/// As such, some requests are posted to the UI thread and handled +/// asynchronously. pub struct InjectingAdapter { vm: JavaVM, delegate_class: &'static JClass<'static>, From 78c7d95e8787f1074676536f8fd9627f2127ddcb Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 30 Dec 2024 15:35:33 -0600 Subject: [PATCH 31/46] Bring back the better way of doing explore by touch, now that we're not trying to work with NativeActivity --- platforms/android/classes.dex | Bin 9452 -> 10020 bytes .../java/dev/accesskit/android/Delegate.java | 33 +++++++- platforms/android/src/adapter.rs | 16 ++++ platforms/android/src/inject.rs | 79 +++++------------- platforms/android/src/util.rs | 24 +----- platforms/winit/src/platform_impl/android.rs | 13 +-- 6 files changed, 73 insertions(+), 92 deletions(-) diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index 9e0d02acc7d12cd203ec0f193a77d43a2fd5e455..eb848a1b25786b5637d80c3c860bfaf246ddead4 100644 GIT binary patch literal 10020 zcmb7K4RBP~bw2m)YFE43l~yaQ{sm8q009yblE4BjwuF$d(1P%45tif_R=W>rVfA*k zyAq;KZ0rz&9o*uW#7;_TyN3E_NCw-)ZKsaM9jEO~?Q~K%lVKXSjXO=_v`L1f$;6!@ zb-#1p+hs-UmOS|0Irp4%&pr3td+vKrQcPQNc64o~h8M5ze`?*yokw4LU{}i--?_Jb zy7{@p&+1-lUQ3iBqNVX}FNnMjts%Muj^(QW)k4$_dI_`vw82I+4SF547}Vt?s)52) zLUel#(Eu<4%m9xAe*ye6V5ucq4YUBQKre7R&<_j(BfuyS1`YsIz`ejUa2PlW%mZhD zXMt}59{}z;B0sPl(1F*0{{Z~;M7w|);9=k;;O~H609A;08*nf15O4u_19%_!6;RSh zv=Z0^bOSv=0O$t>f!)A9U;>B$v%nmX1{Q(izyrYNfX@R@0Ox^=z>C1wfmeVxfVY9G zzz>0+1HS0v`d5E0J%Y8|VW@ zfdfDS_%v_|I14-vd<}RN_!r;<;2Llps8o>)U@g!F+y>kM+zA{2;y@Bu0!{!=0~dfl z1Kt9@2mA=2CZakZ0E_}hfRn&iffs;31^ycNC*VJU{|3ta*fYRpUz-wobl@__>RPVijd7JzLkK&=2@%-bbr@F-__RpQRM z;dt<0@UK z*}p{*fp#t8pOjRgc~C26bOf{rm3GjinxuA`1+}4HOj1F*Nt2*<%vgXr$SI$54seHM zdo9H!Euo{5R#HaNPWp_bHS~E&Yv~-Q6V-kKv;>(&GZLm`>aVH zH|ZBl`h=v-^hLA%B}wb(NlEKX>Z32C?Z)YJkEAV>HR*z+WpaEsP6&>}jd|WL=^8pC zso~#^)9fo~mzsI-k}C7zMf*X>D{wk;ek#m%B~D3hSDI}fDxBLsvt5P#!R;!uT}_R$ zU2V2&aFVjV1|uFaX++X>bRTFfda}M2JH((3G$Pv@DJbcB+9Roj_DZ^vMkQTA`y_3o zyCf~9kfd!iCTS6cCAHChNjK2Aq^)$oY~L$sIYlM4(2S&3nv+zew4`oYlC+(EPtx^f z+->w}*{-7pByFOHByFJ2n)IZkE9rA4eOS^JbjqZsC2gcfO!}y#t@MjM=?dUek}9+ax)M*EDNsKD`Dy_8EZ}qg&DE%nW|kUfGt)x-Rp@I?eI+Y#73!}+ zzuwgQ4D~X7@fw_aroQT9^eXgIrXC^5tI&TB^h>55N143%xxyF6zYqGSOnoidknUki7oICVR8=jF z^(*AHr24jzr!(9Sy;_Q123H2I6jt?ACsLGBKZQD5i{1+U@wlIbUmNI@u;$0KK*MM7 zY3O)WubQ^ueQpZfg?Wa` zC^S||p|FeQ!+p}P9jnW6{yA*!o|Fbo zQbnO@FYS+(Qz+)6kXA{@Z2hn*MWz+GfBqKsTti-U6;?F-at&76318lycQrU*VSxE2& zBqilYk`;{D)ecBX zFuU84?Lun@teZ@03#`4Ca%^Q|pK~2s0p6Wli$BBoZ-9EqTi)t=23+`SMh@J_!F=C# z>U`Vj-s(P$x7Z%{I;wNoFbhH7ma#M;nj+bDlT&OJ7HX;1RSRovOd++0@yf$Fb#YEF z)R6ebG21TAV@X*_fzB0v1}oJ!MDJZ@O`U>TgNHVFs6gmlt*Eh@7R6g!=ifX!ii3}B`Yft>+{OmJ z)(zcn_uKb>^t&_PkIx^DVIVi2jh}#q&tgQ$;{~DPeCZzEr%tL6B)q@aAiv96+Cm5F zsdMdmR??Yt#yx$seQh0ga%ShduSi*?Oyb;BP%D2ek7%8k!;z- z1blj{$~Z7Q12`XiHge9Ga!g#YT(i@;T&TOb@?@S69yErI`-JdpFuwb^2i6~bSUw>j zhpDmRH|)o-cD#(d*6e8x_WXU9^6)9?y*A;gr3p_xO?vChc-YeL%dw?8w#sJ(?-Z8v zda;}{#Ltl2IxgnqedF@X=4T79Ij@DZ58o~2xi#WF!SZ7^etS3eBDe2{+yUyx$&ul8K52N?r}8!xc;arr{BGFf_Tyu);XSW=O3=$i=?;$>udxr#<$d6}(bu+Y zoLpU;hYja$zGDwHSzHRfV~e}+jP+LND49;2kRhD${2aB)xjvuw>7!J*eVI=OY&d_H zVUqy$BCGjd}7u_1mh*e#kyA9op_S)CPOb%T?I(J1lan%6!XbX*t##Ylj(hpiecvpmlWxYJej*o@6xz z>%eayTwfo+7q6S&UQPCt{Xy{s@tn9K{z`mr(cT3)kG_P_?Xc&ue+m0WP#;FikFD0i zbC0!%Do{8r7TgtGt$>W?mI==%{Y|BzwMWv6Nnw(VbYov|5V>eN0JRYB-5D!v{ z&yCVA(GqWRH14C)EA%K{+>GX9uz#1%3r-!bQhFMKYxFWs^U}an;P{buM})5|u<=yj zNMMToi^ai`k*Js4)%LVo*-j(6J?A4KglJ0$fq-|bJS=l?mWJ4B$;`nS+HH(I5)9;0aR%>ja?bBwtnYhkWjwI7A`p*{ zTwK|9_$|&7#tfL-Z2~;4o@5$8Xed_MlCsAXxIKdgL}gV z2k#CK?%jECY%n-z^x82x8XWB3``ea#M#l$bmtCR$z59dxA@(}FH%v`8`@G?dU9-um zPc|6~_lLsLNHvWHFn;J`qZb3D3=ETdr!rH9a51hOYu}U1#*_NsB0Bccy5+iY7&P}sqgo~tpN=Qu+2gr@dui3jn2zdqCKt7I zFrGmiTDmX*ITritEeJR-+!sm9@!OX>jT8>uzAO+k;GtXYus0dghV{eA<@4!}^6Y!5 z^LO^sOIv=YKKs(i#dwU9vMLyhBo^aG+atQ3%tknX_CYt%pNT}n~e3+ijOfG z){)eKL?n~xrN)oZkHu&7NOmEOMfL|{+G4wrzoYRihBCHDZvC1!-GWW$E!cG3f=%}= z*lfN9n=Q9sb6YQ||IgfesV2zv)}DyyGwlPjk@T20zo6++j{`iK@q-BcFHLJPVcndV9>v>eN(vAX!thhe#pjVIc)F z7-!=c4qjRMRKllbB(7&hwBuACO>0=~{=#!7cW{v7a9ocWL)Oe_*=0t#x}t`gHQYT{ z`{a;X-bdsFyCa!QXc2=q4{dToKCGt}vc^a@+00S}XfPR-r^To~hB!5yN+H}Ai5`v2 zXnP}bnsjd*rDTVSCrUmrG@c>kPHE}G$@HALik?(5wUEFd`Q@;t5H;D;TJ(4n%Wg|+ za|n$Z(_G9Mtgwz9Q1Iw7#<8Rqbn<4P97xWk5?WS^843r>47NPa4&yQ z$AvzFDOKMj$XnLr6!|Td>!N;=Cf`>M%%gpz-$cLufk-9nK@bQ$EAs56$>Ezt79@Ca&YdEXT86K*j z+k+LD!;^ehI-+B>(s*OwWM^-BF`!b&3O6p&n+?hJGsEFP$C3-_s74;J%S+bDlB5v> zYscewJxOTcAn7bQq(rjPWtOYc;E-FA0WRJs zRFqf|dSIaB{rB(exz9h1LPxazZGPn5-|CNI|He@fI855+aL7o-^ui2ODB-r~tQI|* zS(wZCw;hfoGFq#DF0QvlQgQ#bO|5?3UFe8DT^qYLc6PMkbzws!nMrN-e}*)-zS>#U=bkO$h5* zbpr$^t6Rl0)npN(NE*Se4dNeEZxknM$SOo}P7F;uG%wV2i0{-;kr1u|4V0Up{II4| zT&N|R5ar92(04)qo!V}3q7EThs-=E2WT(|F;&Po3*Xsd%cKDTAMR^`R5ibIsz! zGuGFe#cB1sd^_EQ+XcQ|UWMCFSaxzXZs+-Son>#b_8sQ_jc@O9^TQ_bC_aGzrowS$ zwQ#}74W|<;Qz*1 z>`n4^Q-1y?IsZh4{l9RNoPSor@++nsO?mOpR`}v?Q@)s90#X3y0Mn3vKV|{# z2KaBt_!FOh$H#w1Rs=c?@ZXa0w<&*{^WT#R)%d#^{+lujcpjF2Z-*ak;rE%a!!H>W q$ayUOn=>nT{vMZW0iONv?=>y3XF11igPgz3-(+WybF7Ef5dAO2VXYMa literal 9452 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 diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 237bfe21..03665a04 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -11,15 +11,17 @@ package dev.accesskit.android; import android.os.Bundle; +import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityNodeProvider; -public final class Delegate extends View.AccessibilityDelegate { +public final class Delegate extends View.AccessibilityDelegate implements View.OnHoverListener { private final long adapterHandle; private int accessibilityFocus = AccessibilityNodeProvider.HOST_VIEW_ID; + private int hoverId = AccessibilityNodeProvider.HOST_VIEW_ID; private Delegate(long adapterHandle) { super(); @@ -35,6 +37,7 @@ public void run() { } Delegate delegate = new Delegate(adapterHandle); host.setAccessibilityDelegate(delegate); + host.setOnHoverListener(delegate); } }); } @@ -46,6 +49,7 @@ public void run() { View.AccessibilityDelegate delegate = host.getAccessibilityDelegate(); if (delegate != null && delegate instanceof Delegate) { host.setAccessibilityDelegate(null); + host.setOnHoverListener(null); } } }); @@ -159,6 +163,7 @@ public void run() { private static native boolean populateNodeInfo(long adapterHandle, View host, int screenX, int screenY, int virtualViewId, AccessibilityNodeInfo nodeInfo); private static native int getInputFocus(long adapterHandle); + private static native int getVirtualViewAtPoint(long adapterHandle, float x, float y); private static native boolean performAction(long adapterHandle, int virtualViewId, int action); private static native boolean setTextSelection(long adapterHandle, View host, int virtualViewId, int anchor, int focus); private static native boolean collapseTextSelection(long adapterHandle, View host, int virtualViewId); @@ -255,4 +260,30 @@ public AccessibilityNodeInfo findFocus(int focusType) { } }; } + + @Override + public boolean onHover(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + int newId = getVirtualViewAtPoint(adapterHandle, event.getX(), event.getY()); + if (newId != hoverId) { + if (newId != AccessibilityNodeProvider.HOST_VIEW_ID) { + sendEventInternal(v, newId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + } + if (hoverId != AccessibilityNodeProvider.HOST_VIEW_ID) { + sendEventInternal(v, hoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + hoverId = newId; + } + break; + case MotionEvent.ACTION_HOVER_EXIT: + if (hoverId != AccessibilityNodeProvider.HOST_VIEW_ID) { + sendEventInternal(v, hoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + hoverId = AccessibilityNodeProvider.HOST_VIEW_ID; + } + break; + } + return true; + } } diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index a6de7812..0539b395 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -22,6 +22,22 @@ use jni::{ use crate::{filters::filter, node::NodeWrapper, util::*}; +fn send_event( + env: &mut JNIEnv, + callback_class: &JClass, + host: &JObject, + virtual_view_id: jint, + event_type: jint, +) { + env.call_static_method( + callback_class, + "sendEvent", + "(Landroid/view/View;II)V", + &[host.into(), virtual_view_id.into(), event_type.into()], + ) + .unwrap(); +} + fn send_window_content_changed(env: &mut JNIEnv, callback_class: &JClass, host: &JObject) { send_event( env, diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index 7cccb0bb..7df41352 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -56,15 +56,15 @@ impl InnerInjectingAdapter { ) } + fn input_focus(&mut self) -> jint { + self.adapter.input_focus(&mut *self.activation_handler) + } + fn virtual_view_at_point(&mut self, x: jfloat, y: jfloat) -> jint { self.adapter .virtual_view_at_point(&mut *self.activation_handler, x, y) } - fn input_focus(&mut self) -> jint { - self.adapter.input_focus(&mut *self.activation_handler) - } - fn perform_action(&mut self, virtual_view_id: jint, action: jint) -> bool { self.adapter .perform_action(&mut *self.action_handler, virtual_view_id, action) @@ -178,6 +178,20 @@ extern "system" fn get_input_focus(_env: JNIEnv, _class: JClass, adapter_handle: inner_adapter.input_focus() } +extern "system" fn get_virtual_view_at_point( + _env: JNIEnv, + _class: JClass, + adapter_handle: jlong, + x: jfloat, + y: jfloat, +) -> jint { + let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { + return HOST_VIEW_ID; + }; + let mut inner_adapter = inner_adapter.lock().unwrap(); + inner_adapter.virtual_view_at_point(x, y) +} + extern "system" fn perform_action( _env: JNIEnv, _class: JClass, @@ -305,6 +319,11 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { sig: "(J)I".into(), fn_ptr: get_input_focus as *mut c_void, }, + NativeMethod { + name: "getVirtualViewAtPoint".into(), + sig: "(JFF)I".into(), + fn_ptr: get_virtual_view_at_point as *mut c_void, + }, NativeMethod { name: "performAction".into(), sig: "(JII)Z".into(), @@ -348,7 +367,6 @@ pub struct InjectingAdapter { host: WeakRef, handle: jlong, inner: Arc>, - hover_view_id: jint, } impl InjectingAdapter { @@ -381,7 +399,6 @@ impl InjectingAdapter { host: env.new_weak_ref(host_view)?.unwrap(), handle, inner, - hover_view_id: HOST_VIEW_ID, }) } @@ -402,56 +419,6 @@ impl InjectingAdapter { &host, ); } - - pub fn handle_hover_enter_or_move(&mut self, x: jfloat, y: jfloat) { - let old_id = self.hover_view_id; - let new_id = self.inner.lock().unwrap().virtual_view_at_point(x, y); - if new_id == old_id { - return; - } - let mut env = self.vm.get_env().unwrap(); - let Some(host) = self.host.upgrade_local(&env).unwrap() else { - return; - }; - self.hover_view_id = new_id; - if new_id != HOST_VIEW_ID { - send_event( - &mut env, - self.delegate_class, - &host, - new_id, - EVENT_VIEW_HOVER_ENTER, - ); - } - if old_id != HOST_VIEW_ID { - send_event( - &mut env, - self.delegate_class, - &host, - old_id, - EVENT_VIEW_HOVER_EXIT, - ); - } - } - - pub fn handle_hover_exit(&mut self) { - if self.hover_view_id == HOST_VIEW_ID { - return; - } - let old_id = self.hover_view_id; - self.hover_view_id = HOST_VIEW_ID; - let mut env = self.vm.get_env().unwrap(); - let Some(host) = self.host.upgrade_local(&env).unwrap() else { - return; - }; - send_event( - &mut env, - self.delegate_class, - &host, - old_id, - EVENT_VIEW_HOVER_EXIT, - ); - } } impl Drop for InjectingAdapter { diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 262a27b4..77a58e22 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -5,11 +5,7 @@ use accesskit::NodeId; use accesskit_consumer::Node; -use jni::{ - objects::{JClass, JObject}, - sys::jint, - JNIEnv, -}; +use jni::sys::jint; use std::collections::HashMap; pub(crate) const ACTION_FOCUS: jint = 1 << 0; @@ -18,8 +14,6 @@ pub(crate) const ACTION_NEXT_AT_MOVEMENT_GRANULARITY: jint = 1 << 8; pub(crate) const ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: jint = 1 << 9; pub(crate) const ACTION_SET_SELECTION: jint = 1 << 17; pub(crate) const EVENT_VIEW_FOCUSED: jint = 1 << 3; -pub(crate) const EVENT_VIEW_HOVER_ENTER: jint = 1 << 7; -pub(crate) const EVENT_VIEW_HOVER_EXIT: jint = 1 << 8; pub(crate) const EVENT_WINDOW_CONTENT_CHANGED: jint = 1 << 11; pub(crate) const HOST_VIEW_ID: jint = -1; pub(crate) const LIVE_REGION_NONE: jint = 0; @@ -57,19 +51,3 @@ impl NodeIdMap { java_id } } - -pub(crate) fn send_event( - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, - virtual_view_id: jint, - event_type: jint, -) { - env.call_static_method( - callback_class, - "sendEvent", - "(Landroid/view/View;II)V", - &[host.into(), virtual_view_id.into(), event_type.into()], - ) - .unwrap(); -} diff --git a/platforms/winit/src/platform_impl/android.rs b/platforms/winit/src/platform_impl/android.rs index 7b6b09a2..f7bb2f80 100644 --- a/platforms/winit/src/platform_impl/android.rs +++ b/platforms/winit/src/platform_impl/android.rs @@ -46,16 +46,5 @@ impl Adapter { self.adapter.update_if_active(updater); } - pub fn process_event(&mut self, _window: &Window, event: &WindowEvent) { - match event { - WindowEvent::CursorMoved { position, .. } => { - self.adapter - .handle_hover_enter_or_move(position.x as _, position.y as _); - } - WindowEvent::CursorLeft { .. } => { - self.adapter.handle_hover_exit(); - } - _ => (), - } - } + pub fn process_event(&mut self, _window: &Window, _event: &WindowEvent) {} } From f35fad7204b07843804fffa2895667bc30e83eab Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 30 Dec 2024 15:46:34 -0600 Subject: [PATCH 32/46] Document that the Android adapter basically assumes GameActivity --- platforms/winit/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platforms/winit/README.md b/platforms/winit/README.md index 6926d59f..7c3657b7 100644 --- a/platforms/winit/README.md +++ b/platforms/winit/README.md @@ -10,3 +10,7 @@ While this crate's API is purely blocking, it internally spawns asynchronous tas - If you use tokio, make sure to enable the `tokio` feature of this crate. - If you use another async runtime or if you don't use one at all, the default feature will suit your needs. + +## Android activity compatibility + +The Android implementation of this adapter currently assumes that the Android activity Java class stores its content view in an instance variable called `mSurfaceView`. This is the case for [GameActivity](https://developer.android.com/games/agdk/game-activity), which is one of the two activity implementations that winit currently supports. From ca2d6f45a3015e16c866cf8d3081667e66da9370 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 30 Dec 2024 16:02:02 -0600 Subject: [PATCH 33/46] Now that we no longer support NativeActivity in the winit adapter, don't try to make the examples runnable on Android using cargo-apk --- platforms/winit/Cargo.toml | 22 +-------- platforms/winit/examples/mixed_handlers.rs | 45 ++---------------- platforms/winit/examples/simple.rs | 54 ++-------------------- 3 files changed, 9 insertions(+), 112 deletions(-) diff --git a/platforms/winit/Cargo.toml b/platforms/winit/Cargo.toml index b0ccb3fd..35f3886d 100644 --- a/platforms/winit/Cargo.toml +++ b/platforms/winit/Cargo.toml @@ -39,24 +39,4 @@ accesskit_android = { version = "0.1.0", path = "../android", features = ["embed [dev-dependencies.winit] version = "0.30.5" default-features = false -features = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita", "android-native-activity"] - -[[example]] -name = "simple" - -[[example]] -# A custom example target which uses the same `simple.rs` file but for android -name = "simple_android" -path = "examples/simple.rs" -# cdylib is required for cargo-apk -crate-type = ["cdylib"] - -[[example]] -name = "mixed_handlers" - -[[example]] -# A custom example target which uses the same `mixed_handlers.rs` file but for android -name = "mixed_handlers_android" -path = "examples/mixed_handlers.rs" -# cdylib is required for cargo-apk -crate-type = ["cdylib"] +features = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] diff --git a/platforms/winit/examples/mixed_handlers.rs b/platforms/winit/examples/mixed_handlers.rs index 731be625..ef5246f5 100644 --- a/platforms/winit/examples/mixed_handlers.rs +++ b/platforms/winit/examples/mixed_handlers.rs @@ -276,17 +276,7 @@ impl ApplicationHandler for Application { } } -fn run(event_loop: EventLoop) { - let mut app = Application::new(event_loop.create_proxy()); - event_loop.run_app(&mut app).unwrap(); -} - -#[cfg(not(target_os = "android"))] -#[allow(dead_code)] -// This is treated as dead code by the Android version of the example, but is actually live -// This hackery is required because Cargo doesn't care to support this use case, of one -// example which works across Android and desktop -fn main() { +fn main() -> Result<(), Box> { println!("This example has no visible GUI, and a keyboard interface:"); println!("- [Tab] switches focus between two logical buttons."); println!("- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed."); @@ -304,34 +294,7 @@ fn main() { ))] println!("Enable Orca with [Super]+[Alt]+[S]."); - let event_loop = EventLoop::with_user_event().build().unwrap(); - run(event_loop); -} - -// Boilerplate code for android: Identical across all applications - -#[cfg(target_os = "android")] -use winit::platform::android::activity::AndroidApp; - -#[cfg(target_os = "android")] -// Safety: We are following `android_activity`'s docs here -// We believe that there are no other declarations using this name in the compiled objects here -#[allow(unsafe_code)] -#[no_mangle] -fn android_main(app: AndroidApp) { - use winit::platform::android::EventLoopBuilderExtAndroid; - - let event_loop = EventLoop::with_user_event() - .with_android_app(app) - .build() - .unwrap(); - run(event_loop); -} - -// TODO: This is a hack because of how we handle our examples in Cargo.toml -// Ideally, we change Cargo to be more sensible here? -#[cfg(target_os = "android")] -#[allow(dead_code)] -fn main() { - unreachable!() + let event_loop = EventLoop::with_user_event().build()?; + let mut state = Application::new(event_loop.create_proxy()); + event_loop.run_app(&mut state).map_err(Into::into) } diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs index 145044bc..39ccd28e 100644 --- a/platforms/winit/examples/simple.rs +++ b/platforms/winit/examples/simple.rs @@ -31,13 +31,6 @@ const BUTTON_2_RECT: Rect = Rect { y1: 100.0, }; -const ANNOUNCEMENT_RECT: Rect = Rect { - x0: 20.0, - y0: 100.0, - x1: 100.0, - y1: 140.0, -}; - fn build_button(id: NodeId, label: &str) -> Node { let rect = match id { BUTTON_1_ID => BUTTON_1_RECT, @@ -55,7 +48,6 @@ fn build_button(id: NodeId, label: &str) -> Node { fn build_announcement(text: &str) -> Node { let mut node = Node::new(Role::Label); - node.set_bounds(ANNOUNCEMENT_RECT); node.set_value(text); node.set_live(Live::Polite); node @@ -253,7 +245,6 @@ impl ApplicationHandler for Application { .expect("failed to create initial window"); } - #[cfg(not(target_os = "android"))] fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { if self.window.is_none() { event_loop.exit(); @@ -261,17 +252,7 @@ impl ApplicationHandler for Application { } } -fn run(event_loop: EventLoop) { - let mut app = Application::new(event_loop.create_proxy()); - event_loop.run_app(&mut app).unwrap(); -} - -#[cfg(not(target_os = "android"))] -#[allow(dead_code)] -// This is treated as dead code by the Android version of the example, but is actually live -// This hackery is required because Cargo doesn't care to support this use case, of one -// example which works across Android and desktop -fn main() { +fn main() -> Result<(), Box> { println!("This example has no visible GUI, and a keyboard interface:"); println!("- [Tab] switches focus between two logical buttons."); println!("- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed."); @@ -289,34 +270,7 @@ fn main() { ))] println!("Enable Orca with [Super]+[Alt]+[S]."); - let event_loop = EventLoop::with_user_event().build().unwrap(); - run(event_loop); -} - -// Boilerplate code for android: Identical across all applications - -#[cfg(target_os = "android")] -use winit::platform::android::activity::AndroidApp; - -#[cfg(target_os = "android")] -// Safety: We are following `android_activity`'s docs here -// We believe that there are no other declarations using this name in the compiled objects here -#[allow(unsafe_code)] -#[no_mangle] -fn android_main(app: AndroidApp) { - use winit::platform::android::EventLoopBuilderExtAndroid; - - let event_loop = EventLoop::with_user_event() - .with_android_app(app) - .build() - .unwrap(); - run(event_loop); -} - -// TODO: This is a hack because of how we handle our examples in Cargo.toml -// Ideally, we change Cargo to be more sensible here? -#[cfg(target_os = "android")] -#[allow(dead_code)] -fn main() { - unreachable!() + let event_loop = EventLoop::with_user_event().build()?; + let mut state = Application::new(event_loop.create_proxy()); + event_loop.run_app(&mut state).map_err(Into::into) } From ce099830ee7bda03b612814cad2253cb45f4e648 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 30 Dec 2024 16:15:05 -0600 Subject: [PATCH 34/46] The winit adapter's assumption of GameActivity in particular is stronger than I remembered; update the documentation to reflect that --- platforms/winit/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/winit/README.md b/platforms/winit/README.md index 7c3657b7..bbcf3f29 100644 --- a/platforms/winit/README.md +++ b/platforms/winit/README.md @@ -13,4 +13,4 @@ While this crate's API is purely blocking, it internally spawns asynchronous tas ## Android activity compatibility -The Android implementation of this adapter currently assumes that the Android activity Java class stores its content view in an instance variable called `mSurfaceView`. This is the case for [GameActivity](https://developer.android.com/games/agdk/game-activity), which is one of the two activity implementations that winit currently supports. +The Android implementation of this adapter currently only works with [GameActivity](https://developer.android.com/games/agdk/game-activity), which is one of the two activity implementations that winit currently supports. From e345d61d9ad89ce18761aa1315854ee9755548a3 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 30 Dec 2024 16:24:33 -0600 Subject: [PATCH 35/46] Reformat Java code using the Google Java formatter in AOSP mode --- platforms/android/classes.dex | Bin 10020 -> 10064 bytes .../java/dev/accesskit/android/Delegate.java | 332 ++++++++++++------ 2 files changed, 217 insertions(+), 115 deletions(-) diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index eb848a1b25786b5637d80c3c860bfaf246ddead4..8685c5d7c65ac3c4f46e6b60f314d5d60675cb77 100644 GIT binary patch delta 2971 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 NVS2$g)7!zJ{1wcgrF{SZ diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 03665a04..03dd5a10 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -29,30 +29,33 @@ private Delegate(long adapterHandle) { } public static void inject(final View host, final long adapterHandle) { - host.post(new Runnable() { - @Override - public void run() { - if (host.getAccessibilityDelegate() != null) { - throw new IllegalStateException("host already has an accessibility delegate"); - } - Delegate delegate = new Delegate(adapterHandle); - host.setAccessibilityDelegate(delegate); - host.setOnHoverListener(delegate); - } - }); + host.post( + new Runnable() { + @Override + public void run() { + if (host.getAccessibilityDelegate() != null) { + throw new IllegalStateException( + "host already has an accessibility delegate"); + } + Delegate delegate = new Delegate(adapterHandle); + host.setAccessibilityDelegate(delegate); + host.setOnHoverListener(delegate); + } + }); } public static void remove(final View host) { - host.post(new Runnable() { - @Override - public void run() { - View.AccessibilityDelegate delegate = host.getAccessibilityDelegate(); - if (delegate != null && delegate instanceof Delegate) { - host.setAccessibilityDelegate(null); - host.setOnHoverListener(null); - } - } - }); + host.post( + new Runnable() { + @Override + public void run() { + View.AccessibilityDelegate delegate = host.getAccessibilityDelegate(); + if (delegate != null && delegate instanceof Delegate) { + host.setAccessibilityDelegate(null); + host.setOnHoverListener(null); + } + } + }); } private static AccessibilityEvent newEvent(View host, int virtualViewId, int type) { @@ -79,15 +82,17 @@ private static void sendEventInternal(View host, int virtualViewId, int type) { } public static void sendEvent(final View host, final int virtualViewId, final int type) { - host.post(new Runnable() { - @Override - public void run() { - sendEventInternal(host, virtualViewId, type); - } - }); + host.post( + new Runnable() { + @Override + public void run() { + sendEventInternal(host, virtualViewId, type); + } + }); } - private static void sendTextChangedInternal(View host, int virtualViewId, String oldValue, String newValue) { + private static void sendTextChangedInternal( + View host, int virtualViewId, String oldValue, String newValue) { int i; for (i = 0; i < oldValue.length() && i < newValue.length(); ++i) { if (oldValue.charAt(i) != newValue.charAt(i)) { @@ -97,7 +102,8 @@ private static void sendTextChangedInternal(View host, int virtualViewId, String if (i >= oldValue.length() && i >= newValue.length()) { return; // Text did not change } - AccessibilityEvent e = newEvent(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); + AccessibilityEvent e = + newEvent(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); e.setBeforeText(oldValue); e.getText().add(newValue); int firstDifference = i; @@ -116,17 +122,24 @@ private static void sendTextChangedInternal(View host, int virtualViewId, String sendCompletedEvent(host, e); } - public static void sendTextChanged(final View host, final int virtualViewId, final String oldValue, final String newValue) { - host.post(new Runnable() { - @Override - public void run() { - sendTextChangedInternal(host, virtualViewId, oldValue, newValue); - } - }); + public static void sendTextChanged( + final View host, + final int virtualViewId, + final String oldValue, + final String newValue) { + host.post( + new Runnable() { + @Override + public void run() { + sendTextChangedInternal(host, virtualViewId, oldValue, newValue); + } + }); } - private static void sendTextSelectionChangedInternal(View host, int virtualViewId, String text, int start, int end) { - AccessibilityEvent e = newEvent(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); + private static void sendTextSelectionChangedInternal( + View host, int virtualViewId, String text, int start, int end) { + AccessibilityEvent e = + newEvent(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); e.getText().add(text); e.setFromIndex(start); e.setToIndex(end); @@ -134,40 +147,92 @@ private static void sendTextSelectionChangedInternal(View host, int virtualViewI sendCompletedEvent(host, e); } - public static void sendTextSelectionChanged(final View host, final int virtualViewId, final String text, final int start, final int end) { - host.post(new Runnable() { - @Override - public void run() { - sendTextSelectionChangedInternal(host, virtualViewId, text, start, end); - } - }); + public static void sendTextSelectionChanged( + final View host, + final int virtualViewId, + final String text, + final int start, + final int end) { + host.post( + new Runnable() { + @Override + public void run() { + sendTextSelectionChangedInternal(host, virtualViewId, text, start, end); + } + }); } - private static void sendTextTraversedInternal(View host, int virtualViewId, int granularity, boolean forward, int segmentStart, int segmentEnd) { - AccessibilityEvent e = newEvent(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); + private static void sendTextTraversedInternal( + View host, + int virtualViewId, + int granularity, + boolean forward, + int segmentStart, + int segmentEnd) { + AccessibilityEvent e = + newEvent( + host, + virtualViewId, + AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); e.setMovementGranularity(granularity); - e.setAction(forward ? AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY : AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); + e.setAction( + forward + ? AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY + : AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); e.setFromIndex(segmentStart); e.setToIndex(segmentEnd); sendCompletedEvent(host, e); } - public static void sendTextTraversed(final View host, final int virtualViewId, final int granularity, final boolean forward, final int segmentStart, final int segmentEnd) { - host.post(new Runnable() { - @Override - public void run() { - sendTextTraversedInternal(host, virtualViewId, granularity, forward, segmentStart, segmentEnd); - } - }); + public static void sendTextTraversed( + final View host, + final int virtualViewId, + final int granularity, + final boolean forward, + final int segmentStart, + final int segmentEnd) { + host.post( + new Runnable() { + @Override + public void run() { + sendTextTraversedInternal( + host, + virtualViewId, + granularity, + forward, + segmentStart, + segmentEnd); + } + }); } - private static native boolean populateNodeInfo(long adapterHandle, View host, int screenX, int screenY, int virtualViewId, AccessibilityNodeInfo nodeInfo); + private static native boolean populateNodeInfo( + long adapterHandle, + View host, + int screenX, + int screenY, + int virtualViewId, + AccessibilityNodeInfo nodeInfo); + private static native int getInputFocus(long adapterHandle); + private static native int getVirtualViewAtPoint(long adapterHandle, float x, float y); + private static native boolean performAction(long adapterHandle, int virtualViewId, int action); - private static native boolean setTextSelection(long adapterHandle, View host, int virtualViewId, int anchor, int focus); - private static native boolean collapseTextSelection(long adapterHandle, View host, int virtualViewId); - private static native boolean traverseText(long adapterHandle, View host, int virtualViewId, int granularity, boolean forward, boolean extendSelection); + + private static native boolean setTextSelection( + long adapterHandle, View host, int virtualViewId, int anchor, int focus); + + private static native boolean collapseTextSelection( + long adapterHandle, View host, int virtualViewId); + + private static native boolean traverseText( + long adapterHandle, + View host, + int virtualViewId, + int granularity, + boolean forward, + boolean extendSelection); @Override public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) { @@ -184,7 +249,8 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } nodeInfo.setPackageName(host.getContext().getPackageName()); nodeInfo.setVisibleToUser(true); - if (!populateNodeInfo(adapterHandle, host, location[0], location[1], virtualViewId, nodeInfo)) { + if (!populateNodeInfo( + adapterHandle, host, location[0], location[1], virtualViewId, nodeInfo)) { nodeInfo.recycle(); return null; } @@ -201,39 +267,71 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { @Override public boolean performAction(int virtualViewId, int action, Bundle arguments) { switch (action) { - case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: - accessibilityFocus = virtualViewId; - host.invalidate(); - sendEventInternal(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); - return true; - case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: - if (accessibilityFocus == virtualViewId) { - accessibilityFocus = AccessibilityNodeProvider.HOST_VIEW_ID; - } - host.invalidate(); - sendEventInternal(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); - return true; - case AccessibilityNodeInfo.ACTION_SET_SELECTION: - if (!(arguments != null && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT) && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT))) { - return Delegate.collapseTextSelection(adapterHandle, host, virtualViewId); - } - int anchor = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT); - int focus = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT); - return Delegate.setTextSelection(adapterHandle, host, virtualViewId, anchor, focus); - case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: - case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: - int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); - boolean forward = (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY); - boolean extendSelection = arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); - return Delegate.traverseText(adapterHandle, host, virtualViewId, granularity, forward, extendSelection); + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + accessibilityFocus = virtualViewId; + host.invalidate(); + sendEventInternal( + host, + virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + return true; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + if (accessibilityFocus == virtualViewId) { + accessibilityFocus = AccessibilityNodeProvider.HOST_VIEW_ID; + } + host.invalidate(); + sendEventInternal( + host, + virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + return true; + case AccessibilityNodeInfo.ACTION_SET_SELECTION: + if (!(arguments != null + && arguments.containsKey( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT) + && arguments.containsKey( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT))) { + return Delegate.collapseTextSelection( + adapterHandle, host, virtualViewId); + } + int anchor = + arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT); + int focus = + arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT); + return Delegate.setTextSelection( + adapterHandle, host, virtualViewId, anchor, focus); + case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: + case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: + int granularity = + arguments.getInt( + AccessibilityNodeInfo + .ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); + boolean forward = + (action + == AccessibilityNodeInfo + .ACTION_NEXT_AT_MOVEMENT_GRANULARITY); + boolean extendSelection = + arguments.getBoolean( + AccessibilityNodeInfo + .ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); + return Delegate.traverseText( + adapterHandle, + host, + virtualViewId, + granularity, + forward, + extendSelection); } if (!Delegate.performAction(adapterHandle, virtualViewId, action)) { return false; } switch (action) { - case AccessibilityNodeInfo.ACTION_CLICK: - sendEventInternal(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); - break; + case AccessibilityNodeInfo.ACTION_CLICK: + sendEventInternal( + host, virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); + break; } return true; } @@ -241,20 +339,24 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { @Override public AccessibilityNodeInfo findFocus(int focusType) { switch (focusType) { - case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: { - AccessibilityNodeInfo result = createAccessibilityNodeInfo(accessibilityFocus); - if (result != null && result.isAccessibilityFocused()) { - return result; + case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: + { + AccessibilityNodeInfo result = + createAccessibilityNodeInfo(accessibilityFocus); + if (result != null && result.isAccessibilityFocused()) { + return result; + } + break; } - break; - } - case AccessibilityNodeInfo.FOCUS_INPUT: { - AccessibilityNodeInfo result = createAccessibilityNodeInfo(getInputFocus(adapterHandle)); - if (result != null && result.isFocused()) { - return result; + case AccessibilityNodeInfo.FOCUS_INPUT: + { + AccessibilityNodeInfo result = + createAccessibilityNodeInfo(getInputFocus(adapterHandle)); + if (result != null && result.isFocused()) { + return result; + } + break; } - break; - } } return null; } @@ -264,25 +366,25 @@ public AccessibilityNodeInfo findFocus(int focusType) { @Override public boolean onHover(View v, MotionEvent event) { switch (event.getAction()) { - case MotionEvent.ACTION_HOVER_ENTER: - case MotionEvent.ACTION_HOVER_MOVE: - int newId = getVirtualViewAtPoint(adapterHandle, event.getX(), event.getY()); - if (newId != hoverId) { - if (newId != AccessibilityNodeProvider.HOST_VIEW_ID) { - sendEventInternal(v, newId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + int newId = getVirtualViewAtPoint(adapterHandle, event.getX(), event.getY()); + if (newId != hoverId) { + if (newId != AccessibilityNodeProvider.HOST_VIEW_ID) { + sendEventInternal(v, newId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + } + if (hoverId != AccessibilityNodeProvider.HOST_VIEW_ID) { + sendEventInternal(v, hoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + hoverId = newId; } + break; + case MotionEvent.ACTION_HOVER_EXIT: if (hoverId != AccessibilityNodeProvider.HOST_VIEW_ID) { sendEventInternal(v, hoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + hoverId = AccessibilityNodeProvider.HOST_VIEW_ID; } - hoverId = newId; - } - break; - case MotionEvent.ACTION_HOVER_EXIT: - if (hoverId != AccessibilityNodeProvider.HOST_VIEW_ID) { - sendEventInternal(v, hoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); - hoverId = AccessibilityNodeProvider.HOST_VIEW_ID; - } - break; + break; } return true; } From d7f1bf5559bdd8e293290703c743cee196624eeb Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 30 Dec 2024 17:16:29 -0600 Subject: [PATCH 36/46] Fix the clippy warning --- platforms/android/src/node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index ae146e3c..b73fb3f7 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -16,7 +16,7 @@ use crate::{filters::filter, util::*}; pub(crate) struct NodeWrapper<'a>(pub(crate) &'a Node<'a>); -impl<'a> NodeWrapper<'a> { +impl NodeWrapper<'_> { fn is_editable(&self) -> bool { self.0.is_text_input() && !self.0.is_read_only() } From 72a0cccd45afd1b3a12ff59c17c1d13c6773edc0 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 23 Feb 2025 10:51:01 -0600 Subject: [PATCH 37/46] Derive or implement `Debug` on public types --- platforms/android/src/adapter.rs | 4 ++-- platforms/android/src/inject.rs | 23 +++++++++++++++++++++++ platforms/android/src/util.rs | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 0539b395..08a6e372 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -184,7 +184,7 @@ impl TreeChangeHandler for AdapterChangeHandler<'_, '_, '_, '_> { const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0); -#[derive(Default)] +#[derive(Debug, Default)] enum State { #[default] Inactive, @@ -238,7 +238,7 @@ fn update_tree( tree.update_and_process_changes(update, &mut handler); } -#[derive(Default)] +#[derive(Debug, Default)] pub struct Adapter { node_id_map: NodeIdMap, state: State, diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index 7df41352..9e5a0b6a 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -21,6 +21,7 @@ use once_cell::sync::OnceCell; use std::{ collections::BTreeMap, ffi::c_void, + fmt::{Debug, Formatter}, sync::{ atomic::{AtomicI64, Ordering}, Arc, Mutex, Weak, @@ -35,6 +36,16 @@ struct InnerInjectingAdapter { action_handler: Box, } +impl Debug for InnerInjectingAdapter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InnerInjectingAdapter") + .field("adapter", &self.adapter) + .field("activation_handler", &"ActivationHandler") + .field("action_handler", &"ActionHandler") + .finish() + } +} + impl InnerInjectingAdapter { fn populate_node_info( &mut self, @@ -369,6 +380,18 @@ pub struct InjectingAdapter { inner: Arc>, } +impl Debug for InjectingAdapter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InnerInjectingAdapter") + .field("vm", &self.vm) + .field("delegate_class", &self.delegate_class) + .field("host", &"WeakRef") + .field("handle", &self.handle) + .field("inner", &self.inner) + .finish() + } +} + impl InjectingAdapter { pub fn new( env: &mut JNIEnv, diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 77a58e22..1a52ee37 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -24,7 +24,7 @@ pub(crate) const MOVEMENT_GRANULARITY_WORD: jint = 1 << 1; pub(crate) const MOVEMENT_GRANULARITY_LINE: jint = 1 << 2; pub(crate) const MOVEMENT_GRANULARITY_PARAGRAPH: jint = 1 << 3; -#[derive(Default)] +#[derive(Debug, Default)] pub(crate) struct NodeIdMap { java_to_accesskit: HashMap, accesskit_to_java: HashMap, From f68ff4575ff7f447244aeceacf9b186f33a877c7 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 23 Feb 2025 10:58:38 -0600 Subject: [PATCH 38/46] Fix struct doc comments --- platforms/android/src/adapter.rs | 24 +++++++++++++----------- platforms/android/src/inject.rs | 1 + 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 08a6e372..c232cc9e 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -238,17 +238,13 @@ fn update_tree( tree.update_and_process_changes(update, &mut handler); } -#[derive(Debug, Default)] -pub struct Adapter { - node_id_map: NodeIdMap, - state: State, -} - -/// Low-level AccessKit adapter for Android. This layer provides maximum -/// flexibility in the application threading model, the interface between -/// Java and native code, and the implementation of action callbacks, -/// at the expense of requiring its caller to provide glue code. For a -/// higher-level implementation built on this type, see [`InjectingAdapter`]. +/// Low-level AccessKit adapter for Android. +/// +/// This layer provides maximum flexibility in the application threading +/// model, the interface between Java and native code, and the implementation +/// of action callbacks, at the expense of requiring its caller to provide +/// glue code. For a higher-level implementation built on this type, see +/// [`InjectingAdapter`]. /// /// Several of this type's functions have a `callback_class` parameter. /// The reference implementation of the duck-typed contract for this Java class @@ -264,6 +260,12 @@ pub struct Adapter { /// a Java object whose class must derive from `android.view.View`. /// /// [`InjectingAdapter`]: crate::InjectingAdapter +#[derive(Debug, Default)] +pub struct Adapter { + node_id_map: NodeIdMap, + state: State, +} + impl Adapter { /// If and only if the tree has been initialized, call the provided function /// and apply the resulting update. Note: If the caller's implementation of diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index 9e5a0b6a..9402e13f 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -364,6 +364,7 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { /// High-level AccessKit Android adapter that injects itself into an Android /// view without requiring the view class to be modified for accessibility. +/// /// This depends on the Java `dev.accesskit.android.Delegate` class, the source /// code for which is in this crate's `java` directory. If the `embedded-dex` /// feature is enabled, then that class is loaded from a prebuilt `.dex` file From 928d2b24a425beee934d1f4cd8de42de2950f58d Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 23 Feb 2025 11:02:37 -0600 Subject: [PATCH 39/46] Fix copyright year --- platforms/android/java/dev/accesskit/android/Delegate.java | 2 +- platforms/android/src/adapter.rs | 2 +- platforms/android/src/filters.rs | 2 +- platforms/android/src/inject.rs | 2 +- platforms/android/src/lib.rs | 2 +- platforms/android/src/node.rs | 2 +- platforms/android/src/util.rs | 2 +- platforms/winit/src/platform_impl/android.rs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 03dd5a10..3abca62c 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -1,4 +1,4 @@ -// Copyright 2024 The AccessKit Authors. All rights reserved. +// Copyright 2025 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. diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index c232cc9e..a80a3679 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The AccessKit Authors. All rights reserved. +// Copyright 2025 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. diff --git a/platforms/android/src/filters.rs b/platforms/android/src/filters.rs index d4ba0505..40b64ff2 100644 --- a/platforms/android/src/filters.rs +++ b/platforms/android/src/filters.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The AccessKit Authors. All rights reserved. +// Copyright 2025 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. diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index 9402e13f..ebe399be 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The AccessKit Authors. All rights reserved. +// Copyright 2025 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. diff --git a/platforms/android/src/lib.rs b/platforms/android/src/lib.rs index d1cbe8b2..0e9af77f 100644 --- a/platforms/android/src/lib.rs +++ b/platforms/android/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The AccessKit Authors. All rights reserved. +// Copyright 2025 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. diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index b73fb3f7..be68aefa 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The AccessKit Authors. All rights reserved. +// Copyright 2025 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. diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 1a52ee37..b20b78fe 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The AccessKit Authors. All rights reserved. +// Copyright 2025 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. diff --git a/platforms/winit/src/platform_impl/android.rs b/platforms/winit/src/platform_impl/android.rs index f7bb2f80..6de4310f 100644 --- a/platforms/winit/src/platform_impl/android.rs +++ b/platforms/winit/src/platform_impl/android.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The AccessKit Authors. All rights reserved. +// Copyright 2025 The AccessKit Authors. All rights reserved. // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file). From cbc60c7d38b3850be2eb67671996a2477955abf6 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 23 Feb 2025 11:17:02 -0600 Subject: [PATCH 40/46] Update READMEs --- README.md | 1 + common/README.md | 1 + platforms/android/README.md | 7 +++++++ 3 files changed, 9 insertions(+) diff --git a/README.md b/README.md index 813fc3b9..07919cc4 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ The current released platform adapters are all at rough feature parity. They don The following platform adapters are currently available: +* [Android adapter](https://crates.io/crates/accesskit_android): This adapter implements the Java-based Android accessibility API. * [macOS adapter](https://crates.io/crates/accesskit_macos): This adapter implements the NSAccessibility protocols in the AppKit framework. * [Unix adapter](https://crates.io/crates/accesskit_unix): This adapter implements the AT-SPI D-Bus interfaces, using [zbus](https://github.com/dbus2/zbus), a pure-Rust implementation of D-Bus. * [Windows adapter](https://crates.io/crates/accesskit_windows): This adapter implements UI Automation, the current Windows accessibility API. diff --git a/common/README.md b/common/README.md index a6264ee0..b1fb3666 100644 --- a/common/README.md +++ b/common/README.md @@ -7,6 +7,7 @@ To use AccessKit in your application or toolkit, you will also need a platform a * [accesskit_windows](https://crates.io/crates/accesskit_windows): exposes an AccessKit tree on Windows using the UI Automation API * [accesskit_macos](https://crates.io/crates/accesskit_macos): exposes an AccessKit tree on MacOS through the Cocoa `NSAccessibility` protocol * [accesskit_unix](https://crates.io/crates/accesskit_unix): exposes an AccessKit tree on Linux and Unix systems through the AT-SPI protocol +* [accesskit_android](https://crates.io/crates/accesskit_android): exposes an AccessKit tree on Android through the Java-based Android accessibility API * [accesskit_winit](https://crates.io/crates/accesskit_winit): wraps other platform adapters for use with the [winit](https://crates.io/crates/winit) windowing library Some platform adapters include simple examples. diff --git a/platforms/android/README.md b/platforms/android/README.md index 9628ede2..2526b146 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -1,3 +1,10 @@ # AccessKit Android adapter This is the Android adapter for [AccessKit](https://accesskit.dev/). + +This adapter is implemented in two layers: + +* The `Adapter` struct is the core low-level adapter. It provides maximum flexibility in the application threading model, the interface between Java and native code, and the implementation of action callbacks, at the expense of requiring its caller to provide glue code. +* 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. From a5a9cfeab43f57dff175dbe8728f1825c63bc536 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 24 Feb 2025 06:57:30 -0600 Subject: [PATCH 41/46] Try to add a CI job for verifying the committed .dex file --- .github/workflows/ci.yml | 18 ++++++++++++++++++ platforms/android/build-dex.sh | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cff9fd83..6eb5ed2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,3 +81,21 @@ jobs: - name: cargo test -p accesskit_windows if: matrix.os == 'windows-2019' run: cargo test -p accesskit_windows + + check-android-dex: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - uses: android-actions/setup-android@v2 + with: + packages: 'platform-tools tools platforms;android-30 build-tools;33.0.2' + + - run: cp platforms/android/classes.dex platforms/android/classes.dex.orig + - run: ./platforms/android/build-dex.sh + - run: cmp platforms/android/classes.dex.orig platforms/android/classes.dex diff --git a/platforms/android/build-dex.sh b/platforms/android/build-dex.sh index b21c9b5c..71b46cef 100755 --- a/platforms/android/build-dex.sh +++ b/platforms/android/build-dex.sh @@ -1,5 +1,6 @@ -#!/bin/sh -set -e +#!/usr/bin/env bash +set -e -u -o pipefail +cd `dirname $0` ANDROID_JAR=$ANDROID_HOME/platforms/android-30/android.jar find java -name '*.class' -delete javac --source 8 --target 8 --boot-class-path $ANDROID_JAR -Xlint:deprecation `find java -name '*.java'` From cb99b1cf587a445e4ec7b0ecebd9c540bb6a0ff3 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 24 Feb 2025 07:08:14 -0600 Subject: [PATCH 42/46] try again --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eb5ed2f..3e22af54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,11 +91,9 @@ jobs: with: distribution: 'temurin' java-version: '17' - - uses: android-actions/setup-android@v2 - with: - packages: 'platform-tools tools platforms;android-30 build-tools;33.0.2' - + - run: sdkmanager platforms;android-30 + - run: sdkmanager build-tools;33.0.2 - run: cp platforms/android/classes.dex platforms/android/classes.dex.orig - run: ./platforms/android/build-dex.sh - run: cmp platforms/android/classes.dex.orig platforms/android/classes.dex From 1ab205d2784dd792d08528fd557bbf6072920683 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 24 Feb 2025 07:13:51 -0600 Subject: [PATCH 43/46] forgot quoting --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e22af54..a883f8d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,8 +92,8 @@ jobs: distribution: 'temurin' java-version: '17' - uses: android-actions/setup-android@v2 - - run: sdkmanager platforms;android-30 - - run: sdkmanager build-tools;33.0.2 + - run: sdkmanager "platforms;android-30" + - run: sdkmanager "build-tools;33.0.2" - run: cp platforms/android/classes.dex platforms/android/classes.dex.orig - run: ./platforms/android/build-dex.sh - run: cmp platforms/android/classes.dex.orig platforms/android/classes.dex From adee2fb96b255ceafaadb387930866f5d9088abe Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 24 Feb 2025 07:26:22 -0600 Subject: [PATCH 44/46] Add CI step to check Java code formatting --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a883f8d1..33e5b783 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 with: distribution: 'temurin' @@ -97,3 +96,11 @@ jobs: - run: cp platforms/android/classes.dex platforms/android/classes.dex.orig - run: ./platforms/android/build-dex.sh - run: cmp platforms/android/classes.dex.orig platforms/android/classes.dex + + check-java-formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: axel-op/googlejavaformat-action@4 + with: + args: "--aosp --set-exit-if-changed" From cdb29d26e34800f4b40d1a90e7e8f671e94a6bbd Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 24 Feb 2025 07:30:45 -0600 Subject: [PATCH 45/46] Fix action reference --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e5b783..b21abd49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: axel-op/googlejavaformat-action@4 + - uses: axel-op/googlejavaformat-action@v4 with: args: "--aosp --set-exit-if-changed" From 8de91b03a3f89f03cf740ae2bf4c503e124a7d56 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Mon, 24 Feb 2025 12:28:00 -0600 Subject: [PATCH 46/46] Merge Java formatting check into the existing fmt job --- .github/workflows/ci.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b21abd49..fc26698f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,11 @@ jobs: - name: cargo fmt run: cargo fmt --all -- --check + - name: check Java formatting + uses: axel-op/googlejavaformat-action@v4 + with: + args: "--aosp --set-exit-if-changed" + cargo-deny: runs-on: ubuntu-22.04 steps: @@ -96,11 +101,3 @@ jobs: - run: cp platforms/android/classes.dex platforms/android/classes.dex.orig - run: ./platforms/android/build-dex.sh - run: cmp platforms/android/classes.dex.orig platforms/android/classes.dex - - check-java-formatting: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: axel-op/googlejavaformat-action@v4 - with: - args: "--aosp --set-exit-if-changed"