parseMobileHierarchy function

MobileElementIndex parseMobileHierarchy(
  1. String xml, {
  2. String packageHint = 'unknown',
  3. int cap = defaultMobileElementCap,
})

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,
  );
}