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