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