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