diff --git a/adapters/android/src/action.rs b/adapters/android/src/action.rs index 5f668e55f..21e0668b0 100644 --- a/adapters/android/src/action.rs +++ b/adapters/android/src/action.rs @@ -3,7 +3,11 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use jni::{JNIEnv, objects::JObject, sys::jint}; +use jni::{ + JNIEnv, + objects::JObject, + sys::{jfloat, jint}, +}; use crate::util::*; @@ -21,6 +25,9 @@ pub(crate) enum PlatformActionInner { forward: bool, extend_selection: bool, }, + SetProgress { + value: jfloat, + }, } pub struct PlatformAction(pub(crate) PlatformActionInner); @@ -63,6 +70,15 @@ impl PlatformAction { extend_selection, })) } + ACTION_SET_PROGRESS => { + if arguments.is_null() + || !bundle_contains_key(env, arguments, ACTION_ARGUMENT_PROGRESS_VALUE) + { + return None; + } + let value = bundle_get_float(env, arguments, ACTION_ARGUMENT_PROGRESS_VALUE); + Some(Self(PlatformActionInner::SetProgress { value })) + } _ => Some(Self(PlatformActionInner::Simple { action })), } } diff --git a/adapters/android/src/adapter.rs b/adapters/android/src/adapter.rs index ea90f068e..aa74b7877 100644 --- a/adapters/android/src/adapter.rs +++ b/adapters/android/src/adapter.rs @@ -455,42 +455,52 @@ impl Adapter { target_node, data: None, }, - ACTION_SCROLL_BACKWARD | ACTION_SCROLL_FORWARD => ActionRequest { - action: { - let node = tree_state.node_by_id(target).unwrap(); - if let Some(orientation) = node.orientation() { - match orientation { - Orientation::Horizontal => { - if action == ACTION_SCROLL_BACKWARD { - Action::ScrollLeft - } else { - Action::ScrollRight - } - } - Orientation::Vertical => { - if action == ACTION_SCROLL_BACKWARD { - Action::ScrollUp - } else { - Action::ScrollDown - } + ACTION_SCROLL_BACKWARD | ACTION_SCROLL_FORWARD => { + let node = tree_state.node_by_id(target).unwrap(); + let backward = action == ACTION_SCROLL_BACKWARD; + let is_range_control = + node.supports_increment(&filter) || node.supports_decrement(&filter); + let action = if is_range_control { + if backward { + Action::Decrement + } else { + Action::Increment + } + } else if let Some(orientation) = node.orientation() { + match orientation { + Orientation::Horizontal => { + if backward { + Action::ScrollLeft + } else { + Action::ScrollRight } } - } else if action == ACTION_SCROLL_BACKWARD { - if node.supports_action(Action::ScrollUp, &filter) { - Action::ScrollUp - } else { - Action::ScrollLeft + Orientation::Vertical => { + if backward { + Action::ScrollUp + } else { + Action::ScrollDown + } } - } else if node.supports_action(Action::ScrollDown, &filter) { - Action::ScrollDown + } + } else if backward { + if node.supports_action(Action::ScrollUp, &filter) { + Action::ScrollUp } else { - Action::ScrollRight + Action::ScrollLeft } - }, - target_tree, - target_node, - data: Some(ActionData::ScrollUnit(ScrollUnit::Page)), - }, + } else if node.supports_action(Action::ScrollDown, &filter) { + Action::ScrollDown + } else { + Action::ScrollRight + }; + ActionRequest { + action, + target_tree, + target_node, + data: (!is_range_control).then_some(ActionData::ScrollUnit(ScrollUnit::Page)), + } + } ACTION_ACCESSIBILITY_FOCUS => { self.accessibility_focus = Some(virtual_view_id); events.push(QueuedEvent::InvalidateHost); @@ -525,6 +535,29 @@ impl Adapter { Some(QueuedEvents(events)) } + fn set_progress( + &mut self, + action_handler: &mut H, + virtual_view_id: jint, + value: jfloat, + ) -> Option { + let tree = self.state.get_full_tree()?; + let tree_state = tree.state(); + let target = self.node_id_map.get_accesskit_id(virtual_view_id)?; + let (target_node, target_tree) = tree_state.locate_node(target)?; + let node = tree_state.node_by_id(target)?; + if !node.supports_action(Action::SetValue, &filter) || node.numeric_value().is_none() { + return None; + } + action_handler.do_action(ActionRequest { + action: Action::SetValue, + target_tree, + target_node, + data: Some(ActionData::NumericValue(value.into())), + }); + Some(QueuedEvents(Vec::new())) + } + fn set_text_selection_common( &mut self, action_handler: &mut H, @@ -795,6 +828,9 @@ impl Adapter { forward, extend_selection, ), + PlatformActionInner::SetProgress { value } => { + self.set_progress(action_handler, virtual_view_id, value) + } } } diff --git a/adapters/android/src/event.rs b/adapters/android/src/event.rs index 4c771c74c..f3c6c9e38 100644 --- a/adapters/android/src/event.rs +++ b/adapters/android/src/event.rs @@ -345,6 +345,9 @@ impl QueuedEvents { /// any locks required by the host view's implementations of Android /// framework callbacks. pub fn raise(self, env: &mut JNIEnv, host: &JObject) { + if self.0.is_empty() { + return; + } // The framework throws an exception if events are sent from the // UI thread while accessibility is disabled. if !is_accessibility_enabled(env, host) { diff --git a/adapters/android/src/node.rs b/adapters/android/src/node.rs index 07c907fb9..dab2029d3 100644 --- a/adapters/android/src/node.rs +++ b/adapters/android/src/node.rs @@ -74,6 +74,14 @@ impl NodeWrapper<'_> { Range::from_node(self.0) } + fn supports_set_progress(&self) -> bool { + self.range().is_some() && self.0.supports_action(Action::SetValue, &filter) + } + + fn is_range_control(&self) -> bool { + self.0.supports_increment(&filter) || self.0.supports_decrement(&filter) + } + pub(crate) fn content_description(&self) -> Option { if self.0.label_comes_from_value() { self.0.value() @@ -358,7 +366,10 @@ impl NodeWrapper<'_> { .unwrap(); let can_focus = self.is_focusable() && !self.0.is_focused(); - if self.0.is_clickable(&filter) || can_focus { + // Without a click action, TalkBack handles a double-tap on a range + // control by tapping its center which can be useful to quickly + // set a slider value in the middle. + if self.0.is_clickable(&filter) || (can_focus && !self.is_range_control()) { add_action(env, node_info, ACTION_CLICK); } if can_focus { @@ -382,11 +393,13 @@ impl NodeWrapper<'_> { } if self.0.supports_action(Action::ScrollLeft, &filter) || self.0.supports_action(Action::ScrollUp, &filter) + || self.0.supports_decrement(&filter) { add_action(env, node_info, ACTION_SCROLL_BACKWARD); } if self.0.supports_action(Action::ScrollRight, &filter) || self.0.supports_action(Action::ScrollDown, &filter) + || self.0.supports_increment(&filter) { add_action(env, node_info, ACTION_SCROLL_FORWARD); } @@ -401,6 +414,27 @@ impl NodeWrapper<'_> { ) .unwrap(); } + if self.supports_set_progress() { + let action_class = env + .find_class("android/view/accessibility/AccessibilityNodeInfo$AccessibilityAction") + .unwrap(); + let action = env + .get_static_field( + &action_class, + "ACTION_SET_PROGRESS", + "Landroid/view/accessibility/AccessibilityNodeInfo$AccessibilityAction;", + ) + .unwrap() + .l() + .unwrap(); + env.call_method( + node_info, + "addAction", + "(Landroid/view/accessibility/AccessibilityNodeInfo$AccessibilityAction;)V", + &[(&action).into()], + ) + .unwrap(); + } let live = match self.0.live() { Live::Off => LIVE_REGION_NONE, diff --git a/adapters/android/src/util.rs b/adapters/android/src/util.rs index 8868e6e3d..5abf84a2e 100644 --- a/adapters/android/src/util.rs +++ b/adapters/android/src/util.rs @@ -7,7 +7,7 @@ use accesskit_consumer::{FullNodeId, NodeRef}; use jni::{ JNIEnv, objects::{GlobalRef, JObject}, - sys::jint, + sys::{jfloat, jint}, }; use std::{collections::HashMap, sync::OnceLock}; @@ -20,6 +20,7 @@ pub(crate) const ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: jint = 1 << 9; pub(crate) const ACTION_SCROLL_FORWARD: jint = 1 << 12; pub(crate) const ACTION_SCROLL_BACKWARD: jint = 1 << 13; pub(crate) const ACTION_SET_SELECTION: jint = 1 << 17; +pub(crate) const ACTION_SET_PROGRESS: jint = 0x0102003d; pub(crate) const ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT: &str = "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT"; @@ -27,6 +28,8 @@ pub(crate) const ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN: &str = "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN"; pub(crate) const ACTION_ARGUMENT_SELECTION_START_INT: &str = "ACTION_ARGUMENT_SELECTION_START_INT"; pub(crate) const ACTION_ARGUMENT_SELECTION_END_INT: &str = "ACTION_ARGUMENT_SELECTION_END_INT"; +pub(crate) const ACTION_ARGUMENT_PROGRESS_VALUE: &str = + "android.view.accessibility.action.ARGUMENT_PROGRESS_VALUE"; pub(crate) const CONTENT_CHANGE_TYPE_SUBTREE: jint = 1 << 0; @@ -176,6 +179,19 @@ pub(crate) fn bundle_get_int(env: &mut JNIEnv, bundle: &JObject, key: &str) -> j .unwrap() } +pub(crate) fn bundle_get_float(env: &mut JNIEnv, bundle: &JObject, key: &str) -> jfloat { + let key = env.new_string(key).unwrap(); + env.call_method( + bundle, + "getFloat", + "(Ljava/lang/String;)F", + &[(&key).into()], + ) + .unwrap() + .f() + .unwrap() +} + pub(crate) fn bundle_get_bool(env: &mut JNIEnv, bundle: &JObject, key: &str) -> bool { let key = env.new_string(key).unwrap(); env.call_method(