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