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