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