• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 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 android.view;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.TestApi;
22 import android.compat.annotation.UnsupportedAppUsage;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Bitmap;
26 import android.graphics.Canvas;
27 import android.graphics.HardwareRenderer;
28 import android.graphics.Picture;
29 import android.graphics.RecordingCanvas;
30 import android.graphics.Rect;
31 import android.graphics.RenderNode;
32 import android.os.Build;
33 import android.os.Debug;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.Message;
37 import android.util.DisplayMetrics;
38 import android.util.Log;
39 import android.util.TypedValue;
40 
41 import libcore.util.HexEncoding;
42 
43 import java.io.BufferedOutputStream;
44 import java.io.BufferedWriter;
45 import java.io.ByteArrayOutputStream;
46 import java.io.DataOutputStream;
47 import java.io.IOException;
48 import java.io.OutputStream;
49 import java.io.OutputStreamWriter;
50 import java.lang.annotation.Annotation;
51 import java.lang.annotation.ElementType;
52 import java.lang.annotation.Retention;
53 import java.lang.annotation.RetentionPolicy;
54 import java.lang.annotation.Target;
55 import java.lang.reflect.AccessibleObject;
56 import java.lang.reflect.Field;
57 import java.lang.reflect.InvocationTargetException;
58 import java.lang.reflect.Member;
59 import java.lang.reflect.Method;
60 import java.util.ArrayDeque;
61 import java.util.Arrays;
62 import java.util.HashMap;
63 import java.util.concurrent.Callable;
64 import java.util.concurrent.CountDownLatch;
65 import java.util.concurrent.ExecutionException;
66 import java.util.concurrent.Executor;
67 import java.util.concurrent.FutureTask;
68 import java.util.concurrent.TimeUnit;
69 import java.util.concurrent.TimeoutException;
70 import java.util.concurrent.atomic.AtomicReference;
71 import java.util.concurrent.locks.ReentrantLock;
72 import java.util.function.Function;
73 import java.util.stream.Stream;
74 
75 /**
76  * Various debugging/tracing tools related to {@link View} and the view hierarchy.
77  */
78 public class ViewDebug {
79     /**
80      * @deprecated This flag is now unused
81      */
82     @Deprecated
83     public static final boolean TRACE_HIERARCHY = false;
84 
85     /**
86      * @deprecated This flag is now unused
87      */
88     @Deprecated
89     public static final boolean TRACE_RECYCLER = false;
90 
91     /**
92      * Enables detailed logging of drag/drop operations.
93      * @hide
94      */
95     public static final boolean DEBUG_DRAG = false;
96 
97     /**
98      * Enables detailed logging of task positioning operations.
99      * @hide
100      */
101     public static final boolean DEBUG_POSITIONING = false;
102 
103     /**
104      * This annotation can be used to mark fields and methods to be dumped by
105      * the view server. Only non-void methods with no arguments can be annotated
106      * by this annotation.
107      */
108     @Target({ ElementType.FIELD, ElementType.METHOD })
109     @Retention(RetentionPolicy.RUNTIME)
110     public @interface ExportedProperty {
111         /**
112          * When resolveId is true, and if the annotated field/method return value
113          * is an int, the value is converted to an Android's resource name.
114          *
115          * @return true if the property's value must be transformed into an Android
116          *         resource name, false otherwise
117          */
resolveId()118         boolean resolveId() default false;
119 
120         /**
121          * A mapping can be defined to map int values to specific strings. For
122          * instance, View.getVisibility() returns 0, 4 or 8. However, these values
123          * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
124          * these human readable values:
125          *
126          * <pre>
127          * {@literal @}ViewDebug.ExportedProperty(mapping = {
128          *     {@literal @}ViewDebug.IntToString(from = 0, to = "VISIBLE"),
129          *     {@literal @}ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
130          *     {@literal @}ViewDebug.IntToString(from = 8, to = "GONE")
131          * })
132          * public int getVisibility() { ...
133          * <pre>
134          *
135          * @return An array of int to String mappings
136          *
137          * @see android.view.ViewDebug.IntToString
138          */
mapping()139         IntToString[] mapping() default { };
140 
141         /**
142          * A mapping can be defined to map array indices to specific strings.
143          * A mapping can be used to see human readable values for the indices
144          * of an array:
145          *
146          * <pre>
147          * {@literal @}ViewDebug.ExportedProperty(indexMapping = {
148          *     {@literal @}ViewDebug.IntToString(from = 0, to = "INVALID"),
149          *     {@literal @}ViewDebug.IntToString(from = 1, to = "FIRST"),
150          *     {@literal @}ViewDebug.IntToString(from = 2, to = "SECOND")
151          * })
152          * private int[] mElements;
153          * <pre>
154          *
155          * @return An array of int to String mappings
156          *
157          * @see android.view.ViewDebug.IntToString
158          * @see #mapping()
159          */
indexMapping()160         IntToString[] indexMapping() default { };
161 
162         /**
163          * A flags mapping can be defined to map flags encoded in an integer to
164          * specific strings. A mapping can be used to see human readable values
165          * for the flags of an integer:
166          *
167          * <pre>
168          * {@literal @}ViewDebug.ExportedProperty(flagMapping = {
169          *     {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED,
170          *             name = "ENABLED"),
171          *     {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED,
172          *             name = "DISABLED"),
173          * })
174          * private int mFlags;
175          * <pre>
176          *
177          * A specified String is output when the following is true:
178          *
179          * @return An array of int to String mappings
180          */
flagMapping()181         FlagToString[] flagMapping() default { };
182 
183         /**
184          * When deep export is turned on, this property is not dumped. Instead, the
185          * properties contained in this property are dumped. Each child property
186          * is prefixed with the name of this property.
187          *
188          * @return true if the properties of this property should be dumped
189          *
190          * @see #prefix()
191          */
deepExport()192         boolean deepExport() default false;
193 
194         /**
195          * The prefix to use on child properties when deep export is enabled
196          *
197          * @return a prefix as a String
198          *
199          * @see #deepExport()
200          */
prefix()201         String prefix() default "";
202 
203         /**
204          * Specifies the category the property falls into, such as measurement,
205          * layout, drawing, etc.
206          *
207          * @return the category as String
208          */
category()209         String category() default "";
210 
211         /**
212          * Indicates whether or not to format an {@code int} or {@code byte} value as a hex string.
213          *
214          * @return true if the supported values should be formatted as a hex string.
215          */
formatToHexString()216         boolean formatToHexString() default false;
217 
218         /**
219          * Indicates whether or not the key to value mappings are held in adjacent indices.
220          *
221          * Note: Applies only to fields and methods that return String[].
222          *
223          * @return true if the key to value mappings are held in adjacent indices.
224          */
hasAdjacentMapping()225         boolean hasAdjacentMapping() default false;
226     }
227 
228     /**
229      * Defines a mapping from an int value to a String. Such a mapping can be used
230      * in an @ExportedProperty to provide more meaningful values to the end user.
231      *
232      * @see android.view.ViewDebug.ExportedProperty
233      */
234     @Target({ ElementType.TYPE })
235     @Retention(RetentionPolicy.RUNTIME)
236     public @interface IntToString {
237         /**
238          * The original int value to map to a String.
239          *
240          * @return An arbitrary int value.
241          */
from()242         int from();
243 
244         /**
245          * The String to use in place of the original int value.
246          *
247          * @return An arbitrary non-null String.
248          */
to()249         String to();
250     }
251 
252     /**
253      * Defines a mapping from a flag to a String. Such a mapping can be used
254      * in an @ExportedProperty to provide more meaningful values to the end user.
255      *
256      * @see android.view.ViewDebug.ExportedProperty
257      */
258     @Target({ ElementType.TYPE })
259     @Retention(RetentionPolicy.RUNTIME)
260     public @interface FlagToString {
261         /**
262          * The mask to apply to the original value.
263          *
264          * @return An arbitrary int value.
265          */
mask()266         int mask();
267 
268         /**
269          * The value to compare to the result of:
270          * <code>original value &amp; {@link #mask()}</code>.
271          *
272          * @return An arbitrary value.
273          */
equals()274         int equals();
275 
276         /**
277          * The String to use in place of the original int value.
278          *
279          * @return An arbitrary non-null String.
280          */
name()281         String name();
282 
283         /**
284          * Indicates whether to output the flag when the test is true,
285          * or false. Defaults to true.
286          */
outputIf()287         boolean outputIf() default true;
288     }
289 
290     /**
291      * This annotation can be used to mark fields and methods to be dumped when
292      * the view is captured. Methods with this annotation must have no arguments
293      * and must return a valid type of data.
294      */
295     @Target({ ElementType.FIELD, ElementType.METHOD })
296     @Retention(RetentionPolicy.RUNTIME)
297     public @interface CapturedViewProperty {
298         /**
299          * When retrieveReturn is true, we need to retrieve second level methods
300          * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
301          * we will set retrieveReturn = true on the annotation of
302          * myView.getFirstLevelMethod()
303          * @return true if we need the second level methods
304          */
retrieveReturn()305         boolean retrieveReturn() default false;
306     }
307 
308     /**
309      * Allows a View to inject custom children into HierarchyViewer. For example,
310      * WebView uses this to add its internal layer tree as a child to itself
311      * @hide
312      */
313     public interface HierarchyHandler {
314         /**
315          * Dumps custom children to hierarchy viewer.
316          * See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int)
317          * for the format
318          *
319          * An empty implementation should simply do nothing
320          *
321          * @param out The output writer
322          * @param level The indentation level
323          */
dumpViewHierarchyWithProperties(BufferedWriter out, int level)324         public void dumpViewHierarchyWithProperties(BufferedWriter out, int level);
325 
326         /**
327          * Returns a View to enable grabbing screenshots from custom children
328          * returned in dumpViewHierarchyWithProperties.
329          *
330          * @param className The className of the view to find
331          * @param hashCode The hashCode of the view to find
332          * @return the View to capture from, or null if not found
333          */
findHierarchyView(String className, int hashCode)334         public View findHierarchyView(String className, int hashCode);
335     }
336 
337     private abstract static class PropertyInfo<T extends Annotation,
338             R extends AccessibleObject & Member> {
339 
340         public final R member;
341         public final T property;
342         public final String name;
343         public final Class<?> returnType;
344 
345         public String entrySuffix = "";
346         public String valueSuffix = "";
347 
PropertyInfo(Class<T> property, R member, Class<?> returnType)348         PropertyInfo(Class<T> property, R member, Class<?> returnType) {
349             this.member = member;
350             this.name = member.getName();
351             this.property = member.getAnnotation(property);
352             this.returnType = returnType;
353         }
354 
invoke(Object target)355         public abstract Object invoke(Object target) throws Exception;
356 
forMethod(Method method, Class<T> property)357         static <T extends Annotation> PropertyInfo<T, ?> forMethod(Method method,
358                 Class<T> property) {
359             // Ensure the method return and parameter types can be resolved.
360             try {
361                 if ((method.getReturnType() == Void.class)
362                         || (method.getParameterTypes().length != 0)) {
363                     return null;
364                 }
365             } catch (NoClassDefFoundError e) {
366                 return null;
367             }
368             if (!method.isAnnotationPresent(property)) {
369                 return null;
370             }
371             method.setAccessible(true);
372 
373             PropertyInfo info = new MethodPI(method, property);
374             info.entrySuffix = "()";
375             info.valueSuffix = ";";
376             return info;
377         }
378 
forField(Field field, Class<T> property)379         static <T extends Annotation> PropertyInfo<T, ?> forField(Field field, Class<T> property) {
380             if (!field.isAnnotationPresent(property)) {
381                 return null;
382             }
383             field.setAccessible(true);
384             return new FieldPI<>(field, property);
385         }
386     }
387 
388     private static class MethodPI<T extends Annotation> extends PropertyInfo<T, Method> {
389 
MethodPI(Method method, Class<T> property)390         MethodPI(Method method, Class<T> property) {
391             super(property, method, method.getReturnType());
392         }
393 
394         @Override
invoke(Object target)395         public Object invoke(Object target) throws Exception {
396             return member.invoke(target);
397         }
398     }
399 
400     private static class FieldPI<T extends Annotation> extends PropertyInfo<T, Field> {
401 
FieldPI(Field field, Class<T> property)402         FieldPI(Field field, Class<T> property) {
403             super(property, field, field.getType());
404         }
405 
406         @Override
invoke(Object target)407         public Object invoke(Object target) throws Exception {
408             return member.get(target);
409         }
410     }
411 
412     // Maximum delay in ms after which we stop trying to capture a View's drawing
413     private static final int CAPTURE_TIMEOUT = 6000;
414 
415     private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
416     private static final String REMOTE_COMMAND_DUMP = "DUMP";
417     private static final String REMOTE_COMMAND_DUMP_THEME = "DUMP_THEME";
418     /**
419      * Similar to REMOTE_COMMAND_DUMP but uses ViewHierarchyEncoder instead of flat text
420      * @hide
421      */
422     public static final String REMOTE_COMMAND_DUMP_ENCODED = "DUMP_ENCODED";
423     private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
424     private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
425     private static final String REMOTE_PROFILE = "PROFILE";
426     private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS";
427     private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST";
428 
429     private static HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> sExportProperties;
430     private static HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]>
431             sCapturedViewProperties;
432 
433     /**
434      * @deprecated This enum is now unused
435      */
436     @Deprecated
437     public enum HierarchyTraceType {
438         INVALIDATE,
439         INVALIDATE_CHILD,
440         INVALIDATE_CHILD_IN_PARENT,
441         REQUEST_LAYOUT,
442         ON_LAYOUT,
443         ON_MEASURE,
444         DRAW,
445         BUILD_CACHE
446     }
447 
448     /**
449      * @deprecated This enum is now unused
450      */
451     @Deprecated
452     public enum RecyclerTraceType {
453         NEW_VIEW,
454         BIND_VIEW,
455         RECYCLE_FROM_ACTIVE_HEAP,
456         RECYCLE_FROM_SCRAP_HEAP,
457         MOVE_TO_SCRAP_HEAP,
458         MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
459     }
460 
461     /**
462      * Returns the number of instanciated Views.
463      *
464      * @return The number of Views instanciated in the current process.
465      *
466      * @hide
467      */
468     @UnsupportedAppUsage
getViewInstanceCount()469     public static long getViewInstanceCount() {
470         return Debug.countInstancesOfClass(View.class);
471     }
472 
473     /**
474      * Returns the number of instanciated ViewAncestors.
475      *
476      * @return The number of ViewAncestors instanciated in the current process.
477      *
478      * @hide
479      */
480     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getViewRootImplCount()481     public static long getViewRootImplCount() {
482         return Debug.countInstancesOfClass(ViewRootImpl.class);
483     }
484 
485     /**
486      * @deprecated This method is now unused and invoking it is a no-op
487      */
488     @Deprecated
489     @SuppressWarnings({ "UnusedParameters", "deprecation" })
trace(View view, RecyclerTraceType type, int... parameters)490     public static void trace(View view, RecyclerTraceType type, int... parameters) {
491     }
492 
493     /**
494      * @deprecated This method is now unused and invoking it is a no-op
495      */
496     @Deprecated
497     @SuppressWarnings("UnusedParameters")
startRecyclerTracing(String prefix, View view)498     public static void startRecyclerTracing(String prefix, View view) {
499     }
500 
501     /**
502      * @deprecated This method is now unused and invoking it is a no-op
503      */
504     @Deprecated
505     @SuppressWarnings("UnusedParameters")
stopRecyclerTracing()506     public static void stopRecyclerTracing() {
507     }
508 
509     /**
510      * @deprecated This method is now unused and invoking it is a no-op
511      */
512     @Deprecated
513     @SuppressWarnings({ "UnusedParameters", "deprecation" })
trace(View view, HierarchyTraceType type)514     public static void trace(View view, HierarchyTraceType type) {
515     }
516 
517     /**
518      * @deprecated This method is now unused and invoking it is a no-op
519      */
520     @Deprecated
521     @SuppressWarnings("UnusedParameters")
startHierarchyTracing(String prefix, View view)522     public static void startHierarchyTracing(String prefix, View view) {
523     }
524 
525     /**
526      * @deprecated This method is now unused and invoking it is a no-op
527      */
528     @Deprecated
stopHierarchyTracing()529     public static void stopHierarchyTracing() {
530     }
531 
532     @UnsupportedAppUsage
dispatchCommand(View view, String command, String parameters, OutputStream clientStream)533     static void dispatchCommand(View view, String command, String parameters,
534             OutputStream clientStream) throws IOException {
535         // Just being cautious...
536         view = view.getRootView();
537 
538         if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
539             dump(view, false, true, clientStream);
540         } else if (REMOTE_COMMAND_DUMP_THEME.equalsIgnoreCase(command)) {
541             dumpTheme(view, clientStream);
542         } else if (REMOTE_COMMAND_DUMP_ENCODED.equalsIgnoreCase(command)) {
543             dumpEncoded(view, clientStream);
544         } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) {
545             captureLayers(view, new DataOutputStream(clientStream));
546         } else {
547             final String[] params = parameters.split(" ");
548             if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
549                 capture(view, clientStream, params[0]);
550             } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) {
551                 outputDisplayList(view, params[0]);
552             } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
553                 invalidate(view, params[0]);
554             } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
555                 requestLayout(view, params[0]);
556             } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
557                 profile(view, clientStream, params[0]);
558             }
559         }
560     }
561 
562     /** @hide */
findView(View root, String parameter)563     public static View findView(View root, String parameter) {
564         // Look by type/hashcode
565         if (parameter.indexOf('@') != -1) {
566             final String[] ids = parameter.split("@");
567             final String className = ids[0];
568             final int hashCode = (int) Long.parseLong(ids[1], 16);
569 
570             View view = root.getRootView();
571             if (view instanceof ViewGroup) {
572                 return findView((ViewGroup) view, className, hashCode);
573             }
574         } else {
575             // Look by id
576             final int id = root.getResources().getIdentifier(parameter, null, null);
577             return root.getRootView().findViewById(id);
578         }
579 
580         return null;
581     }
582 
invalidate(View root, String parameter)583     private static void invalidate(View root, String parameter) {
584         final View view = findView(root, parameter);
585         if (view != null) {
586             view.postInvalidate();
587         }
588     }
589 
requestLayout(View root, String parameter)590     private static void requestLayout(View root, String parameter) {
591         final View view = findView(root, parameter);
592         if (view != null) {
593             root.post(new Runnable() {
594                 public void run() {
595                     view.requestLayout();
596                 }
597             });
598         }
599     }
600 
profile(View root, OutputStream clientStream, String parameter)601     private static void profile(View root, OutputStream clientStream, String parameter)
602             throws IOException {
603 
604         final View view = findView(root, parameter);
605         BufferedWriter out = null;
606         try {
607             out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
608 
609             if (view != null) {
610                 profileViewAndChildren(view, out);
611             } else {
612                 out.write("-1 -1 -1");
613                 out.newLine();
614             }
615             out.write("DONE.");
616             out.newLine();
617         } catch (Exception e) {
618             android.util.Log.w("View", "Problem profiling the view:", e);
619         } finally {
620             if (out != null) {
621                 out.close();
622             }
623         }
624     }
625 
626     /** @hide */
profileViewAndChildren(final View view, BufferedWriter out)627     public static void profileViewAndChildren(final View view, BufferedWriter out)
628             throws IOException {
629         RenderNode node = RenderNode.create("ViewDebug", null);
630         profileViewAndChildren(view, node, out, true);
631     }
632 
profileViewAndChildren(View view, RenderNode node, BufferedWriter out, boolean root)633     private static void profileViewAndChildren(View view, RenderNode node, BufferedWriter out,
634             boolean root) throws IOException {
635         long durationMeasure =
636                 (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0)
637                         ? profileViewMeasure(view) : 0;
638         long durationLayout =
639                 (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0)
640                         ? profileViewLayout(view) : 0;
641         long durationDraw =
642                 (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0)
643                         ? profileViewDraw(view, node) : 0;
644 
645         out.write(String.valueOf(durationMeasure));
646         out.write(' ');
647         out.write(String.valueOf(durationLayout));
648         out.write(' ');
649         out.write(String.valueOf(durationDraw));
650         out.newLine();
651         if (view instanceof ViewGroup) {
652             ViewGroup group = (ViewGroup) view;
653             final int count = group.getChildCount();
654             for (int i = 0; i < count; i++) {
655                 profileViewAndChildren(group.getChildAt(i), node, out, false);
656             }
657         }
658     }
659 
profileViewMeasure(final View view)660     private static long profileViewMeasure(final View view) {
661         return profileViewOperation(view, new ViewOperation() {
662             @Override
663             public void pre() {
664                 forceLayout(view);
665             }
666 
667             private void forceLayout(View view) {
668                 view.forceLayout();
669                 if (view instanceof ViewGroup) {
670                     ViewGroup group = (ViewGroup) view;
671                     final int count = group.getChildCount();
672                     for (int i = 0; i < count; i++) {
673                         forceLayout(group.getChildAt(i));
674                     }
675                 }
676             }
677 
678             @Override
679             public void run() {
680                 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
681             }
682         });
683     }
684 
685     private static long profileViewLayout(View view) {
686         return profileViewOperation(view,
687                 () -> view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom));
688     }
689 
690     private static long profileViewDraw(View view, RenderNode node) {
691         DisplayMetrics dm = view.getResources().getDisplayMetrics();
692         if (dm == null) {
693             return 0;
694         }
695 
696         if (view.isHardwareAccelerated()) {
697             RecordingCanvas canvas = node.beginRecording(dm.widthPixels, dm.heightPixels);
698             try {
699                 return profileViewOperation(view, () -> view.draw(canvas));
700             } finally {
701                 node.endRecording();
702             }
703         } else {
704             Bitmap bitmap = Bitmap.createBitmap(
705                     dm, dm.widthPixels, dm.heightPixels, Bitmap.Config.RGB_565);
706             Canvas canvas = new Canvas(bitmap);
707             try {
708                 return profileViewOperation(view, () -> view.draw(canvas));
709             } finally {
710                 canvas.setBitmap(null);
711                 bitmap.recycle();
712             }
713         }
714     }
715 
716     interface ViewOperation {
717         default void pre() {}
718 
719         void run();
720     }
721 
722     private static long profileViewOperation(View view, final ViewOperation operation) {
723         final CountDownLatch latch = new CountDownLatch(1);
724         final long[] duration = new long[1];
725 
726         view.post(() -> {
727             try {
728                 operation.pre();
729                 long start = Debug.threadCpuTimeNanos();
730                 //noinspection unchecked
731                 operation.run();
732                 duration[0] = Debug.threadCpuTimeNanos() - start;
733             } finally {
734                 latch.countDown();
735             }
736         });
737 
738         try {
739             if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) {
740                 Log.w("View", "Could not complete the profiling of the view " + view);
741                 return -1;
742             }
743         } catch (InterruptedException e) {
744             Log.w("View", "Could not complete the profiling of the view " + view);
745             Thread.currentThread().interrupt();
746             return -1;
747         }
748 
749         return duration[0];
750     }
751 
752     /** @hide */
753     public static void captureLayers(View root, final DataOutputStream clientStream)
754             throws IOException {
755 
756         try {
757             Rect outRect = new Rect();
758             root.mAttachInfo.mViewRootImpl.getDisplayFrame(outRect);
759 
760             clientStream.writeInt(outRect.width());
761             clientStream.writeInt(outRect.height());
762 
763             captureViewLayer(root, clientStream, true);
764 
765             clientStream.write(2);
766         } finally {
767             clientStream.close();
768         }
769     }
770 
771     private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
772             throws IOException {
773 
774         final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
775 
776         if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) {
777             final int id = view.getId();
778             String name = view.getClass().getSimpleName();
779             if (id != View.NO_ID) {
780                 name = resolveId(view.getContext(), id).toString();
781             }
782 
783             clientStream.write(1);
784             clientStream.writeUTF(name);
785             clientStream.writeByte(localVisible ? 1 : 0);
786 
787             int[] position = new int[2];
788             // XXX: Should happen on the UI thread
789             view.getLocationInWindow(position);
790 
791             clientStream.writeInt(position[0]);
792             clientStream.writeInt(position[1]);
793             clientStream.flush();
794 
795             Bitmap b = performViewCapture(view, true);
796             if (b != null) {
797                 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
798                         b.getHeight() * 2);
799                 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
800                 clientStream.writeInt(arrayOut.size());
801                 arrayOut.writeTo(clientStream);
802             }
803             clientStream.flush();
804         }
805 
806         if (view instanceof ViewGroup) {
807             ViewGroup group = (ViewGroup) view;
808             int count = group.getChildCount();
809 
810             for (int i = 0; i < count; i++) {
811                 captureViewLayer(group.getChildAt(i), clientStream, localVisible);
812             }
813         }
814 
815         if (view.mOverlay != null) {
816             ViewGroup overlayContainer = view.getOverlay().mOverlayViewGroup;
817             captureViewLayer(overlayContainer, clientStream, localVisible);
818         }
819     }
820 
821     private static void outputDisplayList(View root, String parameter) throws IOException {
822         final View view = findView(root, parameter);
823         view.getViewRootImpl().outputDisplayList(view);
824     }
825 
826     /** @hide */
827     public static void outputDisplayList(View root, View target) {
828         root.getViewRootImpl().outputDisplayList(target);
829     }
830 
831     private static class PictureCallbackHandler implements AutoCloseable,
832             HardwareRenderer.PictureCapturedCallback, Runnable {
833         private final HardwareRenderer mRenderer;
834         private final Function<Picture, Boolean> mCallback;
835         private final Executor mExecutor;
836         private final ReentrantLock mLock = new ReentrantLock(false);
837         private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
838         private boolean mStopListening;
839         private Thread mRenderThread;
840 
841         private PictureCallbackHandler(HardwareRenderer renderer,
842                 Function<Picture, Boolean> callback, Executor executor) {
843             mRenderer = renderer;
844             mCallback = callback;
845             mExecutor = executor;
846             mRenderer.setPictureCaptureCallback(this);
847         }
848 
849         @Override
850         public void close() {
851             mLock.lock();
852             mStopListening = true;
853             mLock.unlock();
854             mRenderer.setPictureCaptureCallback(null);
855         }
856 
857         @Override
858         public void onPictureCaptured(Picture picture) {
859             mLock.lock();
860             if (mStopListening) {
861                 mLock.unlock();
862                 mRenderer.setPictureCaptureCallback(null);
863                 return;
864             }
865             if (mRenderThread == null) {
866                 mRenderThread = Thread.currentThread();
867             }
868             Picture toDestroy = null;
869             if (mQueue.size() == 3) {
870                 toDestroy = mQueue.removeLast();
871             }
872             mQueue.add(picture);
873             mLock.unlock();
874             if (toDestroy == null) {
875                 mExecutor.execute(this);
876             } else {
877                 toDestroy.close();
878             }
879         }
880 
881         @Override
882         public void run() {
883             mLock.lock();
884             final Picture picture = mQueue.poll();
885             final boolean isStopped = mStopListening;
886             mLock.unlock();
887             if (Thread.currentThread() == mRenderThread) {
888                 close();
889                 throw new IllegalStateException(
890                         "ViewDebug#startRenderingCommandsCapture must be given an executor that "
891                         + "invokes asynchronously");
892             }
893             if (isStopped) {
894                 picture.close();
895                 return;
896             }
897             final boolean keepReceiving = mCallback.apply(picture);
898             if (!keepReceiving) {
899                 close();
900             }
901         }
902     }
903 
904     /**
905      * Begins capturing the entire rendering commands for the view tree referenced by the given
906      * view. The view passed may be any View in the tree as long as it is attached. That is,
907      * {@link View#isAttachedToWindow()} must be true.
908      *
909      * Every time a frame is rendered a Picture will be passed to the given callback via the given
910      * executor. As long as the callback returns 'true' it will continue to receive new frames.
911      * The system will only invoke the callback at a rate that the callback is able to keep up with.
912      * That is, if it takes 48ms for the callback to complete and there is a 60fps animation running
913      * then the callback will only receive 33% of the frames produced.
914      *
915      * This method must be called on the same thread as the View tree.
916      *
917      * @param tree The View tree to capture the rendering commands.
918      * @param callback The callback to invoke on every frame produced. Should return true to
919      *                 continue receiving new frames, false to stop capturing.
920      * @param executor The executor to invoke the callback on. Recommend using a background thread
921      *                 to avoid stalling the UI thread. Must be an asynchronous invoke or an
922      *                 exception will be thrown.
923      * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
924      * that the callback may continue to receive another frame or two depending on thread timings.
925      * Returns null if the capture stream cannot be started, such as if there's no
926      * HardwareRenderer for the given view tree.
927      * @hide
928      * @deprecated use {@link #startRenderingCommandsCapture(View, Executor, Callable)} instead.
929      */
930     @TestApi
931     @Nullable
932     @Deprecated
933     public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
934             Function<Picture, Boolean> callback) {
935         final View.AttachInfo attachInfo = tree.mAttachInfo;
936         if (attachInfo == null) {
937             throw new IllegalArgumentException("Given view isn't attached");
938         }
939         if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
940             throw new IllegalStateException("Called on the wrong thread."
941                     + " Must be called on the thread that owns the given View");
942         }
943         final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
944         if (renderer != null) {
945             return new PictureCallbackHandler(renderer, callback, executor);
946         }
947         return null;
948     }
949 
950     private static class StreamingPictureCallbackHandler implements AutoCloseable,
951             HardwareRenderer.PictureCapturedCallback, Runnable {
952         private final HardwareRenderer mRenderer;
953         private final Callable<OutputStream> mCallback;
954         private final Executor mExecutor;
955         private final ReentrantLock mLock = new ReentrantLock(false);
956         private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
957         private boolean mStopListening;
958         private Thread mRenderThread;
959 
960         private StreamingPictureCallbackHandler(HardwareRenderer renderer,
961                 Callable<OutputStream> callback, Executor executor) {
962             mRenderer = renderer;
963             mCallback = callback;
964             mExecutor = executor;
965             mRenderer.setPictureCaptureCallback(this);
966         }
967 
968         @Override
969         public void close() {
970             mLock.lock();
971             mStopListening = true;
972             mLock.unlock();
973             mRenderer.setPictureCaptureCallback(null);
974         }
975 
976         @Override
977         public void onPictureCaptured(Picture picture) {
978             mLock.lock();
979             if (mStopListening) {
980                 mLock.unlock();
981                 mRenderer.setPictureCaptureCallback(null);
982                 return;
983             }
984             if (mRenderThread == null) {
985                 mRenderThread = Thread.currentThread();
986             }
987             boolean needsInvoke = true;
988             if (mQueue.size() == 3) {
989                 mQueue.removeLast();
990                 needsInvoke = false;
991             }
992             mQueue.add(picture);
993             mLock.unlock();
994 
995             if (needsInvoke) {
996                 mExecutor.execute(this);
997             }
998         }
999 
1000         @Override
1001         public void run() {
1002             mLock.lock();
1003             final Picture picture = mQueue.poll();
1004             final boolean isStopped = mStopListening;
1005             mLock.unlock();
1006             if (Thread.currentThread() == mRenderThread) {
1007                 close();
1008                 throw new IllegalStateException(
1009                         "ViewDebug#startRenderingCommandsCapture must be given an executor that "
1010                         + "invokes asynchronously");
1011             }
1012             if (isStopped) {
1013                 return;
1014             }
1015             OutputStream stream = null;
1016             try {
1017                 stream = mCallback.call();
1018             } catch (Exception ex) {
1019                 Log.w("ViewDebug", "Aborting rendering commands capture "
1020                         + "because callback threw exception", ex);
1021             }
1022             if (stream != null) {
1023                 try {
1024                     picture.writeToStream(stream);
1025                     stream.flush();
1026                 } catch (IOException ex) {
1027                     Log.w("ViewDebug", "Aborting rendering commands capture "
1028                             + "due to IOException writing to output stream", ex);
1029                 }
1030             } else {
1031                 close();
1032             }
1033         }
1034     }
1035 
1036     /**
1037      * Begins capturing the entire rendering commands for the view tree referenced by the given
1038      * view. The view passed may be any View in the tree as long as it is attached. That is,
1039      * {@link View#isAttachedToWindow()} must be true.
1040      *
1041      * Every time a frame is rendered the callback will be invoked on the given executor to
1042      * provide an OutputStream to serialize to. As long as the callback returns a valid
1043      * OutputStream the capturing will continue. The system will only invoke the callback at a rate
1044      * that the callback & OutputStream is able to keep up with. That is, if it takes 48ms for the
1045      * callback & serialization to complete and there is a 60fps animation running
1046      * then the callback will only receive 33% of the frames produced.
1047      *
1048      * This method must be called on the same thread as the View tree.
1049      *
1050      * @param tree The View tree to capture the rendering commands.
1051      * @param callback The callback to invoke on every frame produced. Should return an
1052      *                 OutputStream to write the data to. Return null to cancel capture. The
1053      *                 same stream may be returned each time as the serialized data contains
1054      *                 start & end markers. The callback will not be invoked while a previous
1055      *                 serialization is being performed, so if a single continuous stream is being
1056      *                 used it is valid for the callback to write its own metadata to that stream
1057      *                 in response to callback invocation.
1058      * @param executor The executor to invoke the callback on. Recommend using a background thread
1059      *                 to avoid stalling the UI thread. Must be an asynchronous invoke or an
1060      *                 exception will be thrown.
1061      * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
1062      * that the callback may continue to receive another frame or two depending on thread timings.
1063      * Returns null if the capture stream cannot be started, such as if there's no
1064      * HardwareRenderer for the given view tree.
1065      * @hide
1066      */
1067     @TestApi
1068     @Nullable
1069     @UnsupportedAppUsage // Visible for Studio; least-worst option available
1070     public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
1071             Callable<OutputStream> callback) {
1072         final View.AttachInfo attachInfo = tree.mAttachInfo;
1073         if (attachInfo == null) {
1074             throw new IllegalArgumentException("Given view isn't attached");
1075         }
1076         if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
1077             throw new IllegalStateException("Called on the wrong thread."
1078                     + " Must be called on the thread that owns the given View");
1079         }
1080         final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
1081         if (renderer != null) {
1082             return new StreamingPictureCallbackHandler(renderer, callback, executor);
1083         }
1084         return null;
1085     }
1086 
1087     private static void capture(View root, final OutputStream clientStream, String parameter)
1088             throws IOException {
1089 
1090         final View captureView = findView(root, parameter);
1091         capture(root, clientStream, captureView);
1092     }
1093 
1094     /** @hide */
1095     public static void capture(View root, final OutputStream clientStream, View captureView)
1096             throws IOException {
1097         Bitmap b = performViewCapture(captureView, false);
1098 
1099         if (b == null) {
1100             Log.w("View", "Failed to create capture bitmap!");
1101             // Send an empty one so that it doesn't get stuck waiting for
1102             // something.
1103             b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(),
1104                     1, 1, Bitmap.Config.ARGB_8888);
1105         }
1106 
1107         BufferedOutputStream out = null;
1108         try {
1109             out = new BufferedOutputStream(clientStream, 32 * 1024);
1110             b.compress(Bitmap.CompressFormat.PNG, 100, out);
1111             out.flush();
1112         } finally {
1113             if (out != null) {
1114                 out.close();
1115             }
1116             b.recycle();
1117         }
1118     }
1119 
1120     private static Bitmap performViewCapture(final View captureView, final boolean skipChildren) {
1121         if (captureView != null) {
1122             final CountDownLatch latch = new CountDownLatch(1);
1123             final Bitmap[] cache = new Bitmap[1];
1124 
1125             captureView.post(() -> {
1126                 try {
1127                     CanvasProvider provider = captureView.isHardwareAccelerated()
1128                             ? new HardwareCanvasProvider() : new SoftwareCanvasProvider();
1129                     cache[0] = captureView.createSnapshot(provider, skipChildren);
1130                 } catch (OutOfMemoryError e) {
1131                     Log.w("View", "Out of memory for bitmap");
1132                 } finally {
1133                     latch.countDown();
1134                 }
1135             });
1136 
1137             try {
1138                 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
1139                 return cache[0];
1140             } catch (InterruptedException e) {
1141                 Log.w("View", "Could not complete the capture of the view " + captureView);
1142                 Thread.currentThread().interrupt();
1143             }
1144         }
1145 
1146         return null;
1147     }
1148 
1149     /**
1150      * Dumps the view hierarchy starting from the given view.
1151      * @deprecated See {@link #dumpv2(View, ByteArrayOutputStream)} below.
1152      * @hide
1153      */
1154     @Deprecated
1155     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1156     public static void dump(View root, boolean skipChildren, boolean includeProperties,
1157             OutputStream clientStream) throws IOException {
1158         BufferedWriter out = null;
1159         try {
1160             out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
1161             View view = root.getRootView();
1162             if (view instanceof ViewGroup) {
1163                 ViewGroup group = (ViewGroup) view;
1164                 dumpViewHierarchy(group.getContext(), group, out, 0,
1165                         skipChildren, includeProperties);
1166             }
1167             out.write("DONE.");
1168             out.newLine();
1169         } catch (Exception e) {
1170             android.util.Log.w("View", "Problem dumping the view:", e);
1171         } finally {
1172             if (out != null) {
1173                 out.close();
1174             }
1175         }
1176     }
1177 
1178     /**
1179      * Dumps the view hierarchy starting from the given view.
1180      * Rather than using reflection, it uses View's encode method to obtain all the properties.
1181      * @hide
1182      */
1183     public static void dumpv2(@NonNull final View view, @NonNull ByteArrayOutputStream out)
1184             throws InterruptedException {
1185         final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(out);
1186         final CountDownLatch latch = new CountDownLatch(1);
1187 
1188         view.post(new Runnable() {
1189             @Override
1190             public void run() {
1191                 encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
1192                 encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
1193                 view.encode(encoder);
1194                 latch.countDown();
1195             }
1196         });
1197 
1198         latch.await(2, TimeUnit.SECONDS);
1199         encoder.endStream();
1200     }
1201 
1202     private static void dumpEncoded(@NonNull final View view, @NonNull OutputStream out)
1203             throws IOException {
1204         ByteArrayOutputStream baOut = new ByteArrayOutputStream();
1205 
1206         final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(baOut);
1207         encoder.setUserPropertiesEnabled(false);
1208         encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
1209         encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
1210         view.encode(encoder);
1211         encoder.endStream();
1212         out.write(baOut.toByteArray());
1213     }
1214 
1215     /**
1216      * Dumps the theme attributes from the given View.
1217      * @hide
1218      */
1219     public static void dumpTheme(View view, OutputStream clientStream) throws IOException {
1220         BufferedWriter out = null;
1221         try {
1222             out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
1223             String[] attributes = getStyleAttributesDump(view.getContext().getResources(),
1224                     view.getContext().getTheme());
1225             if (attributes != null) {
1226                 for (int i = 0; i < attributes.length; i += 2) {
1227                     if (attributes[i] != null) {
1228                         out.write(attributes[i] + "\n");
1229                         out.write(attributes[i + 1] + "\n");
1230                     }
1231                 }
1232             }
1233             out.write("DONE.");
1234             out.newLine();
1235         } catch (Exception e) {
1236             android.util.Log.w("View", "Problem dumping View Theme:", e);
1237         } finally {
1238             if (out != null) {
1239                 out.close();
1240             }
1241         }
1242     }
1243 
1244     /**
1245      * Gets the style attributes from the {@link Resources.Theme}. For debugging only.
1246      *
1247      * @param resources Resources to resolve attributes from.
1248      * @param theme Theme to dump.
1249      * @return a String array containing pairs of adjacent Theme attribute data: name followed by
1250      * its value.
1251      *
1252      * @hide
1253      */
1254     private static String[] getStyleAttributesDump(Resources resources, Resources.Theme theme) {
1255         TypedValue outValue = new TypedValue();
1256         String nullString = "null";
1257         int i = 0;
1258         int[] attributes = theme.getAllAttributes();
1259         String[] data = new String[attributes.length * 2];
1260         for (int attributeId : attributes) {
1261             try {
1262                 data[i] = resources.getResourceName(attributeId);
1263                 data[i + 1] = theme.resolveAttribute(attributeId, outValue, true) ?
1264                         outValue.coerceToString().toString() :  nullString;
1265                 i += 2;
1266 
1267                 // attempt to replace reference data with its name
1268                 if (outValue.type == TypedValue.TYPE_REFERENCE) {
1269                     data[i - 1] = resources.getResourceName(outValue.resourceId);
1270                 }
1271             } catch (Resources.NotFoundException e) {
1272                 // ignore resources we can't resolve
1273             }
1274         }
1275         return data;
1276     }
1277 
1278     private static View findView(ViewGroup group, String className, int hashCode) {
1279         if (isRequestedView(group, className, hashCode)) {
1280             return group;
1281         }
1282 
1283         final int count = group.getChildCount();
1284         for (int i = 0; i < count; i++) {
1285             final View view = group.getChildAt(i);
1286             if (view instanceof ViewGroup) {
1287                 final View found = findView((ViewGroup) view, className, hashCode);
1288                 if (found != null) {
1289                     return found;
1290                 }
1291             } else if (isRequestedView(view, className, hashCode)) {
1292                 return view;
1293             }
1294             if (view.mOverlay != null) {
1295                 final View found = findView((ViewGroup) view.mOverlay.mOverlayViewGroup,
1296                         className, hashCode);
1297                 if (found != null) {
1298                     return found;
1299                 }
1300             }
1301             if (view instanceof HierarchyHandler) {
1302                 final View found = ((HierarchyHandler)view)
1303                         .findHierarchyView(className, hashCode);
1304                 if (found != null) {
1305                     return found;
1306                 }
1307             }
1308         }
1309         return null;
1310     }
1311 
1312     private static boolean isRequestedView(View view, String className, int hashCode) {
1313         if (view.hashCode() == hashCode) {
1314             String viewClassName = view.getClass().getName();
1315             if (className.equals("ViewOverlay")) {
1316                 return viewClassName.equals("android.view.ViewOverlay$OverlayViewGroup");
1317             } else {
1318                 return className.equals(viewClassName);
1319             }
1320         }
1321         return false;
1322     }
1323 
1324     private static void dumpViewHierarchy(Context context, ViewGroup group,
1325             BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
1326         cacheExportedProperties(group.getClass());
1327         if (!skipChildren) {
1328             cacheExportedPropertiesForChildren(group);
1329         }
1330         // Try to use the handler provided by the view
1331         Handler handler = group.getHandler();
1332         // Fall back on using the main thread
1333         if (handler == null) {
1334             handler = new Handler(Looper.getMainLooper());
1335         }
1336 
1337         if (handler.getLooper() == Looper.myLooper()) {
1338             dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren,
1339                     includeProperties);
1340         } else {
1341             FutureTask task = new FutureTask(() ->
1342                     dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren,
1343                             includeProperties), null);
1344             Message msg = Message.obtain(handler, task);
1345             msg.setAsynchronous(true);
1346             handler.sendMessage(msg);
1347             while (true) {
1348                 try {
1349                     task.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
1350                     return;
1351                 } catch (InterruptedException e) {
1352                     // try again
1353                 } catch (ExecutionException | TimeoutException e) {
1354                     // Something unexpected happened.
1355                     throw new RuntimeException(e);
1356                 }
1357             }
1358         }
1359     }
1360 
1361     private static void cacheExportedPropertiesForChildren(ViewGroup group) {
1362         final int count = group.getChildCount();
1363         for (int i = 0; i < count; i++) {
1364             final View view = group.getChildAt(i);
1365             cacheExportedProperties(view.getClass());
1366             if (view instanceof ViewGroup) {
1367                 cacheExportedPropertiesForChildren((ViewGroup) view);
1368             }
1369         }
1370     }
1371 
1372     private static void cacheExportedProperties(Class<?> klass) {
1373         if (sExportProperties != null && sExportProperties.containsKey(klass)) {
1374             return;
1375         }
1376         do {
1377             for (PropertyInfo<ExportedProperty, ?> info : getExportedProperties(klass)) {
1378                 if (!info.returnType.isPrimitive() && info.property.deepExport()) {
1379                     cacheExportedProperties(info.returnType);
1380                 }
1381             }
1382             klass = klass.getSuperclass();
1383         } while (klass != Object.class);
1384     }
1385 
1386 
1387     private static void dumpViewHierarchyOnUIThread(Context context, ViewGroup group,
1388             BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
1389         if (!dumpView(context, group, out, level, includeProperties)) {
1390             return;
1391         }
1392 
1393         if (skipChildren) {
1394             return;
1395         }
1396 
1397         final int count = group.getChildCount();
1398         for (int i = 0; i < count; i++) {
1399             final View view = group.getChildAt(i);
1400             if (view instanceof ViewGroup) {
1401                 dumpViewHierarchyOnUIThread(context, (ViewGroup) view, out, level + 1,
1402                         skipChildren, includeProperties);
1403             } else {
1404                 dumpView(context, view, out, level + 1, includeProperties);
1405             }
1406             if (view.mOverlay != null) {
1407                 ViewOverlay overlay = view.getOverlay();
1408                 ViewGroup overlayContainer = overlay.mOverlayViewGroup;
1409                 dumpViewHierarchyOnUIThread(context, overlayContainer, out, level + 2,
1410                         skipChildren, includeProperties);
1411             }
1412         }
1413         if (group instanceof HierarchyHandler) {
1414             ((HierarchyHandler)group).dumpViewHierarchyWithProperties(out, level + 1);
1415         }
1416     }
1417 
1418     private static boolean dumpView(Context context, View view,
1419             BufferedWriter out, int level, boolean includeProperties) {
1420 
1421         try {
1422             for (int i = 0; i < level; i++) {
1423                 out.write(' ');
1424             }
1425             String className = view.getClass().getName();
1426             if (className.equals("android.view.ViewOverlay$OverlayViewGroup")) {
1427                 className = "ViewOverlay";
1428             }
1429             out.write(className);
1430             out.write('@');
1431             out.write(Integer.toHexString(view.hashCode()));
1432             out.write(' ');
1433             if (includeProperties) {
1434                 dumpViewProperties(context, view, out);
1435             }
1436             out.newLine();
1437         } catch (IOException e) {
1438             Log.w("View", "Error while dumping hierarchy tree");
1439             return false;
1440         }
1441         return true;
1442     }
1443 
1444     private static <T extends Annotation> PropertyInfo<T, ?>[] convertToPropertyInfos(
1445             Method[] methods, Field[] fields, Class<T> property) {
1446         return Stream.of(Arrays.stream(methods).map(m -> PropertyInfo.forMethod(m, property)),
1447                 Arrays.stream(fields).map(f -> PropertyInfo.forField(f, property)))
1448                 .flatMap(Function.identity())
1449                 .filter(i -> i != null)
1450                 .toArray(PropertyInfo[]::new);
1451     }
1452 
1453     private static PropertyInfo<ExportedProperty, ?>[] getExportedProperties(Class<?> klass) {
1454         if (sExportProperties == null) {
1455             sExportProperties = new HashMap<>();
1456         }
1457         final HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> map = sExportProperties;
1458         PropertyInfo<ExportedProperty, ?>[] properties = sExportProperties.get(klass);
1459 
1460         if (properties == null) {
1461             properties = convertToPropertyInfos(klass.getDeclaredMethods(),
1462                     klass.getDeclaredFields(), ExportedProperty.class);
1463             map.put(klass, properties);
1464         }
1465         return properties;
1466     }
1467 
1468     private static void dumpViewProperties(Context context, Object view,
1469             BufferedWriter out) throws IOException {
1470 
1471         dumpViewProperties(context, view, out, "");
1472     }
1473 
1474     private static void dumpViewProperties(Context context, Object view,
1475             BufferedWriter out, String prefix) throws IOException {
1476 
1477         if (view == null) {
1478             out.write(prefix + "=4,null ");
1479             return;
1480         }
1481 
1482         Class<?> klass = view.getClass();
1483         do {
1484             writeExportedProperties(context, view, out, klass, prefix);
1485             klass = klass.getSuperclass();
1486         } while (klass != Object.class);
1487     }
1488 
1489     private static String formatIntToHexString(int value) {
1490         return "0x" + Integer.toHexString(value).toUpperCase();
1491     }
1492 
1493     private static void writeExportedProperties(Context context, Object view, BufferedWriter out,
1494             Class<?> klass, String prefix) throws IOException {
1495         for (PropertyInfo<ExportedProperty, ?> info : getExportedProperties(klass)) {
1496             //noinspection EmptyCatchBlock
1497             Object value;
1498             try {
1499                 value = info.invoke(view);
1500             } catch (Exception e) {
1501                 // ignore
1502                 continue;
1503             }
1504 
1505             String categoryPrefix =
1506                     info.property.category().length() != 0 ? info.property.category() + ":" : "";
1507 
1508             if (info.returnType == int.class || info.returnType == byte.class) {
1509                 if (info.property.resolveId() && context != null) {
1510                     final int id = (Integer) value;
1511                     value = resolveId(context, id);
1512 
1513                 } else if (info.property.formatToHexString()) {
1514                     if (info.returnType == int.class) {
1515                         value = formatIntToHexString((Integer) value);
1516                     } else if (info.returnType == byte.class) {
1517                         value = "0x"
1518                                 + HexEncoding.encodeToString((Byte) value, true);
1519                     }
1520                 } else {
1521                     final ViewDebug.FlagToString[] flagsMapping = info.property.flagMapping();
1522                     if (flagsMapping.length > 0) {
1523                         final int intValue = (Integer) value;
1524                         final String valuePrefix =
1525                                 categoryPrefix + prefix + info.name + '_';
1526                         exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1527                     }
1528 
1529                     final ViewDebug.IntToString[] mapping = info.property.mapping();
1530                     if (mapping.length > 0) {
1531                         final int intValue = (Integer) value;
1532                         boolean mapped = false;
1533                         int mappingCount = mapping.length;
1534                         for (int j = 0; j < mappingCount; j++) {
1535                             final ViewDebug.IntToString mapper = mapping[j];
1536                             if (mapper.from() == intValue) {
1537                                 value = mapper.to();
1538                                 mapped = true;
1539                                 break;
1540                             }
1541                         }
1542 
1543                         if (!mapped) {
1544                             value = intValue;
1545                         }
1546                     }
1547                 }
1548             } else if (info.returnType == int[].class) {
1549                 final int[] array = (int[]) value;
1550                 final String valuePrefix = categoryPrefix + prefix + info.name + '_';
1551                 exportUnrolledArray(context, out, info.property, array, valuePrefix,
1552                         info.entrySuffix);
1553 
1554                 continue;
1555             } else if (info.returnType == String[].class) {
1556                 final String[] array = (String[]) value;
1557                 if (info.property.hasAdjacentMapping() && array != null) {
1558                     for (int j = 0; j < array.length; j += 2) {
1559                         if (array[j] != null) {
1560                             writeEntry(out, categoryPrefix + prefix, array[j],
1561                                     info.entrySuffix, array[j + 1] == null ? "null" : array[j + 1]);
1562                         }
1563                     }
1564                 }
1565 
1566                 continue;
1567             } else if (!info.returnType.isPrimitive()) {
1568                 if (info.property.deepExport()) {
1569                     dumpViewProperties(context, value, out, prefix + info.property.prefix());
1570                     continue;
1571                 }
1572             }
1573 
1574             writeEntry(out, categoryPrefix + prefix, info.name, info.entrySuffix, value);
1575         }
1576     }
1577 
1578     private static void writeEntry(BufferedWriter out, String prefix, String name,
1579             String suffix, Object value) throws IOException {
1580 
1581         out.write(prefix);
1582         out.write(name);
1583         out.write(suffix);
1584         out.write("=");
1585         writeValue(out, value);
1586         out.write(' ');
1587     }
1588 
1589     private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
1590             int intValue, String prefix) throws IOException {
1591 
1592         final int count = mapping.length;
1593         for (int j = 0; j < count; j++) {
1594             final FlagToString flagMapping = mapping[j];
1595             final boolean ifTrue = flagMapping.outputIf();
1596             final int maskResult = intValue & flagMapping.mask();
1597             final boolean test = maskResult == flagMapping.equals();
1598             if ((test && ifTrue) || (!test && !ifTrue)) {
1599                 final String name = flagMapping.name();
1600                 final String value = formatIntToHexString(maskResult);
1601                 writeEntry(out, prefix, name, "", value);
1602             }
1603         }
1604     }
1605 
1606     /**
1607      * Converts an integer from a field that is mapped with {@link IntToString} to its string
1608      * representation.
1609      *
1610      * @param clazz The class the field is defined on.
1611      * @param field The field on which the {@link ExportedProperty} is defined on.
1612      * @param integer The value to convert.
1613      * @return The value converted into its string representation.
1614      * @hide
1615      */
1616     public static String intToString(Class<?> clazz, String field, int integer) {
1617         final IntToString[] mapping = getMapping(clazz, field);
1618         if (mapping == null) {
1619             return Integer.toString(integer);
1620         }
1621         final int count = mapping.length;
1622         for (int j = 0; j < count; j++) {
1623             final IntToString map = mapping[j];
1624             if (map.from() == integer) {
1625                 return map.to();
1626             }
1627         }
1628         return Integer.toString(integer);
1629     }
1630 
1631     /**
1632      * Converts a set of flags from a field that is mapped with {@link FlagToString} to its string
1633      * representation.
1634      *
1635      * @param clazz The class the field is defined on.
1636      * @param field The field on which the {@link ExportedProperty} is defined on.
1637      * @param flags The flags to convert.
1638      * @return The flags converted into their string representations.
1639      * @hide
1640      */
1641     public static String flagsToString(Class<?> clazz, String field, int flags) {
1642         final FlagToString[] mapping = getFlagMapping(clazz, field);
1643         if (mapping == null) {
1644             return Integer.toHexString(flags);
1645         }
1646         final StringBuilder result = new StringBuilder();
1647         final int count = mapping.length;
1648         for (int j = 0; j < count; j++) {
1649             final FlagToString flagMapping = mapping[j];
1650             final boolean ifTrue = flagMapping.outputIf();
1651             final int maskResult = flags & flagMapping.mask();
1652             final boolean test = maskResult == flagMapping.equals();
1653             if (test && ifTrue) {
1654                 final String name = flagMapping.name();
1655                 result.append(name).append(' ');
1656             }
1657         }
1658         if (result.length() > 0) {
1659             result.deleteCharAt(result.length() - 1);
1660         }
1661         return result.toString();
1662     }
1663 
1664     private static FlagToString[] getFlagMapping(Class<?> clazz, String field) {
1665         try {
1666             return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class)
1667                     .flagMapping();
1668         } catch (NoSuchFieldException e) {
1669             return null;
1670         }
1671     }
1672 
1673     private static IntToString[] getMapping(Class<?> clazz, String field) {
1674         try {
1675             return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class).mapping();
1676         } catch (NoSuchFieldException e) {
1677             return null;
1678         }
1679     }
1680 
1681     private static void exportUnrolledArray(Context context, BufferedWriter out,
1682             ExportedProperty property, int[] array, String prefix, String suffix)
1683             throws IOException {
1684 
1685         final IntToString[] indexMapping = property.indexMapping();
1686         final boolean hasIndexMapping = indexMapping.length > 0;
1687 
1688         final IntToString[] mapping = property.mapping();
1689         final boolean hasMapping = mapping.length > 0;
1690 
1691         final boolean resolveId = property.resolveId() && context != null;
1692         final int valuesCount = array.length;
1693 
1694         for (int j = 0; j < valuesCount; j++) {
1695             String name;
1696             String value = null;
1697 
1698             final int intValue = array[j];
1699 
1700             name = String.valueOf(j);
1701             if (hasIndexMapping) {
1702                 int mappingCount = indexMapping.length;
1703                 for (int k = 0; k < mappingCount; k++) {
1704                     final IntToString mapped = indexMapping[k];
1705                     if (mapped.from() == j) {
1706                         name = mapped.to();
1707                         break;
1708                     }
1709                 }
1710             }
1711 
1712             if (hasMapping) {
1713                 int mappingCount = mapping.length;
1714                 for (int k = 0; k < mappingCount; k++) {
1715                     final IntToString mapped = mapping[k];
1716                     if (mapped.from() == intValue) {
1717                         value = mapped.to();
1718                         break;
1719                     }
1720                 }
1721             }
1722 
1723             if (resolveId) {
1724                 if (value == null) value = (String) resolveId(context, intValue);
1725             } else {
1726                 value = String.valueOf(intValue);
1727             }
1728 
1729             writeEntry(out, prefix, name, suffix, value);
1730         }
1731     }
1732 
1733     static Object resolveId(Context context, int id) {
1734         Object fieldValue;
1735         final Resources resources = context.getResources();
1736         if (id >= 0) {
1737             try {
1738                 fieldValue = resources.getResourceTypeName(id) + '/' +
1739                         resources.getResourceEntryName(id);
1740             } catch (Resources.NotFoundException e) {
1741                 fieldValue = "id/" + formatIntToHexString(id);
1742             }
1743         } else {
1744             fieldValue = "NO_ID";
1745         }
1746         return fieldValue;
1747     }
1748 
1749     private static void writeValue(BufferedWriter out, Object value) throws IOException {
1750         if (value != null) {
1751             String output = "[EXCEPTION]";
1752             try {
1753                 output = value.toString().replace("\n", "\\n");
1754             } finally {
1755                 out.write(String.valueOf(output.length()));
1756                 out.write(",");
1757                 out.write(output);
1758             }
1759         } else {
1760             out.write("4,null");
1761         }
1762     }
1763 
1764     private static PropertyInfo<CapturedViewProperty, ?>[] getCapturedViewProperties(
1765             Class<?> klass) {
1766         if (sCapturedViewProperties == null) {
1767             sCapturedViewProperties = new HashMap<>();
1768         }
1769         final HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]> map =
1770                 sCapturedViewProperties;
1771 
1772         PropertyInfo<CapturedViewProperty, ?>[] infos = map.get(klass);
1773         if (infos == null) {
1774             infos = convertToPropertyInfos(klass.getMethods(), klass.getFields(),
1775                     CapturedViewProperty.class);
1776             map.put(klass, infos);
1777         }
1778         return infos;
1779     }
1780 
1781     private static String exportCapturedViewProperties(Object obj, Class<?> klass, String prefix) {
1782         if (obj == null) {
1783             return "null";
1784         }
1785 
1786         StringBuilder sb = new StringBuilder();
1787 
1788         for (PropertyInfo<CapturedViewProperty, ?> pi : getCapturedViewProperties(klass)) {
1789             try {
1790                 Object methodValue = pi.invoke(obj);
1791 
1792                 if (pi.property.retrieveReturn()) {
1793                     //we are interested in the second level data only
1794                     sb.append(exportCapturedViewProperties(methodValue, pi.returnType,
1795                             pi.name + "#"));
1796                 } else {
1797                     sb.append(prefix).append(pi.name).append(pi.entrySuffix).append("=");
1798 
1799                     if (methodValue != null) {
1800                         final String value = methodValue.toString().replace("\n", "\\n");
1801                         sb.append(value);
1802                     } else {
1803                         sb.append("null");
1804                     }
1805                     sb.append(pi.valueSuffix).append(" ");
1806                 }
1807             } catch (Exception e) {
1808                 //It is OK here, we simply ignore this property
1809             }
1810         }
1811         return sb.toString();
1812     }
1813 
1814     /**
1815      * Dump view info for id based instrument test generation
1816      * (and possibly further data analysis). The results are dumped
1817      * to the log.
1818      * @param tag for log
1819      * @param view for dump
1820      */
1821     public static void dumpCapturedView(String tag, Object view) {
1822         Class<?> klass = view.getClass();
1823         StringBuilder sb = new StringBuilder(klass.getName() + ": ");
1824         sb.append(exportCapturedViewProperties(view, klass, ""));
1825         Log.d(tag, sb.toString());
1826     }
1827 
1828     /**
1829      * Invoke a particular method on given view.
1830      * The given method is always invoked on the UI thread. The caller thread will stall until the
1831      * method invocation is complete. Returns an object equal to the result of the method
1832      * invocation, null if the method is declared to return void
1833      * @throws Exception if the method invocation caused any exception
1834      * @hide
1835      */
1836     public static Object invokeViewMethod(final View view, final Method method,
1837             final Object[] args) {
1838         final CountDownLatch latch = new CountDownLatch(1);
1839         final AtomicReference<Object> result = new AtomicReference<Object>();
1840         final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
1841 
1842         view.post(new Runnable() {
1843             @Override
1844             public void run() {
1845                 try {
1846                     result.set(method.invoke(view, args));
1847                 } catch (InvocationTargetException e) {
1848                     exception.set(e.getCause());
1849                 } catch (Exception e) {
1850                     exception.set(e);
1851                 }
1852 
1853                 latch.countDown();
1854             }
1855         });
1856 
1857         try {
1858             latch.await();
1859         } catch (InterruptedException e) {
1860             throw new RuntimeException(e);
1861         }
1862 
1863         if (exception.get() != null) {
1864             throw new RuntimeException(exception.get());
1865         }
1866 
1867         return result.get();
1868     }
1869 
1870     /**
1871      * @hide
1872      */
1873     public static void setLayoutParameter(final View view, final String param, final int value)
1874             throws NoSuchFieldException, IllegalAccessException {
1875         final ViewGroup.LayoutParams p = view.getLayoutParams();
1876         final Field f = p.getClass().getField(param);
1877         if (f.getType() != int.class) {
1878             throw new RuntimeException("Only integer layout parameters can be set. Field "
1879                     + param + " is of type " + f.getType().getSimpleName());
1880         }
1881 
1882         f.set(p, Integer.valueOf(value));
1883 
1884         view.post(new Runnable() {
1885             @Override
1886             public void run() {
1887                 view.setLayoutParams(p);
1888             }
1889         });
1890     }
1891 
1892     /**
1893      * @hide
1894      */
1895     public static class SoftwareCanvasProvider implements CanvasProvider {
1896 
1897         private Canvas mCanvas;
1898         private Bitmap mBitmap;
1899         private boolean mEnabledHwFeaturesInSwMode;
1900 
1901         @Override
1902         public Canvas getCanvas(View view, int width, int height) {
1903             mBitmap = Bitmap.createBitmap(view.getResources().getDisplayMetrics(),
1904                     width, height, Bitmap.Config.ARGB_8888);
1905             if (mBitmap == null) {
1906                 throw new OutOfMemoryError();
1907             }
1908             mBitmap.setDensity(view.getResources().getDisplayMetrics().densityDpi);
1909 
1910             if (view.mAttachInfo != null) {
1911                 mCanvas = view.mAttachInfo.mCanvas;
1912             }
1913             if (mCanvas == null) {
1914                 mCanvas = new Canvas();
1915             }
1916             mEnabledHwFeaturesInSwMode = mCanvas.isHwFeaturesInSwModeEnabled();
1917             mCanvas.setBitmap(mBitmap);
1918             return mCanvas;
1919         }
1920 
1921         @Override
1922         public Bitmap createBitmap() {
1923             mCanvas.setBitmap(null);
1924             mCanvas.setHwFeaturesInSwModeEnabled(mEnabledHwFeaturesInSwMode);
1925             return mBitmap;
1926         }
1927     }
1928 
1929     /**
1930      * @hide
1931      */
1932     public static class HardwareCanvasProvider implements CanvasProvider {
1933         private Picture mPicture;
1934 
1935         @Override
1936         public Canvas getCanvas(View view, int width, int height) {
1937             mPicture = new Picture();
1938             return mPicture.beginRecording(width, height);
1939         }
1940 
1941         @Override
1942         public Bitmap createBitmap() {
1943             mPicture.endRecording();
1944             return Bitmap.createBitmap(mPicture);
1945         }
1946     }
1947 
1948     /**
1949      * @hide
1950      */
1951     public interface CanvasProvider {
1952 
1953         /**
1954          * Returns a canvas which can be used to draw {@param view}
1955          */
1956         Canvas getCanvas(View view, int width, int height);
1957 
1958         /**
1959          * Creates a bitmap from previously returned canvas
1960          * @return
1961          */
1962         Bitmap createBitmap();
1963     }
1964 }
1965