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