use crate::compat::{lock, HashMap, Mutex, String, ToString};
use crate::core::ObjectId;
use crate::platform::accessibility::AccessibilityBridge;
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum A11yEventType {
AccessibilityFocused = 1,
AccessibilityTextUpdate = 2,
AccessibilityValueUpdate = 3,
AccessibilityStateUpdate = 5,
}
pub struct HarmonyAccessibilityBridge {
provider: core::sync::atomic::AtomicUsize,
names: Mutex<HashMap<ObjectId, String>>,
}
impl HarmonyAccessibilityBridge {
pub fn new() -> Self {
Self {
provider: core::sync::atomic::AtomicUsize::new(0),
names: Mutex::new(HashMap::new()),
}
}
pub fn attach_provider(&self, provider: *mut core::ffi::c_void) {
self.provider.store(provider as usize, core::sync::atomic::Ordering::Release);
}
pub fn has_provider(&self) -> bool {
self.provider.load(core::sync::atomic::Ordering::Acquire) != 0
}
fn post_event(&self, id: ObjectId, event_type: A11yEventType) -> bool {
let provider = self.provider.load(core::sync::atomic::Ordering::Acquire);
if provider == 0 {
log::debug!(
"[harmony] a11y: no ArkUI accessibility provider (no bound XComponent); \
the {event_type:?} notification for widget {id} was not posted"
);
return false;
}
let provider = provider as *mut core::ffi::c_void;
#[cfg(target_env = "ohos")]
unsafe {
let event = OH_ArkUI_CreateAccessibilityEventInfo();
if event.is_null() {
log::warn!(
"[harmony] a11y: OH_ArkUI_CreateAccessibilityEventInfo returned null; the \
{event_type:?} notification for widget {id} was not posted"
);
return false;
}
let typed = OH_ArkUI_AccessibilityEventInfo_SetEventType(event, event_type as i32);
let identified = OH_ArkUI_AccessibilityEventInfo_SetElementId(event, id as i64);
if typed != ACCESSIBILITY_RESULT_SUCCESSFUL
|| identified != ACCESSIBILITY_RESULT_SUCCESSFUL
{
log::warn!(
"[harmony] a11y: could not describe the {event_type:?} notification for \
widget {id} (setEventType={typed}, setElementId={identified})"
);
OH_ArkUI_DestoryAccessibilityEventInfo(event);
return false;
}
OH_ArkUI_SendAccessibilityAsyncEvent(provider, event, None);
OH_ArkUI_DestoryAccessibilityEventInfo(event);
}
#[cfg(not(target_env = "ohos"))]
let _ = (provider, id, event_type);
true
}
}
crate::impl_default_via_new!(HarmonyAccessibilityBridge);
impl AccessibilityBridge for HarmonyAccessibilityBridge {
fn set_accessibility_name(&self, id: ObjectId, name: &str) {
lock(&self.names).insert(id, name.to_string());
}
fn accessibility_name(&self, id: ObjectId) -> Option<String> {
lock(&self.names).get(&id).cloned()
}
fn notify_name_changed(&self, id: ObjectId) {
self.post_event(id, A11yEventType::AccessibilityTextUpdate);
}
fn notify_value_changed(&self, id: ObjectId) {
self.post_event(id, A11yEventType::AccessibilityValueUpdate);
}
fn notify_state_changed(&self, id: ObjectId) {
self.post_event(id, A11yEventType::AccessibilityStateUpdate);
}
fn notify_focus_changed(&self, id: ObjectId) {
self.post_event(id, A11yEventType::AccessibilityFocused);
}
}
#[cfg(target_env = "ohos")]
const ACCESSIBILITY_RESULT_SUCCESSFUL: i32 = 0;
#[cfg(target_env = "ohos")]
extern "C" {
fn OH_NativeXComponent_GetNativeAccessibilityProvider(
component: *mut core::ffi::c_void,
handle: *mut *mut core::ffi::c_void,
) -> i32;
fn OH_ArkUI_CreateAccessibilityEventInfo() -> *mut core::ffi::c_void;
fn OH_ArkUI_DestoryAccessibilityEventInfo(event_info: *mut core::ffi::c_void);
fn OH_ArkUI_AccessibilityEventInfo_SetEventType(
event_info: *mut core::ffi::c_void,
event_type: i32,
) -> i32;
fn OH_ArkUI_AccessibilityEventInfo_SetElementId(
event_info: *mut core::ffi::c_void,
element_id: i64,
) -> i32;
fn OH_ArkUI_SendAccessibilityAsyncEvent(
provider: *mut core::ffi::c_void,
event_info: *mut core::ffi::c_void,
callback: Option<extern "C" fn(i32)>,
);
}
pub unsafe fn attach_for_component(
bridge: &HarmonyAccessibilityBridge,
component: *mut core::ffi::c_void,
) -> bool {
if component.is_null() {
return false;
}
#[cfg(target_env = "ohos")]
{
let mut handle: *mut core::ffi::c_void = core::ptr::null_mut();
let status =
unsafe { OH_NativeXComponent_GetNativeAccessibilityProvider(component, &mut handle) };
if status != RESULT_SUCCESS || handle.is_null() {
log::warn!(
"[harmony] a11y: this XComponent exposes no accessibility provider \
(status={status}); accessibility notifications will not be posted"
);
return false;
}
bridge.attach_provider(handle);
log::info!("[harmony] a11y: accessibility provider attached to the bound XComponent");
return true;
}
#[cfg(not(target_env = "ohos"))]
{
let _ = (bridge, component);
log::debug!(
"[harmony] a11y: this build is not for OpenHarmony, so it has no accessibility \
provider to attach"
);
false
}
}
#[cfg(target_env = "ohos")]
const RESULT_SUCCESS: i32 = 0;
pub fn bridge() -> &'static HarmonyAccessibilityBridge {
static BRIDGE: crate::compat::OnceLock<HarmonyAccessibilityBridge> =
crate::compat::OnceLock::new();
BRIDGE.get_or_init(HarmonyAccessibilityBridge::new)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing_is_posted_without_a_provider() {
let bridge = HarmonyAccessibilityBridge::new();
assert!(!bridge.has_provider(), "an unbound bridge must have no provider");
assert!(
!bridge.post_event(1, A11yEventType::AccessibilityFocused),
"a notification with no provider must report that it was not posted"
);
}
#[test]
fn names_round_trip_without_a_provider() {
let bridge = HarmonyAccessibilityBridge::new();
assert_eq!(bridge.accessibility_name(7), None, "an unknown widget has no name");
bridge.set_accessibility_name(7, "Play");
assert_eq!(bridge.accessibility_name(7), Some("Play".to_string()));
bridge.set_accessibility_name(7, "Pause");
assert_eq!(
bridge.accessibility_name(7),
Some("Pause".to_string()),
"a second write must replace the first, not add a second name"
);
}
#[test]
fn attaching_for_a_null_component_is_refused() {
let bridge = HarmonyAccessibilityBridge::new();
let attached = unsafe { attach_for_component(&bridge, core::ptr::null_mut()) };
assert!(!attached);
assert!(!bridge.has_provider());
}
#[test]
fn event_type_values_match_the_sdk_header() {
assert_eq!(A11yEventType::AccessibilityFocused as i32, 1);
assert_eq!(A11yEventType::AccessibilityTextUpdate as i32, 2);
assert_eq!(A11yEventType::AccessibilityValueUpdate as i32, 3);
assert_eq!(A11yEventType::AccessibilityStateUpdate as i32, 5);
}
}