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