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