Philipp
Philipp

Reputation: 11813

Infinite recursion in Android accessibility service when traversing AccessibilityNodeInfo

Users of my app are experiencing crashes since a few days. I was able to collect a logcat on my device, with some added debugging output. So here's what's happening:

In my handleAccessibilityEvent(), I call

AccessibilityNodeInfo root = getRootInActiveWindow();
int eventWindowId = event.getWindowId();
if (ExistsNodeOrChildren(root, new WindowIdCondition(eventWindowId)))
{
}

which walks through the node tree recursively:

private boolean ExistsNodeOrChildren(AccessibilityNodeInfo n, NodeCondition condition) 
{
    Log.d(_logTag, "ExistsNodeOrChildren" + n.toString());
    if (n == null) return false;
    if (condition.check(n))
        return true;
    for (int i = 0; i < n.getChildCount(); i++)
    {
        Log.d(_logTag, "ExistsNodeOrChildren child" + i);
        if (ExistsNodeOrChildren(n.getChild(i), condition))
            return true;
    }
    return false;
}

The NodeCondition is a simple interface for predicate-like checks on a node.

ExistsNodeOrChildren runs into infinite recursion. From the logs (see below), it looks to me like a node is returning itself as its own child.

My main question is: Is this allowed and must be handeled in my Accessibility service? It seems like this was introduced with some recent updates, maybe concerning WebView or Chrome.

If yes: How should I compare AccessibilityNodeInfo objects to check if they refer to the same node?

(latest events are on top)

02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildrenandroid.view.accessibility.AccessibilityNodeInfo@c723; boundsInParent: Rect(0, 0 - 1080, 1605); boundsInScreen: Rect(0, 210 - 1080, 1668); packageName: com.opera.mini.native; className: android.webkit.WebView; text: null; error: null; maxTextLength: -1; contentDescription: null; viewIdResName: null; checkable: false; checked: false; focusable: false; focused: false; selected: false; clickable: false; longClickable: false; contextClickable: false; enabled: true; password: false; scrollable: false; importantForAccessibility: false; actions: null
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildren child0
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildrenandroid.view.accessibility.AccessibilityNodeInfo@c723; boundsInParent: Rect(0, 0 - 1080, 1605); boundsInScreen: Rect(0, 210 - 1080, 1668); packageName: com.opera.mini.native; className: android.webkit.WebView; text: null; error: null; maxTextLength: -1; contentDescription: null; viewIdResName: null; checkable: false; checked: false; focusable: false; focused: false; selected: false; clickable: false; longClickable: false; contextClickable: false; enabled: true; password: false; scrollable: false; importantForAccessibility: false; actions: null
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildren child0
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildrenandroid.view.accessibility.AccessibilityNodeInfo@c723; boundsInParent: Rect(0, 0 - 1080, 1605); boundsInScreen: Rect(0, 210 - 1080, 1668); packageName: com.opera.mini.native; className: android.webkit.WebView; text: null; error: null; maxTextLength: -1; contentDescription: null; viewIdResName: null; checkable: false; checked: false; focusable: false; focused: false; selected: false; clickable: false; longClickable: false; contextClickable: false; enabled: true; password: false; scrollable: false; importantForAccessibility: false; actions: null
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildren child0
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildren child0
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildren child0
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildrenandroid.view.accessibility.AccessibilityNodeInfo@c723; boundsInParent: Rect(0, 0 - 1080, 1605); boundsInScreen: Rect(0, 210 - 1080, 1668); packageName: com.opera.mini.native; className: android.webkit.WebView; text: null; error: null; maxTextLength: -1; contentDescription: null; viewIdResName: null; checkable: false; checked: false; focusable: false; focused: false; selected: false; clickable: false; longClickable: false; contextClickable: false; enabled: true; password: false; scrollable: false; importantForAccessibility: false; actions: null
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildrenandroid.view.accessibility.AccessibilityNodeInfo@c723; boundsInParent: Rect(0, 0 - 1080, 1605); boundsInScreen: Rect(0, 210 - 1080, 1668); packageName: com.opera.mini.native; className: android.webkit.WebView; text: null; error: null; maxTextLength: -1; contentDescription: null; viewIdResName: null; checkable: false; checked: false; focusable: false; focused: false; selected: false; clickable: false; longClickable: false; contextClickable: false; enabled: true; password: false; scrollable: false; importantForAccessibility: false; actions: null
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildren child0
02-08 10:49:01.074  Google Pixel    Debug   9314    KP2AAF  ExistsNodeOrChildren child0

Upvotes: 1

Views: 831

Answers (1)

MobA11y
MobA11y

Reputation: 18870

If this isn't caused by some other recursion in your NodeCondition function (which you probably should have provided), and this is actually a bug in the Android OS virtual view hierarchy, you could attempt to remediate it by doing something like this:

private boolean ExistsNodeOrChildren(AccessibilityNodeInfo n, NodeCondition condition) {

    //For God sake do this first if you think n might actually be null!!!
    //Or just don't do it, and let n.toString() throw a NPE. (BAD IDEA)
    if (n == null) return false; 

    Log.d(_logTag, "ExistsNodeOrChildren" + n.toString());

    //NOTE: This could also cause recursion, you didn't provide this code.
    if (condition.check(n)) return true;

    for (int i = 0; i < n.getChildCount(); i++) {
        AccessibilityNodeInfo child = n.getChild(i);

        //Skip recursion the times n.getChild() returns n.
        //The check really is this simple, because we can only skip this
        //When the child is literally the same object, otherwise it might be
        //a node that has identical properties, which can happen.
        if (child != n) {
            Log.d(_logTag, "ExistsNodeOrChildren child" + i);
            if (ExistsNodeOrChildren(n.getChild(i), condition)) return true;
        } else {
            log.e("We should report this as a bug in the AOSP");
        }
    }

    return false;
}

Upvotes: 1

Related Questions