fune/accessible/android/AccessibleWrap.cpp
James Teh 426407a812 Bug 1810596: When computing the text for a live region on Android, always include the text from the subtree. r=eeejay,geckoview-reviewers,ohall
Previously, we only included the subtree for roles which are allowed to be traversed during recursive name computation.
This meant that no text would be produced for live regions with roles such as status, where name computation traversal is not allowed.
Now, we first get the name to handle cases where the name is explicitly specified; e.g. images with alt text.
We then add the subtree text if the name wasn't already computed from it.

Differential Revision: https://phabricator.services.mozilla.com/D210628
2024-05-22 22:14:01 +00:00

482 lines
16 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "AccessibleWrap.h"
#include "JavaBuiltins.h"
#include "LocalAccessible-inl.h"
#include "HyperTextAccessible-inl.h"
#include "AccAttributes.h"
#include "AccEvent.h"
#include "AndroidInputType.h"
#include "DocAccessibleWrap.h"
#include "SessionAccessibility.h"
#include "TextLeafAccessible.h"
#include "TraversalRule.h"
#include "Pivot.h"
#include "Platform.h"
#include "nsAccessibilityService.h"
#include "nsEventShell.h"
#include "nsIAccessibleAnnouncementEvent.h"
#include "nsIAccessiblePivot.h"
#include "nsAccUtils.h"
#include "nsTextEquivUtils.h"
#include "nsWhitespaceTokenizer.h"
#include "RootAccessible.h"
#include "TextLeafRange.h"
#include "mozilla/a11y/PDocAccessibleChild.h"
#include "mozilla/jni/GeckoBundleUtils.h"
#include "mozilla/a11y/DocAccessibleParent.h"
#include "mozilla/Maybe.h"
// icu TRUE conflicting with java::sdk::Boolean::TRUE()
// https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265
// https://searchfox.org/mozilla-central/source/__GENERATED__/widget/android/bindings/JavaBuiltins.h#78
#ifdef TRUE
# undef TRUE
#endif
using namespace mozilla::a11y;
using mozilla::Maybe;
//-----------------------------------------------------
// construction
//-----------------------------------------------------
AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
: LocalAccessible(aContent, aDoc), mID(SessionAccessibility::kUnsetID) {
if (!IPCAccessibilityActive()) {
MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
SessionAccessibility::RegisterAccessible(this);
}
}
//-----------------------------------------------------
// destruction
//-----------------------------------------------------
AccessibleWrap::~AccessibleWrap() {}
nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
auto accessible = static_cast<AccessibleWrap*>(aEvent->GetAccessible());
NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE);
nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
NS_ENSURE_SUCCESS(rv, rv);
accessible->HandleLiveRegionEvent(aEvent);
return NS_OK;
}
void AccessibleWrap::Shutdown() {
if (!IPCAccessibilityActive()) {
MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
SessionAccessibility::UnregisterAccessible(this);
}
LocalAccessible::Shutdown();
}
bool AccessibleWrap::DoAction(uint8_t aIndex) const {
if (ActionCount()) {
return LocalAccessible::DoAction(aIndex);
}
if (mContent) {
// We still simulate a click on an accessible even if there is no
// known actions. For the sake of bad markup.
DoCommand();
return true;
}
return false;
}
Accessible* AccessibleWrap::DoPivot(Accessible* aAccessible,
int32_t aGranularity, bool aForward,
bool aInclusive) {
Accessible* pivotRoot = nullptr;
if (aAccessible->IsRemote()) {
// If this is a remote accessible provide the top level
// remote doc as the pivot root for thread safety reasons.
DocAccessibleParent* doc = aAccessible->AsRemote()->Document();
while (doc && !doc->IsTopLevel()) {
doc = doc->ParentDoc();
}
MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent");
pivotRoot = doc;
}
a11y::Pivot pivot(pivotRoot);
// Depending on the start accessible, the pivot rule will either traverse
// local or remote accessibles exclusively.
TraversalRule rule(aGranularity, aAccessible->IsLocal());
Accessible* result = aForward ? pivot.Next(aAccessible, rule, aInclusive)
: pivot.Prev(aAccessible, rule, aInclusive);
if (result && (result != aAccessible || aInclusive)) {
return result;
}
return nullptr;
}
Accessible* AccessibleWrap::ExploreByTouch(Accessible* aAccessible, float aX,
float aY) {
Accessible* root;
if (LocalAccessible* local = aAccessible->AsLocal()) {
root = local->RootAccessible();
} else {
// If this is a RemoteAccessible, provide the top level
// remote doc as the pivot root for thread safety reasons.
DocAccessibleParent* doc = aAccessible->AsRemote()->Document();
while (doc && !doc->IsTopLevel()) {
doc = doc->ParentDoc();
}
MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent");
root = doc;
}
a11y::Pivot pivot(root);
TraversalRule rule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
aAccessible->IsLocal());
Accessible* result = pivot.AtPoint(aX, aY, rule);
if (result == aAccessible) {
return nullptr;
}
return result;
}
static TextLeafPoint ToTextLeafPoint(Accessible* aAccessible, int32_t aOffset) {
if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
return ht->ToTextLeafPoint(aOffset);
}
return TextLeafPoint(aAccessible, aOffset);
}
Maybe<std::pair<int32_t, int32_t>> AccessibleWrap::NavigateText(
Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
int32_t aEndOffset, bool aForward, bool aSelect) {
int32_t startOffset = aStartOffset;
int32_t endOffset = aEndOffset;
if (startOffset == -1) {
MOZ_ASSERT(endOffset == -1,
"When start offset is unset, end offset should be too");
startOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
endOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
}
// If the accessible is an editable, set the virtual cursor position
// to its caret offset. Otherwise use the document's virtual cursor
// position as a starting offset.
if (aAccessible->State() & states::EDITABLE) {
startOffset = endOffset = aAccessible->AsHyperTextBase()->CaretOffset();
}
TextLeafRange currentRange =
TextLeafRange(ToTextLeafPoint(aAccessible, startOffset),
ToTextLeafPoint(aAccessible, endOffset));
uint16_t startBoundaryType = nsIAccessibleText::BOUNDARY_LINE_START;
uint16_t endBoundaryType = nsIAccessibleText::BOUNDARY_LINE_END;
switch (aGranularity) {
case 1: // MOVEMENT_GRANULARITY_CHARACTER
startBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
endBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
break;
case 2: // MOVEMENT_GRANULARITY_WORD
startBoundaryType = nsIAccessibleText::BOUNDARY_WORD_START;
endBoundaryType = nsIAccessibleText::BOUNDARY_WORD_END;
break;
default:
break;
}
TextLeafRange resultRange;
if (aForward) {
resultRange.SetEnd(
currentRange.End().FindBoundary(endBoundaryType, eDirNext));
resultRange.SetStart(
resultRange.End().FindBoundary(startBoundaryType, eDirPrevious));
} else {
resultRange.SetStart(
currentRange.Start().FindBoundary(startBoundaryType, eDirPrevious));
resultRange.SetEnd(
resultRange.Start().FindBoundary(endBoundaryType, eDirNext));
}
if (!resultRange.Crop(aAccessible)) {
// If the new range does not intersect at all with the given
// accessible/container this navigation has failed or reached an edge.
return Nothing();
}
if (resultRange == currentRange || resultRange.Start() == resultRange.End()) {
// If the result range equals the current range, or if the result range is
// collapsed, we failed or reached an edge.
return Nothing();
}
if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
DebugOnly<bool> ok = false;
std::tie(ok, startOffset) = ht->TransformOffset(
resultRange.Start().mAcc, resultRange.Start().mOffset, false);
MOZ_ASSERT(ok, "Accessible of range start should be in container.");
std::tie(ok, endOffset) = ht->TransformOffset(
resultRange.End().mAcc, resultRange.End().mOffset, false);
MOZ_ASSERT(ok, "Accessible range end should be in container.");
} else {
startOffset = resultRange.Start().mOffset;
endOffset = resultRange.End().mOffset;
}
return Some(std::make_pair(startOffset, endOffset));
}
uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,
uint8_t aActionCount) {
uint32_t flags = 0;
if (aState & states::CHECKABLE) {
flags |= java::SessionAccessibility::FLAG_CHECKABLE;
}
if (aState & states::CHECKED) {
flags |= java::SessionAccessibility::FLAG_CHECKED;
}
if (aState & states::INVALID) {
flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
}
if (aState & states::EDITABLE) {
flags |= java::SessionAccessibility::FLAG_EDITABLE;
}
if (aActionCount && aRole != roles::TEXT_LEAF) {
flags |= java::SessionAccessibility::FLAG_CLICKABLE;
}
if (aState & states::ENABLED) {
flags |= java::SessionAccessibility::FLAG_ENABLED;
}
if (aState & states::FOCUSABLE) {
flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
}
if (aState & states::FOCUSED) {
flags |= java::SessionAccessibility::FLAG_FOCUSED;
}
if (aState & states::MULTI_LINE) {
flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
}
if (aState & states::SELECTABLE) {
flags |= java::SessionAccessibility::FLAG_SELECTABLE;
}
if (aState & states::SELECTED) {
flags |= java::SessionAccessibility::FLAG_SELECTED;
}
if (aState & states::EXPANDABLE) {
flags |= java::SessionAccessibility::FLAG_EXPANDABLE;
}
if (aState & states::EXPANDED) {
flags |= java::SessionAccessibility::FLAG_EXPANDED;
}
if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
}
if (aRole == roles::PASSWORD_TEXT) {
flags |= java::SessionAccessibility::FLAG_PASSWORD;
}
return flags;
}
void AccessibleWrap::GetRoleDescription(role aRole, AccAttributes* aAttributes,
nsAString& aGeckoRole,
nsAString& aRoleDescription) {
if (aRole == roles::HEADING && aAttributes) {
// The heading level is an attribute, so we need that.
nsAutoString headingLevel;
if (aAttributes->GetAttribute(nsGkAtoms::level, headingLevel)) {
nsAutoString token(u"heading-");
token.Append(headingLevel);
if (LocalizeString(token, aRoleDescription)) {
return;
}
}
}
if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) {
nsAutoString xmlRoles;
if (aAttributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles)) {
nsWhitespaceTokenizer tokenizer(xmlRoles);
while (tokenizer.hasMoreTokens()) {
if (LocalizeString(tokenizer.nextToken(), aRoleDescription)) {
return;
}
}
}
}
GetAccService()->GetStringRole(aRole, aGeckoRole);
LocalizeString(aGeckoRole, aRoleDescription);
}
int32_t AccessibleWrap::AndroidClass(Accessible* aAccessible) {
return GetVirtualViewID(aAccessible) == SessionAccessibility::kNoID
? java::SessionAccessibility::CLASSNAME_WEBVIEW
: GetAndroidClass(aAccessible->Role());
}
int32_t AccessibleWrap::GetVirtualViewID(Accessible* aAccessible) {
if (aAccessible->IsLocal()) {
return static_cast<AccessibleWrap*>(aAccessible)->mID;
}
return static_cast<int32_t>(aAccessible->AsRemote()->GetWrapper());
}
void AccessibleWrap::SetVirtualViewID(Accessible* aAccessible,
int32_t aVirtualViewID) {
if (aAccessible->IsLocal()) {
static_cast<AccessibleWrap*>(aAccessible)->mID = aVirtualViewID;
} else {
aAccessible->AsRemote()->SetWrapper(static_cast<uintptr_t>(aVirtualViewID));
}
}
int32_t AccessibleWrap::GetAndroidClass(role aRole) {
#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
nameRule) \
case roles::geckoRole: \
return androidClass;
switch (aRole) {
#include "RoleMap.h"
default:
return java::SessionAccessibility::CLASSNAME_VIEW;
}
#undef ROLE
}
int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) {
if (aInputTypeAttr.EqualsIgnoreCase("email")) {
return java::sdk::InputType::TYPE_CLASS_TEXT |
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
}
if (aInputTypeAttr.EqualsIgnoreCase("number")) {
return java::sdk::InputType::TYPE_CLASS_NUMBER;
}
if (aInputTypeAttr.EqualsIgnoreCase("password")) {
return java::sdk::InputType::TYPE_CLASS_TEXT |
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD;
}
if (aInputTypeAttr.EqualsIgnoreCase("tel")) {
return java::sdk::InputType::TYPE_CLASS_PHONE;
}
if (aInputTypeAttr.EqualsIgnoreCase("text")) {
return java::sdk::InputType::TYPE_CLASS_TEXT |
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
}
if (aInputTypeAttr.EqualsIgnoreCase("url")) {
return java::sdk::InputType::TYPE_CLASS_TEXT |
java::sdk::InputType::TYPE_TEXT_VARIATION_URI;
}
return 0;
}
void AccessibleWrap::GetTextEquiv(nsString& aText) {
// 1. Start with the name, since it might have been explicitly specified.
if (Name(aText) != eNameFromSubtree) {
// 2. If the name didn't come from the subtree, add the text from the
// subtree.
if (aText.IsEmpty()) {
nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
} else {
nsAutoString subtree;
nsTextEquivUtils::GetTextEquivFromSubtree(this, subtree);
if (!subtree.IsEmpty()) {
aText.Append(' ');
aText.Append(subtree);
}
}
}
}
bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) {
auto eventType = aEvent->GetEventType();
if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED &&
eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) {
// XXX: Right now only announce text inserted events. aria-relevant=removals
// is potentially on the chopping block[1]. We also don't support editable
// text because we currently can't descern the source of the change[2].
// 1. https://github.com/w3c/aria/issues/712
// 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
return false;
}
if (aEvent->IsFromUserInput()) {
return false;
}
RefPtr<AccAttributes> attributes = new AccAttributes();
nsAccUtils::SetLiveContainerAttributes(attributes, this);
nsString live;
if (!attributes->GetAttribute(nsGkAtoms::containerLive, live)) {
return false;
}
uint16_t priority = live.EqualsIgnoreCase("assertive")
? nsIAccessibleAnnouncementEvent::ASSERTIVE
: nsIAccessibleAnnouncementEvent::POLITE;
Maybe<bool> atomic =
attributes->GetAttribute<bool>(nsGkAtoms::containerAtomic);
LocalAccessible* announcementTarget = this;
nsAutoString announcement;
if (atomic && *atomic) {
LocalAccessible* atomicAncestor = nullptr;
for (LocalAccessible* parent = announcementTarget; parent;
parent = parent->LocalParent()) {
dom::Element* element = parent->Elm();
if (element &&
nsAccUtils::ARIAAttrValueIs(element, nsGkAtoms::aria_atomic,
nsGkAtoms::_true, eCaseMatters)) {
atomicAncestor = parent;
break;
}
}
if (atomicAncestor) {
announcementTarget = atomicAncestor;
static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
}
} else {
GetTextEquiv(announcement);
}
announcement.CompressWhitespace();
if (announcement.IsEmpty()) {
return false;
}
announcementTarget->Announce(announcement, priority);
return true;
}