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