parseMobileHierarchy function
Extracts the filtered MobileElementIndex from a hierarchy XML dump.
packageHint names the window when the XML carries no package
attribute (defensive: uiautomator dumps always do). cap bounds the
index; hitting it sets truncated with a scroll hint in the render.
Implementation
MobileElementIndex parseMobileHierarchy(
String xml, {
String packageHint = 'unknown',
int cap = defaultMobileElementCap,
}) {
final kept = <MobileElement>[];
var truncated = false;
var packageName = '';
final nodeTag = RegExp(r'<node\b((?:[^>"]|"[^"]*")*?)/?>');
final attr = RegExp(r'([\w-]+)="([^"]*)"');
for (final match in nodeTag.allMatches(xml)) {
final attrs = <String, String>{
for (final a in attr.allMatches(match.group(1)!)) a.group(1)!: a.group(2)!,
};
final pkg = _unescape(attrs['package'] ?? '');
if (pkg.isNotEmpty && packageName.isEmpty) packageName = pkg;
final bounds = _nodeBounds(attrs['bounds']);
if (bounds == null) continue; // malformed/absent bounds or zero-area
final (left, top, right, bottom) = bounds;
final clickable = attrs['clickable'] == 'true';
final scrollable = attrs['scrollable'] == 'true';
// uiautomator dumps carry no `editable` flag — the EditText class is
// the signal (focused nodes of other classes are not text fields).
final editable = attrs['class']?.contains('EditText') == true;
final checkable = attrs['checkable'] == 'true';
final text = _unescape(attrs['text'] ?? '');
final contentDesc = _unescape(attrs['content-desc'] ?? '');
final viewId = _unescape(attrs['resource-id'] ?? '');
final password = attrs['password'] == 'true';
// The artemis keep-rule: informative or interactive only.
if (!_isInformative(
text: text,
contentDesc: contentDesc,
viewId: viewId,
clickable: clickable,
scrollable: scrollable,
editable: editable,
checkable: checkable,
)) {
continue;
}
if (kept.length >= cap) {
truncated = true;
break;
}
kept.add(
MobileElement(
id: 'e${kept.length + 1}',
text: _visibleText(text, password: password),
contentDesc: contentDesc.isEmpty ? null : contentDesc,
className: _classNameOf(attrs['class']),
viewId: viewId.isEmpty ? null : viewId,
clickable: clickable,
scrollable: scrollable,
editable: editable,
checkable: checkable,
checked: attrs['checked'] == 'true',
bounds: '[$left,$top][$right,$bottom]',
centerX: (left + right) ~/ 2,
centerY: (top + bottom) ~/ 2,
),
);
}
return MobileElementIndex(
packageName: packageName.isEmpty ? packageHint : packageName,
elements: kept,
truncated: truncated,
);
}