• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compatibility.common.util;
18 
19 import static android.text.TextUtils.isEmpty;
20 
21 import android.app.Instrumentation;
22 import android.app.UiAutomation;
23 import android.content.res.Configuration;
24 import android.graphics.Point;
25 import android.graphics.Rect;
26 import android.os.Bundle;
27 import android.view.WindowManager;
28 import android.view.accessibility.AccessibilityNodeInfo;
29 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
30 import android.view.accessibility.AccessibilityWindowInfo;
31 
32 import androidx.test.InstrumentationRegistry;
33 
34 import java.lang.reflect.Field;
35 import java.lang.reflect.Method;
36 import java.lang.reflect.Modifier;
37 import java.util.Arrays;
38 import java.util.LinkedHashMap;
39 import java.util.LinkedHashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Objects;
43 import java.util.Set;
44 import java.util.function.BiFunction;
45 import java.util.function.BiPredicate;
46 import java.util.function.Consumer;
47 import java.util.function.Function;
48 import java.util.function.ToIntFunction;
49 import java.util.stream.Stream;
50 
51 /**
52  * Utilities to dump the view hierrarchy as an indented tree
53  *
54  * @see #dumpNodes(AccessibilityNodeInfo, StringBuilder)
55  * @see #wrapWithUiDump(Throwable)
56  */
57 @SuppressWarnings({"PointlessBitwiseExpression"})
58 public class UiDumpUtils {
UiDumpUtils()59     private UiDumpUtils() {}
60 
61     private static final boolean CONCISE = false;
62     private static final boolean SHOW_ACTIONS = false;
63     private static final boolean IGNORE_INVISIBLE = false;
64 
65     private static final int IGNORED_ACTIONS = 0
66             | AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS
67             | AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS
68             | AccessibilityNodeInfo.ACTION_FOCUS
69             | AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY
70             | AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
71             | AccessibilityNodeInfo.ACTION_SELECT
72             | AccessibilityNodeInfo.ACTION_SET_SELECTION
73             | AccessibilityNodeInfo.ACTION_CLEAR_SELECTION
74             | AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT
75             | AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT
76             ;
77 
78     private static final int SPECIALLY_HANDLED_ACTIONS = 0
79             | AccessibilityNodeInfo.ACTION_CLICK
80             | AccessibilityNodeInfo.ACTION_LONG_CLICK
81             | AccessibilityNodeInfo.ACTION_EXPAND
82             | AccessibilityNodeInfo.ACTION_COLLAPSE
83             | AccessibilityNodeInfo.ACTION_FOCUS
84             | AccessibilityNodeInfo.ACTION_CLEAR_FOCUS
85             | AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
86             | AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
87             | AccessibilityNodeInfo.ACTION_SET_TEXT
88             ;
89 
90     /** name -> typical_value */
91     private static Map<String, Boolean> sNodeFlags = new LinkedHashMap<>();
92     static {
93         sNodeFlags.put("focused", false);
94         sNodeFlags.put("selected", false);
95         sNodeFlags.put("contextClickable", false);
96         sNodeFlags.put("dismissable", false);
97         sNodeFlags.put("enabled", true);
98         sNodeFlags.put("password", false);
99         sNodeFlags.put("visibleToUser", true);
100         sNodeFlags.put("contentInvalid", false);
101         sNodeFlags.put("heading", false);
102         sNodeFlags.put("showingHintText", false);
103 
104         // Less important flags below
105         // Too spammy to report all, but can uncomment what's necessary
106 
107 //        sNodeFlags.put("focusable", true);
108 //        sNodeFlags.put("accessibilityFocused", false);
109 //        sNodeFlags.put("screenReaderFocusable", true);
110 //        sNodeFlags.put("clickable", false);
111 //        sNodeFlags.put("longClickable", false);
112 //        sNodeFlags.put("checkable", false);
113 //        sNodeFlags.put("checked", false);
114 //        sNodeFlags.put("editable", false);
115 //        sNodeFlags.put("scrollable", false);
116 //        sNodeFlags.put("importantForAccessibility", true);
117 //        sNodeFlags.put("multiLine", false);
118     }
119 
120     /** action -> pictogram */
121     private static Map<AccessibilityAction, String> sNodeActions = new LinkedHashMap<>();
122     static {
sNodeActions.put(AccessibilityAction.ACTION_PASTE, "\\uD83D\\uDCCB")123         sNodeActions.put(AccessibilityAction.ACTION_PASTE, "\uD83D\uDCCB");
sNodeActions.put(AccessibilityAction.ACTION_CUT, "✂")124         sNodeActions.put(AccessibilityAction.ACTION_CUT, "✂");
sNodeActions.put(AccessibilityAction.ACTION_COPY, "⎘")125         sNodeActions.put(AccessibilityAction.ACTION_COPY, "⎘");
sNodeActions.put(AccessibilityAction.ACTION_SCROLL_BACKWARD, "←")126         sNodeActions.put(AccessibilityAction.ACTION_SCROLL_BACKWARD, "←");
sNodeActions.put(AccessibilityAction.ACTION_SCROLL_LEFT, "←")127         sNodeActions.put(AccessibilityAction.ACTION_SCROLL_LEFT, "←");
sNodeActions.put(AccessibilityAction.ACTION_SCROLL_FORWARD, "→")128         sNodeActions.put(AccessibilityAction.ACTION_SCROLL_FORWARD, "→");
sNodeActions.put(AccessibilityAction.ACTION_SCROLL_RIGHT, "→")129         sNodeActions.put(AccessibilityAction.ACTION_SCROLL_RIGHT, "→");
sNodeActions.put(AccessibilityAction.ACTION_SCROLL_DOWN, "↓")130         sNodeActions.put(AccessibilityAction.ACTION_SCROLL_DOWN, "↓");
sNodeActions.put(AccessibilityAction.ACTION_SCROLL_UP, "↑")131         sNodeActions.put(AccessibilityAction.ACTION_SCROLL_UP, "↑");
132     }
133 
134     private static Instrumentation sInstrumentation = InstrumentationRegistry.getInstrumentation();
135     private static UiAutomation sUiAutomation = sInstrumentation.getUiAutomation();
136 
137     private static Point sDisplaySize = new Point();
138 
139     static {
140         sInstrumentation.getContext()
141                 .getSystemService(WindowManager.class)
142                 .getDefaultDisplay()
143                 .getRealSize(sDisplaySize);
144     }
145 
146 
147     /**
148      * Wraps the given exception, with one containing UI hierrarchy {@link #dumpNodes dump}
149      * in its message.
150      *
151      * <p>
152      * Can be used together with {@link ExceptionUtils#wrappingExceptions}, e.g:
153      * {@code
154      *     ExceptionUtils.wrappingExceptions(UiDumpUtils::wrapWithUiDump, () -> {
155      *         // UI-testing code
156      *     });
157      * }
158      */
wrapWithUiDump(Throwable cause)159     public static UiDumpWrapperException wrapWithUiDump(Throwable cause) {
160         return (cause instanceof UiDumpWrapperException)
161                 ? (UiDumpWrapperException) cause
162                 : new UiDumpWrapperException(cause);
163     }
164 
165     /**
166      * Dumps UI hierarchy into {@code out}.
167      */
dumpNodes(StringBuilder out)168     public static void dumpNodes(StringBuilder out) {
169         dumpNodes(sUiAutomation.getRootInActiveWindow(), out);
170     }
171 
172     /**
173      * Dumps UI hierarchy with a given {@code root} as indented text tree into {@code out}.
174      */
dumpNodes(AccessibilityNodeInfo root, StringBuilder out)175     public static void dumpNodes(AccessibilityNodeInfo root, StringBuilder out) {
176         if (root == null) {
177             appendNode(out, root);
178             return;
179         }
180         // Refresh display size if changed during the test run
181         sInstrumentation.getContext().getSystemService(WindowManager.class)
182                 .getDefaultDisplay().getRealSize(sDisplaySize);
183         out.append("Display size: ");
184         out.append(sDisplaySize.x).append("x").append(sDisplaySize.y).append(" ");
185         int dpi = sInstrumentation.getContext().getResources().getDisplayMetrics().densityDpi;
186         out.append("dpi: ").append(dpi).append(" ");
187         int orientation =
188                 sInstrumentation.getContext().getResources().getConfiguration().orientation;
189         out.append("orientation: ").append(getOrientation(orientation)).append("\n");
190 
191         out.append("--- ").append(root.getPackageName()).append(" ---\n|");
192 
193         recursively(root, AccessibilityNodeInfo::getChildCount, AccessibilityNodeInfo::getChild,
194                 node -> {
195                     if (appendNode(out, node)) {
196                         out.append("\n|");
197                     }
198                 },
199                 action -> node -> {
200                     out.append("  ");
201                     action.accept(node);
202                 });
203     }
204 
recursively(T node, ToIntFunction<T> getChildCount, BiFunction<T, Integer, T> getChildAt, Consumer<T> action, Function<Consumer<T>, Consumer<T>> actionChange)205     private static <T> void recursively(T node,
206             ToIntFunction<T> getChildCount, BiFunction<T, Integer, T> getChildAt,
207             Consumer<T> action, Function<Consumer<T>, Consumer<T>> actionChange) {
208         if (node == null) return;
209 
210         action.accept(node);
211         Consumer<T> childAction = actionChange.apply(action);
212 
213         int size = getChildCount.applyAsInt(node);
214         for (int i = 0; i < size; i++) {
215             recursively(getChildAt.apply(node, i),
216                     getChildCount, getChildAt, childAction, actionChange);
217         }
218     }
219 
appendWindow(AccessibilityWindowInfo window, StringBuilder out)220     private static StringBuilder appendWindow(AccessibilityWindowInfo window, StringBuilder out) {
221         if (window == null) {
222             out.append("<null window>");
223         } else {
224             if (!isEmpty(window.getTitle())) {
225                 out.append("Window title: ");
226                 out.append(window.getTitle());
227                 if (CONCISE) return out;
228                 out.append(" ");
229             }
230             out.append("Window type: ");
231             out.append(valueToString(
232                     AccessibilityWindowInfo.class, "TYPE_", window.getType())).append(" ");
233             if (CONCISE) return out;
234             appendArea(out, window::getBoundsInScreen);
235             if (window.isInPictureInPictureMode()) out.append("#PIP ");
236         }
237         out.append("\n|");
238         return out;
239     }
240 
appendArea(StringBuilder out, Consumer<Rect> getBoundsInScreen)241     private static void appendArea(StringBuilder out, Consumer<Rect> getBoundsInScreen) {
242         Rect rect = new Rect();
243         getBoundsInScreen.accept(rect);
244         out.append("size:");
245         int screenArea = sDisplaySize.x * sDisplaySize.y;
246         out.append(toStringRounding((float) area(rect) * 100 / screenArea)).append("% ");
247         out.append(rect.toString()).append(" ");
248     }
249 
appendNode(StringBuilder out, AccessibilityNodeInfo node)250     private static boolean appendNode(StringBuilder out, AccessibilityNodeInfo node) {
251         if (node == null) {
252             out.append("<null node>");
253             return true;
254         }
255 
256         if (IGNORE_INVISIBLE && !node.isVisibleToUser()) return false;
257 
258         boolean markedClickable = false;
259         boolean markedNonFocusable = false;
260 
261         try {
262             if (node.isFocused() || node.isAccessibilityFocused()) {
263                 out.append(">");
264             }
265 
266             if ((node.getActions() & AccessibilityNodeInfo.ACTION_EXPAND) != 0) {
267                 out.append("[+] ");
268             }
269             if ((node.getActions() & AccessibilityNodeInfo.ACTION_COLLAPSE) != 0) {
270                 out.append("[-] ");
271             }
272 
273             CharSequence txt = node.getText();
274             if (node.isCheckable()) {
275                 out.append("[").append(node.isChecked() ? "X" : "_").append("] ");
276             } else if (node.isEditable()) {
277                 if (txt == null) txt = "";
278                 out.append("[");
279                 appendTextWithCursor(out, node, txt);
280                 out.append("] ");
281             } else if (node.isClickable()) {
282                 markedClickable = true;
283                 out.append("[");
284             } else if (!node.isImportantForAccessibility()) {
285                 markedNonFocusable = true;
286                 out.append("(");
287             }
288 
289             if (appendNodeText(out, node)) return true;
290         } finally {
291             backspaceIf(' ', out);
292             if (markedClickable) {
293                 out.append("]");
294                 if (node.isLongClickable()) out.append("+");
295                 out.append(" ");
296             }
297             if (markedNonFocusable) out.append(") ");
298 
299             if (CONCISE) out.append(" ");
300 
301             for (Map.Entry<String, Boolean> prop : sNodeFlags.entrySet()) {
302                 boolean value = call(node, boolGetter(prop.getKey()));
303                 if (value != prop.getValue()) {
304                     out.append("#");
305                     if (!value) out.append("not_");
306                     out.append(prop.getKey()).append(" ");
307                 }
308             }
309 
310             if (SHOW_ACTIONS) {
311                 LinkedHashSet<String> symbols = new LinkedHashSet<>();
312                 for (AccessibilityAction accessibilityAction : node.getActionList()) {
313                     String symbol = sNodeActions.get(accessibilityAction);
314                     if (symbol != null) symbols.add(symbol);
315                 }
316                 merge(symbols, "←", "→", "↔");
317                 merge(symbols, "↑", "↓", "↕");
318                 symbols.forEach(out::append);
319                 if (!symbols.isEmpty()) out.append(" ");
320 
321                 getActions(node)
322                         .map(a -> "[" + actionToString(a) + "] ")
323                         .forEach(out::append);
324             }
325 
326             Bundle extras = node.getExtras();
327             for (String extra : extras.keySet()) {
328                 if (extra.equals("AccessibilityNodeInfo.chromeRole")) continue;
329                 if (extra.equals("AccessibilityNodeInfo.roleDescription")) continue;
330                 String value = "" + extras.get(extra);
331                 if (value.isEmpty()) continue;
332                 out.append(extra).append(":").append(value).append(" ");
333             }
334         }
335         return true;
336     }
337 
appendTextWithCursor(StringBuilder out, AccessibilityNodeInfo node, CharSequence txt)338     private static StringBuilder appendTextWithCursor(StringBuilder out, AccessibilityNodeInfo node,
339             CharSequence txt) {
340         out.append(txt);
341         insertAtEnd(out, txt.length() - 1 - node.getTextSelectionStart(), "ꕯ");
342         if (node.getTextSelectionEnd() != node.getTextSelectionStart()) {
343             insertAtEnd(out, txt.length() - 1 - node.getTextSelectionEnd(), "ꕯ");
344         }
345         return out;
346     }
347 
appendNodeText(StringBuilder out, AccessibilityNodeInfo node)348     private static boolean appendNodeText(StringBuilder out, AccessibilityNodeInfo node) {
349         CharSequence txt = node.getText();
350 
351         Bundle extras = node.getExtras();
352         if (extras.containsKey("AccessibilityNodeInfo.roleDescription")) {
353             out.append("<").append(extras.getString("AccessibilityNodeInfo.chromeRole"))
354                     .append("> ");
355         } else if (extras.containsKey("AccessibilityNodeInfo.chromeRole")) {
356             out.append("<").append(extras.getString("AccessibilityNodeInfo.chromeRole"))
357                     .append("> ");
358         }
359 
360         if (CONCISE) {
361             if (!isEmpty(node.getContentDescription())) {
362                 out.append(escape(node.getContentDescription()));
363                 return true;
364             }
365             if (!isEmpty(node.getPaneTitle())) {
366                 out.append(escape(node.getPaneTitle()));
367                 return true;
368             }
369             if (!isEmpty(txt) && !node.isEditable()) {
370                 out.append('"');
371                 if (node.getTextSelectionStart() > 0 || node.getTextSelectionEnd() > 0) {
372                     appendTextWithCursor(out, node, txt);
373                 } else {
374                     out.append(escape(txt));
375                 }
376                 out.append('"');
377                 return true;
378             }
379             if (!isEmpty(node.getViewIdResourceName())) {
380                 out.append("@").append(fromLast("/", node.getViewIdResourceName()));
381                 return true;
382             }
383         }
384 
385         if (node.getParent() == null && node.getWindow() != null) {
386             appendWindow(node.getWindow(), out);
387             if (CONCISE) return true;
388             out.append(" ");
389         }
390 
391         if (!extras.containsKey("AccessibilityNodeInfo.chromeRole")) {
392             out.append(fromLast(".", node.getClassName())).append(" ");
393         }
394         ifNotEmpty(node.getViewIdResourceName(),
395                 s -> out.append("@").append(fromLast("/", s)).append(" "));
396         ifNotEmpty(node.getPaneTitle(), s -> out.append("## ").append(s).append(" "));
397         ifNotEmpty(txt, s -> out.append("\"").append(s).append("\" "));
398 
399         ifNotEmpty(node.getContentDescription(), s -> out.append("//").append(s).append(" "));
400 
401         appendArea(out, node::getBoundsInScreen);
402         return false;
403     }
404 
getOrientation(int orientation)405     private static String getOrientation(int orientation) {
406         switch (orientation) {
407             case Configuration.ORIENTATION_PORTRAIT:
408                 return "Portrait";
409             case Configuration.ORIENTATION_LANDSCAPE:
410                 return "Landscape";
411             default:
412                 return "Unknown";
413         }
414     }
415 
valueToString(Class<?> clazz, String prefix, T value)416     private static <T> String valueToString(Class<?> clazz, String prefix, T value) {
417         String s = flagsToString(clazz, prefix, value, Objects::equals);
418         if (s.isEmpty()) s = "" + value;
419         return s;
420     }
421 
flagsToString(Class<?> clazz, String prefix, T flags, BiPredicate<T, T> test)422     private static <T> String flagsToString(Class<?> clazz, String prefix, T flags,
423             BiPredicate<T, T> test) {
424         return mkStr(sb -> {
425             consts(clazz, prefix)
426                     .filter(f -> box(f.getType()).isInstance(flags))
427                     .forEach(c -> {
428                         try {
429                             if (test.test(flags, read(null, c))) {
430                                 sb.append(c.getName().substring(prefix.length())).append("|");
431                             }
432                         } catch (Exception e) {
433                             throw new RuntimeException("Error while dealing with " + c, e);
434                         }
435                     });
436             backspace(sb);
437         });
438     }
439 
440     private static Class box(Class c) {
441         return c == int.class ? Integer.class : c;
442     }
443 
444     private static Stream<Field> consts(Class<?> clazz, String prefix) {
445         return Arrays.stream(clazz.getDeclaredFields())
446                 .filter(f -> isConst(f) && f.getName().startsWith(prefix));
447     }
448 
449     private static boolean isConst(Field f) {
450         return Modifier.isStatic(f.getModifiers()) && Modifier.isFinal(f.getModifiers());
451     }
452 
453     private static Character last(StringBuilder sb) {
454         return sb.length() == 0 ? null : sb.charAt(sb.length() - 1);
455     }
456 
457     private static StringBuilder backspaceIf(char c, StringBuilder sb) {
458         if (Objects.equals(last(sb), c)) backspace(sb);
459         return sb;
460     }
461 
462     private static StringBuilder backspace(StringBuilder sb) {
463         if (sb.length() != 0) {
464             sb.deleteCharAt(sb.length() - 1);
465         }
466         return sb;
467     }
468 
469     private static String toStringRounding(float f) {
470         return f >= 5.0 ? "" + (int) f : String.format("%.1f", f);
471     }
472 
473     private static int area(Rect r) {
474         return Math.abs((r.right - r.left) * (r.bottom - r.top));
475     }
476 
477     private static String escape(CharSequence s) {
478         return mkStr(out -> {
479             for (int i = 0; i < s.length(); i++) {
480                 char c = s.charAt(i);
481                 if (c < 127 || c == 0xa0 || c >= 0x2000 && c < 0x2070) {
482                     out.append(c);
483                 } else {
484                     out.append("\\u").append(Integer.toHexString(c));
485                 }
486             }
487         });
488     }
489 
490     private static Stream<AccessibilityAction> getActions(
491             AccessibilityNodeInfo node) {
492         if (node == null) return Stream.empty();
493         return node.getActionList().stream()
494                 .filter(a -> !AccessibilityAction.ACTION_SHOW_ON_SCREEN.equals(a)
495                         && (a.getId()
496                                 & ~IGNORED_ACTIONS
497                                 & ~SPECIALLY_HANDLED_ACTIONS
498                             ) != 0);
499     }
500 
501     private static String actionToString(AccessibilityAction a) {
502         if (!isEmpty(a.getLabel())) return a.getLabel().toString();
503         return valueToString(AccessibilityAction.class, "ACTION_", a);
504     }
505 
506     private static void merge(Set<String> symbols, String a, String b, String ab) {
507         if (symbols.contains(a) && symbols.contains(b)) {
508             symbols.add(ab);
509             symbols.remove(a);
510             symbols.remove(b);
511         }
512     }
513 
514     private static String fromLast(String substr, CharSequence whole) {
515         if (whole == null) {
516             return null;
517         }
518         String wholeStr = whole.toString();
519         int idx = wholeStr.lastIndexOf(substr);
520         if (idx < 0) return wholeStr;
521         return wholeStr.substring(idx + substr.length());
522     }
523 
524     private static String boolGetter(String propName) {
525         return "is" + Character.toUpperCase(propName.charAt(0)) + propName.substring(1);
526     }
527 
528     private static <T> T read(Object o, Field f) {
529         try {
530             f.setAccessible(true);
531             return (T) f.get(o);
532         } catch (Exception e) {
533             throw new RuntimeException(e);
534         }
535     }
536 
537     private static <T> T call(Object o, String methodName, Object... args) {
538         Class clazz = o instanceof Class ? (Class) o : o.getClass();
539         try {
540             Method method = clazz.getDeclaredMethod(methodName, mapToTypes(args));
541             method.setAccessible(true);
542             //noinspection unchecked
543             return (T) method.invoke(o, args);
544         } catch (NoSuchMethodException e) {
545             throw new RuntimeException(
546                     newlineSeparated(Arrays.asList(clazz.getDeclaredMethods())), e);
547         } catch (Exception e) {
548             throw new RuntimeException(e);
549         }
550     }
551 
552     private static Class[] mapToTypes(Object[] args) {
553         return Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
554     }
555 
556     private static void ifNotEmpty(CharSequence t, Consumer<CharSequence> f) {
557         if (!isEmpty(t)) {
558             f.accept(t);
559         }
560     }
561 
562     private static StringBuilder insertAtEnd(StringBuilder sb, int pos, String s) {
563         return sb.insert(sb.length() - 1 - pos, s);
564     }
565 
566     private static <T, R> R fold(List<T> l, R init, BiFunction<R, T, R> combine) {
567         R result = init;
568         for (T t : l) {
569             result = combine.apply(result, t);
570         }
571         return result;
572     }
573 
574     private static <T> String toString(List<T> l, String sep, Function<T, String> elemToStr) {
575         return fold(l, "", (a, b) -> a + sep + elemToStr.apply(b));
576     }
577 
578     private static <T> String toString(List<T> l, String sep) {
579         return toString(l, sep, String::valueOf);
580     }
581 
582     private static String newlineSeparated(List<?> l) {
583         return toString(l, "\n");
584     }
585 
586     private static String mkStr(Consumer<StringBuilder> build) {
587         StringBuilder t = new StringBuilder();
588         build.accept(t);
589         return t.toString();
590     }
591 
592     private static class UiDumpWrapperException extends RuntimeException {
593         private UiDumpWrapperException(Throwable cause) {
594             super(cause.getMessage() + "\n\nWhile displaying the following UI:\n"
595                     + mkStr(sb -> dumpNodes(sUiAutomation.getRootInActiveWindow(), sb)), cause);
596         }
597     }
598 }
599