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