Commit 78d7ee38 authored by Mark Schillaci's avatar Mark Schillaci Committed by Commit Bot

Alternative approach to aria-invalid content on Android

This CL implements an alternative approach to aria-invalid
content on Android. The current implementation, see here:

https://chromium-review.googlesource.com/c/chromium/src/+/1320219

uses an ElapsedTimer to ensure that content invalid is set on
a focused field only during the first 6 seconds after the field
was focused. This prevents the user from hearing the invalid
content after each letter typed beyond initial 6 seconds, since
it is overwhelming to hear so many warnings.

The new implementation uses a debounce approach and instead will
communicate that an item is content invalid at any time, but at
most once per 3 seconds. This is closer to a "slow drip", which
will allow the user to always know the content is invalid, but
without so many utterances.

This new approach also shifts the code to Java and tracks only
the currently focused virtualViewId and the time index of the
most recent utterance, as opposed to the ElapsedTimer approach.
This eliminates all ElapsedTimer objects from the
browser_accessibility_android nodes.


AX-Relnotes: Throttled the frequency of invalid content warnings on Android.
Change-Id: I1232d70150b0eb6a68737bce9d51eb67834a5c1c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2354836
Commit-Queue: Mark Schillaci <mschillaci@google.com>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: default avatarAkihiro Ota <akihiroota@chromium.org>
Cr-Commit-Position: refs/heads/master@{#798768}
parent f8f5b182
......@@ -55,8 +55,6 @@ enum { ANDROID_VIEW_ACCESSIBILITY_RANGE_TYPE_FLOAT = 1 };
namespace content {
const float kContentInvalidTimeoutMillisecs = 6000.0;
// static
BrowserAccessibility* BrowserAccessibility::Create() {
return new BrowserAccessibilityAndroid();
......@@ -176,20 +174,6 @@ bool BrowserAccessibilityAndroid::IsCollectionItem() const {
}
bool BrowserAccessibilityAndroid::IsContentInvalid() const {
if (IsFocused()) {
// When a node has focus, only report that it's invalid for a short period
// of time. Otherwise it's annoying to hear the invalid message every time
// a character is entered.
if (content_invalid_timer_.Elapsed().InMillisecondsF() <
kContentInvalidTimeoutMillisecs) {
bool invalid_state =
HasIntAttribute(ax::mojom::IntAttribute::kInvalidState) &&
GetData().GetInvalidState() != ax::mojom::InvalidState::kFalse;
if (invalid_state)
return true;
}
return false;
}
return HasIntAttribute(ax::mojom::IntAttribute::kInvalidState) &&
GetData().GetInvalidState() != ax::mojom::InvalidState::kFalse;
}
......@@ -1905,8 +1889,4 @@ base::string16 BrowserAccessibilityAndroid::GetContentInvalidErrorMessage()
return base::string16();
}
void BrowserAccessibilityAndroid::ResetContentInvalidTimer() {
content_invalid_timer_ = base::ElapsedTimer();
}
} // namespace content
......@@ -13,7 +13,6 @@
#include "base/android/scoped_java_ref.h"
#include "base/macros.h"
#include "base/timer/elapsed_timer.h"
#include "content/browser/accessibility/browser_accessibility.h"
#include "ui/accessibility/platform/ax_platform_node.h"
......@@ -162,11 +161,6 @@ class CONTENT_EXPORT BrowserAccessibilityAndroid : public BrowserAccessibility {
void GetSuggestions(std::vector<int>* suggestion_starts,
std::vector<int>* suggestion_ends) const;
// Used to keep track of when to stop reporting content_invalid.
// Timer only applies if node has focus.
void ResetContentInvalidTimer();
base::ElapsedTimer content_invalid_timer_ = base::ElapsedTimer();
private:
// This gives BrowserAccessibility::Create access to the class constructor.
friend class BrowserAccessibility;
......
......@@ -129,7 +129,6 @@ void BrowserAccessibilityManagerAndroid::FireFocusEvent(
return;
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(node);
android_node->ResetContentInvalidTimer();
wcax->HandleFocusChanged(android_node->unique_id());
}
......
......@@ -809,8 +809,8 @@ jboolean WebContentsAccessibilityAndroid::PopulateAccessibilityNodeInfo(
UpdateAccessibilityNodeInfoBoundsRect(env, obj, info, unique_id, node);
Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoAttributes(
env, obj, info, node->CanOpenPopup(), node->IsContentInvalid(),
node->IsDismissable(), node->IsMultiLine(), node->AndroidInputType(),
env, obj, info, node->CanOpenPopup(), node->IsDismissable(),
node->IsMultiLine(), node->AndroidInputType(),
node->AndroidLiveRegionType(),
base::android::ConvertUTF16ToJavaString(
env, node->GetContentInvalidErrorMessage()));
......
......@@ -57,6 +57,7 @@ import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.ui.base.WindowAndroid;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
......@@ -129,6 +130,10 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
private static final int ACCESSIBILITY_EVENT_DELAY_DEFAULT = 100;
private static final int ACCESSIBILITY_EVENT_DELAY_HOVER = 50;
// Throttle time for content invalid utterances. Content invalid will only be announced at most
// once per this time interval in milliseconds for a given focused node.
private static final int CONTENT_INVALID_THROTTLE_DELAY = 3000;
private static SparseArray<AccessibilityAction> sAccessibilityActionMap =
new SparseArray<AccessibilityAction>();
......@@ -187,6 +192,11 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
private String mSystemLanguageTag;
private BroadcastReceiver mBroadcastReceiver;
// These track the last focused content invalid view id and the last time we reported content
// invalid for that node. Used to ensure we report content invalid on a node once per interval.
private int mLastContentInvalidViewId;
private long mLastContentInvalidUtteranceTime;
/**
* Create a WebContentsAccessibilityImpl object.
*/
......@@ -1368,7 +1378,6 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
node.setCheckable(checkable);
node.setChecked(checked);
node.setClickable(clickable);
node.setContentInvalid(contentInvalid);
node.setEnabled(enabled);
node.setFocusable(focusable);
node.setFocused(focused);
......@@ -1377,6 +1386,30 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
node.setSelected(selected);
node.setVisibleToUser(visibleToUser);
// In the special case that we have invalid content on a focused field, we only want to
// report that to the user at most once per {@link CONTENT_INVALID_THROTTLE_DELAY} time
// interval, to be less jarring to the user.
if (contentInvalid && focused) {
if (virtualViewId == mLastContentInvalidViewId) {
// If we are focused on the same node as before, check if it has been longer than
// our delay since our last utterance, and if so, report invalid content and update
// our last reported time, otherwise suppress reporting content invalid.
if (Calendar.getInstance().getTimeInMillis() - mLastContentInvalidUtteranceTime
>= CONTENT_INVALID_THROTTLE_DELAY) {
mLastContentInvalidUtteranceTime = Calendar.getInstance().getTimeInMillis();
node.setContentInvalid(true);
}
} else {
// When we are focused on a new node, report as normal and track new time.
mLastContentInvalidViewId = virtualViewId;
mLastContentInvalidUtteranceTime = Calendar.getInstance().getTimeInMillis();
node.setContentInvalid(true);
}
} else {
// For non-focused fields we want to set contentInvalid as normal.
node.setContentInvalid(contentInvalid);
}
if (hasImage) {
Bundle bundle = node.getExtras();
bundle.putCharSequence("AccessibilityNodeInfo.hasImage", "true");
......@@ -1659,15 +1692,19 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
@CalledByNative
protected void setAccessibilityNodeInfoAttributes(AccessibilityNodeInfo node,
boolean canOpenPopup, boolean contentInvalid, boolean dismissable, boolean multiLine,
int inputType, int liveRegion, String errorMessage) {
boolean canOpenPopup, boolean dismissable, boolean multiLine, int inputType,
int liveRegion, String errorMessage) {
node.setCanOpenPopup(canOpenPopup);
node.setContentInvalid(contentInvalid);
node.setDismissable(contentInvalid);
node.setDismissable(dismissable);
node.setMultiLine(multiLine);
node.setInputType(inputType);
node.setLiveRegion(liveRegion);
node.setError(errorMessage);
// We only apply the |errorMessage| if {@link setAccessibilityNodeInfoBooleanAttributes}
// set |contentInvalid| to true based on throttle delay.
if (node.isContentInvalid()) {
node.setError(errorMessage);
}
}
@CalledByNative
......
......@@ -876,9 +876,9 @@ public class WebContentsAccessibilityTest {
AccessibilityNodeInfo textNode =
provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
Assert.assertNotEquals(textNode, null);
Assert.assertEquals(textNode.isContentInvalid(), false);
Assert.assertEquals(textNode.getError(), "");
Assert.assertNotEquals(null, textNode);
Assert.assertFalse(textNode.isContentInvalid());
Assert.assertNull(textNode.getError());
}
/**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment