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                final long durationMeasure = profileViewOperation(view, new ViewOperation<Void>() {
920                    public Void[] pre() {
921                        forceLayout(view);
922                        return null;
923                    }
924
925                    private void forceLayout(View view) {
926                        view.forceLayout();
927                        if (view instanceof ViewGroup) {
928                            ViewGroup group = (ViewGroup) view;
929                            final int count = group.getChildCount();
930                            for (int i = 0; i < count; i++) {
931                                forceLayout(group.getChildAt(i));
932                            }
933                        }
934                    }
935
936                    public void run(Void... data) {
937                        view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
938                    }
939
940                    public void post(Void... data) {
941                    }
942                });
943
944                final long durationLayout = profileViewOperation(view, new ViewOperation<Void>() {
945                    public Void[] pre() {
946                        return null;
947                    }
948
949                    public void run(Void... data) {
950                        view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom);
951                    }
952
953                    public void post(Void... data) {
954                    }
955                });
956
957                final long durationDraw = profileViewOperation(view, new ViewOperation<Object>() {
958                    public Object[] pre() {
959                        final DisplayMetrics metrics = view.getResources().getDisplayMetrics();
960                        final Bitmap bitmap = Bitmap.createBitmap(metrics.widthPixels,
961                                metrics.heightPixels, Bitmap.Config.RGB_565);
962                        final Canvas canvas = new Canvas(bitmap);
963                        return new Object[] { bitmap, canvas };
964                    }
965
966                    public void run(Object... data) {
967                        view.draw((Canvas) data[1]);
968                    }
969
970                    public void post(Object... data) {
971                        ((Bitmap) data[0]).recycle();
972                    }
973                });
974
975                out.write(String.valueOf(durationMeasure));
976                out.write(' ');
977                out.write(String.valueOf(durationLayout));
978                out.write(' ');
979                out.write(String.valueOf(durationDraw));
980                out.newLine();
981            } else {
982                out.write("-1 -1 -1");
983                out.newLine();
984            }
985        } catch (Exception e) {
986            android.util.Log.w("View", "Problem profiling the view:", e);
987        } finally {
988            if (out != null) {
989                out.close();
990            }
991        }
992    }
993
994    interface ViewOperation<T> {
995        T[] pre();
996        void run(T... data);
997        void post(T... data);
998    }
999
1000    private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) {
1001        final CountDownLatch latch = new CountDownLatch(1);
1002        final long[] duration = new long[1];
1003
1004        view.post(new Runnable() {
1005            public void run() {
1006                try {
1007                    T[] data = operation.pre();
1008                    long start = Debug.threadCpuTimeNanos();
1009                    operation.run(data);
1010                    duration[0] = Debug.threadCpuTimeNanos() - start;
1011                    operation.post(data);
1012                } finally {
1013                    latch.countDown();
1014                }
1015            }
1016        });
1017
1018        try {
1019            latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
1020        } catch (InterruptedException e) {
1021            Log.w("View", "Could not complete the profiling of the view " + view);
1022            Thread.currentThread().interrupt();
1023            return -1;
1024        }
1025
1026        return duration[0];
1027    }
1028
1029    private static void captureLayers(View root, final DataOutputStream clientStream)
1030            throws IOException {
1031
1032        try {
1033            Rect outRect = new Rect();
1034            try {
1035                root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect);
1036            } catch (RemoteException e) {
1037                // Ignore
1038            }
1039
1040            clientStream.writeInt(outRect.width());
1041            clientStream.writeInt(outRect.height());
1042
1043            captureViewLayer(root, clientStream, true);
1044
1045            clientStream.write(2);
1046        } finally {
1047            clientStream.close();
1048        }
1049    }
1050
1051    private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
1052            throws IOException {
1053
1054        final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
1055
1056        if ((view.mPrivateFlags & View.SKIP_DRAW) != View.SKIP_DRAW) {
1057            final int id = view.getId();
1058            String name = view.getClass().getSimpleName();
1059            if (id != View.NO_ID) {
1060                name = resolveId(view.getContext(), id).toString();
1061            }
1062
1063            clientStream.write(1);
1064            clientStream.writeUTF(name);
1065            clientStream.writeByte(localVisible ? 1 : 0);
1066
1067            int[] position = new int[2];
1068            // XXX: Should happen on the UI thread
1069            view.getLocationInWindow(position);
1070
1071            clientStream.writeInt(position[0]);
1072            clientStream.writeInt(position[1]);
1073            clientStream.flush();
1074
1075            Bitmap b = performViewCapture(view, true);
1076            if (b != null) {
1077                ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
1078                        b.getHeight() * 2);
1079                b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
1080                clientStream.writeInt(arrayOut.size());
1081                arrayOut.writeTo(clientStream);
1082            }
1083            clientStream.flush();
1084        }
1085
1086        if (view instanceof ViewGroup) {
1087            ViewGroup group = (ViewGroup) view;
1088            int count = group.getChildCount();
1089
1090            for (int i = 0; i < count; i++) {
1091                captureViewLayer(group.getChildAt(i), clientStream, localVisible);
1092            }
1093        }
1094    }
1095
1096    private static void capture(View root, final OutputStream clientStream, String parameter)
1097            throws IOException {
1098
1099        final View captureView = findView(root, parameter);
1100        Bitmap b = performViewCapture(captureView, false);
1101
1102        if (b != null) {
1103            BufferedOutputStream out = null;
1104            try {
1105                out = new BufferedOutputStream(clientStream, 32 * 1024);
1106                b.compress(Bitmap.CompressFormat.PNG, 100, out);
1107                out.flush();
1108            } finally {
1109                if (out != null) {
1110                    out.close();
1111                }
1112                b.recycle();
1113            }
1114        } else {
1115            Log.w("View", "Failed to create capture bitmap!");
1116            clientStream.close();
1117        }
1118    }
1119
1120    private static Bitmap performViewCapture(final View captureView, final boolean skpiChildren) {
1121        if (captureView != null) {
1122            final CountDownLatch latch = new CountDownLatch(1);
1123            final Bitmap[] cache = new Bitmap[1];
1124
1125            captureView.post(new Runnable() {
1126                public void run() {
1127                    try {
1128                        cache[0] = captureView.createSnapshot(
1129                                Bitmap.Config.ARGB_8888, 0, skpiChildren);
1130                    } catch (OutOfMemoryError e) {
1131                        try {
1132                            cache[0] = captureView.createSnapshot(
1133                                    Bitmap.Config.ARGB_4444, 0, skpiChildren);
1134                        } catch (OutOfMemoryError e2) {
1135                            Log.w("View", "Out of memory for bitmap");
1136                        }
1137                    } finally {
1138                        latch.countDown();
1139                    }
1140                }
1141            });
1142
1143            try {
1144                latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
1145                return cache[0];
1146            } catch (InterruptedException e) {
1147                Log.w("View", "Could not complete the capture of the view " + captureView);
1148                Thread.currentThread().interrupt();
1149            }
1150        }
1151
1152        return null;
1153    }
1154
1155    private static void dump(View root, OutputStream clientStream) throws IOException {
1156        BufferedWriter out = null;
1157        try {
1158            out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
1159            View view = root.getRootView();
1160            if (view instanceof ViewGroup) {
1161                ViewGroup group = (ViewGroup) view;
1162                dumpViewHierarchyWithProperties(group.getContext(), group, out, 0);
1163            }
1164            out.write("DONE.");
1165            out.newLine();
1166        } catch (Exception e) {
1167            android.util.Log.w("View", "Problem dumping the view:", e);
1168        } finally {
1169            if (out != null) {
1170                out.close();
1171            }
1172        }
1173    }
1174
1175    private static View findView(ViewGroup group, String className, int hashCode) {
1176        if (isRequestedView(group, className, hashCode)) {
1177            return group;
1178        }
1179
1180        final int count = group.getChildCount();
1181        for (int i = 0; i < count; i++) {
1182            final View view = group.getChildAt(i);
1183            if (view instanceof ViewGroup) {
1184                final View found = findView((ViewGroup) view, className, hashCode);
1185                if (found != null) {
1186                    return found;
1187                }
1188            } else if (isRequestedView(view, className, hashCode)) {
1189                return view;
1190            }
1191        }
1192
1193        return null;
1194    }
1195
1196    private static boolean isRequestedView(View view, String className, int hashCode) {
1197        return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
1198    }
1199
1200    private static void dumpViewHierarchyWithProperties(Context context, ViewGroup group,
1201            BufferedWriter out, int level) {
1202        if (!dumpViewWithProperties(context, group, out, level)) {
1203            return;
1204        }
1205
1206        final int count = group.getChildCount();
1207        for (int i = 0; i < count; i++) {
1208            final View view = group.getChildAt(i);
1209            if (view instanceof ViewGroup) {
1210                dumpViewHierarchyWithProperties(context, (ViewGroup) view, out, level + 1);
1211            } else {
1212                dumpViewWithProperties(context, view, out, level + 1);
1213            }
1214        }
1215    }
1216
1217    private static boolean dumpViewWithProperties(Context context, View view,
1218            BufferedWriter out, int level) {
1219
1220        try {
1221            for (int i = 0; i < level; i++) {
1222                out.write(' ');
1223            }
1224            out.write(view.getClass().getName());
1225            out.write('@');
1226            out.write(Integer.toHexString(view.hashCode()));
1227            out.write(' ');
1228            dumpViewProperties(context, view, out);
1229            out.newLine();
1230        } catch (IOException e) {
1231            Log.w("View", "Error while dumping hierarchy tree");
1232            return false;
1233        }
1234        return true;
1235    }
1236
1237    private static Field[] getExportedPropertyFields(Class<?> klass) {
1238        if (sFieldsForClasses == null) {
1239            sFieldsForClasses = new HashMap<Class<?>, Field[]>();
1240        }
1241        if (sAnnotations == null) {
1242            sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
1243        }
1244
1245        final HashMap<Class<?>, Field[]> map = sFieldsForClasses;
1246        final HashMap<AccessibleObject, ExportedProperty> annotations = sAnnotations;
1247
1248        Field[] fields = map.get(klass);
1249        if (fields != null) {
1250            return fields;
1251        }
1252
1253        final ArrayList<Field> foundFields = new ArrayList<Field>();
1254        fields = klass.getDeclaredFields();
1255
1256        int count = fields.length;
1257        for (int i = 0; i < count; i++) {
1258            final Field field = fields[i];
1259            if (field.isAnnotationPresent(ExportedProperty.class)) {
1260                field.setAccessible(true);
1261                foundFields.add(field);
1262                annotations.put(field, field.getAnnotation(ExportedProperty.class));
1263            }
1264        }
1265
1266        fields = foundFields.toArray(new Field[foundFields.size()]);
1267        map.put(klass, fields);
1268
1269        return fields;
1270    }
1271
1272    private static Method[] getExportedPropertyMethods(Class<?> klass) {
1273        if (sMethodsForClasses == null) {
1274            sMethodsForClasses = new HashMap<Class<?>, Method[]>(100);
1275        }
1276        if (sAnnotations == null) {
1277            sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
1278        }
1279
1280        final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
1281        final HashMap<AccessibleObject, ExportedProperty> annotations = sAnnotations;
1282
1283        Method[] methods = map.get(klass);
1284        if (methods != null) {
1285            return methods;
1286        }
1287
1288        final ArrayList<Method> foundMethods = new ArrayList<Method>();
1289        methods = klass.getDeclaredMethods();
1290
1291        int count = methods.length;
1292        for (int i = 0; i < count; i++) {
1293            final Method method = methods[i];
1294            if (method.getParameterTypes().length == 0 &&
1295                    method.isAnnotationPresent(ExportedProperty.class) &&
1296                    method.getReturnType() != Void.class) {
1297                method.setAccessible(true);
1298                foundMethods.add(method);
1299                annotations.put(method, method.getAnnotation(ExportedProperty.class));
1300            }
1301        }
1302
1303        methods = foundMethods.toArray(new Method[foundMethods.size()]);
1304        map.put(klass, methods);
1305
1306        return methods;
1307    }
1308
1309    private static void dumpViewProperties(Context context, Object view,
1310            BufferedWriter out) throws IOException {
1311
1312        dumpViewProperties(context, view, out, "");
1313    }
1314
1315    private static void dumpViewProperties(Context context, Object view,
1316            BufferedWriter out, String prefix) throws IOException {
1317
1318        Class<?> klass = view.getClass();
1319
1320        do {
1321            exportFields(context, view, out, klass, prefix);
1322            exportMethods(context, view, out, klass, prefix);
1323            klass = klass.getSuperclass();
1324        } while (klass != Object.class);
1325    }
1326
1327    private static void exportMethods(Context context, Object view, BufferedWriter out,
1328            Class<?> klass, String prefix) throws IOException {
1329
1330        final Method[] methods = getExportedPropertyMethods(klass);
1331
1332        int count = methods.length;
1333        for (int i = 0; i < count; i++) {
1334            final Method method = methods[i];
1335            //noinspection EmptyCatchBlock
1336            try {
1337                // TODO: This should happen on the UI thread
1338                Object methodValue = method.invoke(view, (Object[]) null);
1339                final Class<?> returnType = method.getReturnType();
1340
1341                if (returnType == int.class) {
1342                    final ExportedProperty property = sAnnotations.get(method);
1343                    if (property.resolveId() && context != null) {
1344                        final int id = (Integer) methodValue;
1345                        methodValue = resolveId(context, id);
1346                    } else {
1347                        final FlagToString[] flagsMapping = property.flagMapping();
1348                        if (flagsMapping.length > 0) {
1349                            final int intValue = (Integer) methodValue;
1350                            final String valuePrefix = prefix + method.getName() + '_';
1351                            exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1352                        }
1353
1354                        final IntToString[] mapping = property.mapping();
1355                        if (mapping.length > 0) {
1356                            final int intValue = (Integer) methodValue;
1357                            boolean mapped = false;
1358                            int mappingCount = mapping.length;
1359                            for (int j = 0; j < mappingCount; j++) {
1360                                final IntToString mapper = mapping[j];
1361                                if (mapper.from() == intValue) {
1362                                    methodValue = mapper.to();
1363                                    mapped = true;
1364                                    break;
1365                                }
1366                            }
1367
1368                            if (!mapped) {
1369                                methodValue = intValue;
1370                            }
1371                        }
1372                    }
1373                } else if (returnType == int[].class) {
1374                    final ExportedProperty property = sAnnotations.get(method);
1375                    final int[] array = (int[]) methodValue;
1376                    final String valuePrefix = prefix + method.getName() + '_';
1377                    final String suffix = "()";
1378
1379                    exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1380                } else if (!returnType.isPrimitive()) {
1381                    final ExportedProperty property = sAnnotations.get(method);
1382                    if (property.deepExport()) {
1383                        dumpViewProperties(context, methodValue, out, prefix + property.prefix());
1384                        continue;
1385                    }
1386                }
1387
1388                writeEntry(out, prefix, method.getName(), "()", methodValue);
1389            } catch (IllegalAccessException e) {
1390            } catch (InvocationTargetException e) {
1391            }
1392        }
1393    }
1394
1395    private static void exportFields(Context context, Object view, BufferedWriter out,
1396            Class<?> klass, String prefix) throws IOException {
1397
1398        final Field[] fields = getExportedPropertyFields(klass);
1399
1400        int count = fields.length;
1401        for (int i = 0; i < count; i++) {
1402            final Field field = fields[i];
1403
1404            //noinspection EmptyCatchBlock
1405            try {
1406                Object fieldValue = null;
1407                final Class<?> type = field.getType();
1408
1409                if (type == int.class) {
1410                    final ExportedProperty property = sAnnotations.get(field);
1411                    if (property.resolveId() && context != null) {
1412                        final int id = field.getInt(view);
1413                        fieldValue = resolveId(context, id);
1414                    } else {
1415                        final FlagToString[] flagsMapping = property.flagMapping();
1416                        if (flagsMapping.length > 0) {
1417                            final int intValue = field.getInt(view);
1418                            final String valuePrefix = prefix + field.getName() + '_';
1419                            exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1420                        }
1421
1422                        final IntToString[] mapping = property.mapping();
1423                        if (mapping.length > 0) {
1424                            final int intValue = field.getInt(view);
1425                            int mappingCount = mapping.length;
1426                            for (int j = 0; j < mappingCount; j++) {
1427                                final IntToString mapped = mapping[j];
1428                                if (mapped.from() == intValue) {
1429                                    fieldValue = mapped.to();
1430                                    break;
1431                                }
1432                            }
1433
1434                            if (fieldValue == null) {
1435                                fieldValue = intValue;
1436                            }
1437                        }
1438                    }
1439                } else if (type == int[].class) {
1440                    final ExportedProperty property = sAnnotations.get(field);
1441                    final int[] array = (int[]) field.get(view);
1442                    final String valuePrefix = prefix + field.getName() + '_';
1443                    final String suffix = "";
1444
1445                    exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1446
1447                    // We exit here!
1448                    return;
1449                } else if (!type.isPrimitive()) {
1450                    final ExportedProperty property = sAnnotations.get(field);
1451                    if (property.deepExport()) {
1452                        dumpViewProperties(context, field.get(view), out,
1453                                prefix + property.prefix());
1454                        continue;
1455                    }
1456                }
1457
1458                if (fieldValue == null) {
1459                    fieldValue = field.get(view);
1460                }
1461
1462                writeEntry(out, prefix, field.getName(), "", fieldValue);
1463            } catch (IllegalAccessException e) {
1464            }
1465        }
1466    }
1467
1468    private static void writeEntry(BufferedWriter out, String prefix, String name,
1469            String suffix, Object value) throws IOException {
1470
1471        out.write(prefix);
1472        out.write(name);
1473        out.write(suffix);
1474        out.write("=");
1475        writeValue(out, value);
1476        out.write(' ');
1477    }
1478
1479    private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
1480            int intValue, String prefix) throws IOException {
1481
1482        final int count = mapping.length;
1483        for (int j = 0; j < count; j++) {
1484            final FlagToString flagMapping = mapping[j];
1485            final boolean ifTrue = flagMapping.outputIf();
1486            final int maskResult = intValue & flagMapping.mask();
1487            final boolean test = maskResult == flagMapping.equals();
1488            if ((test && ifTrue) || (!test && !ifTrue)) {
1489                final String name = flagMapping.name();
1490                final String value = "0x" + Integer.toHexString(maskResult);
1491                writeEntry(out, prefix, name, "", value);
1492            }
1493        }
1494    }
1495
1496    private static void exportUnrolledArray(Context context, BufferedWriter out,
1497            ExportedProperty property, int[] array, String prefix, String suffix)
1498            throws IOException {
1499
1500        final IntToString[] indexMapping = property.indexMapping();
1501        final boolean hasIndexMapping = indexMapping.length > 0;
1502
1503        final IntToString[] mapping = property.mapping();
1504        final boolean hasMapping = mapping.length > 0;
1505
1506        final boolean resolveId = property.resolveId() && context != null;
1507        final int valuesCount = array.length;
1508
1509        for (int j = 0; j < valuesCount; j++) {
1510            String name;
1511            String value = null;
1512
1513            final int intValue = array[j];
1514
1515            name = String.valueOf(j);
1516            if (hasIndexMapping) {
1517                int mappingCount = indexMapping.length;
1518                for (int k = 0; k < mappingCount; k++) {
1519                    final IntToString mapped = indexMapping[k];
1520                    if (mapped.from() == j) {
1521                        name = mapped.to();
1522                        break;
1523                    }
1524                }
1525            }
1526
1527            if (hasMapping) {
1528                int mappingCount = mapping.length;
1529                for (int k = 0; k < mappingCount; k++) {
1530                    final IntToString mapped = mapping[k];
1531                    if (mapped.from() == intValue) {
1532                        value = mapped.to();
1533                        break;
1534                    }
1535                }
1536            }
1537
1538            if (resolveId) {
1539                if (value == null) value = (String) resolveId(context, intValue);
1540            } else {
1541                value = String.valueOf(intValue);
1542            }
1543
1544            writeEntry(out, prefix, name, suffix, value);
1545        }
1546    }
1547
1548    static Object resolveId(Context context, int id) {
1549        Object fieldValue;
1550        final Resources resources = context.getResources();
1551        if (id >= 0) {
1552            try {
1553                fieldValue = resources.getResourceTypeName(id) + '/' +
1554                        resources.getResourceEntryName(id);
1555            } catch (Resources.NotFoundException e) {
1556                fieldValue = "id/0x" + Integer.toHexString(id);
1557            }
1558        } else {
1559            fieldValue = "NO_ID";
1560        }
1561        return fieldValue;
1562    }
1563
1564    private static void writeValue(BufferedWriter out, Object value) throws IOException {
1565        if (value != null) {
1566            String output = value.toString().replace("\n", "\\n");
1567            out.write(String.valueOf(output.length()));
1568            out.write(",");
1569            out.write(output);
1570        } else {
1571            out.write("4,null");
1572        }
1573    }
1574
1575    private static void dumpViewHierarchy(ViewGroup group, BufferedWriter out, int level) {
1576        if (!dumpView(group, out, level)) {
1577            return;
1578        }
1579
1580        final int count = group.getChildCount();
1581        for (int i = 0; i < count; i++) {
1582            final View view = group.getChildAt(i);
1583            if (view instanceof ViewGroup) {
1584                dumpViewHierarchy((ViewGroup) view, out, level + 1);
1585            } else {
1586                dumpView(view, out, level + 1);
1587            }
1588        }
1589    }
1590
1591    private static boolean dumpView(Object view, BufferedWriter out, int level) {
1592        try {
1593            for (int i = 0; i < level; i++) {
1594                out.write(' ');
1595            }
1596            out.write(view.getClass().getName());
1597            out.write('@');
1598            out.write(Integer.toHexString(view.hashCode()));
1599            out.newLine();
1600        } catch (IOException e) {
1601            Log.w("View", "Error while dumping hierarchy tree");
1602            return false;
1603        }
1604        return true;
1605    }
1606
1607    private static Field[] capturedViewGetPropertyFields(Class<?> klass) {
1608        if (mCapturedViewFieldsForClasses == null) {
1609            mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>();
1610        }
1611        final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses;
1612
1613        Field[] fields = map.get(klass);
1614        if (fields != null) {
1615            return fields;
1616        }
1617
1618        final ArrayList<Field> foundFields = new ArrayList<Field>();
1619        fields = klass.getFields();
1620
1621        int count = fields.length;
1622        for (int i = 0; i < count; i++) {
1623            final Field field = fields[i];
1624            if (field.isAnnotationPresent(CapturedViewProperty.class)) {
1625                field.setAccessible(true);
1626                foundFields.add(field);
1627            }
1628        }
1629
1630        fields = foundFields.toArray(new Field[foundFields.size()]);
1631        map.put(klass, fields);
1632
1633        return fields;
1634    }
1635
1636    private static Method[] capturedViewGetPropertyMethods(Class<?> klass) {
1637        if (mCapturedViewMethodsForClasses == null) {
1638            mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>();
1639        }
1640        final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses;
1641
1642        Method[] methods = map.get(klass);
1643        if (methods != null) {
1644            return methods;
1645        }
1646
1647        final ArrayList<Method> foundMethods = new ArrayList<Method>();
1648        methods = klass.getMethods();
1649
1650        int count = methods.length;
1651        for (int i = 0; i < count; i++) {
1652            final Method method = methods[i];
1653            if (method.getParameterTypes().length == 0 &&
1654                    method.isAnnotationPresent(CapturedViewProperty.class) &&
1655                    method.getReturnType() != Void.class) {
1656                method.setAccessible(true);
1657                foundMethods.add(method);
1658            }
1659        }
1660
1661        methods = foundMethods.toArray(new Method[foundMethods.size()]);
1662        map.put(klass, methods);
1663
1664        return methods;
1665    }
1666
1667    private static String capturedViewExportMethods(Object obj, Class<?> klass,
1668            String prefix) {
1669
1670        if (obj == null) {
1671            return "null";
1672        }
1673
1674        StringBuilder sb = new StringBuilder();
1675        final Method[] methods = capturedViewGetPropertyMethods(klass);
1676
1677        int count = methods.length;
1678        for (int i = 0; i < count; i++) {
1679            final Method method = methods[i];
1680            try {
1681                Object methodValue = method.invoke(obj, (Object[]) null);
1682                final Class<?> returnType = method.getReturnType();
1683
1684                CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class);
1685                if (property.retrieveReturn()) {
1686                    //we are interested in the second level data only
1687                    sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#"));
1688                } else {
1689                    sb.append(prefix);
1690                    sb.append(method.getName());
1691                    sb.append("()=");
1692
1693                    if (methodValue != null) {
1694                        final String value = methodValue.toString().replace("\n", "\\n");
1695                        sb.append(value);
1696                    } else {
1697                        sb.append("null");
1698                    }
1699                    sb.append("; ");
1700                }
1701              } catch (IllegalAccessException e) {
1702                  //Exception IllegalAccess, it is OK here
1703                  //we simply ignore this method
1704              } catch (InvocationTargetException e) {
1705                  //Exception InvocationTarget, it is OK here
1706                  //we simply ignore this method
1707              }
1708        }
1709        return sb.toString();
1710    }
1711
1712    private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) {
1713
1714        if (obj == null) {
1715            return "null";
1716        }
1717
1718        StringBuilder sb = new StringBuilder();
1719        final Field[] fields = capturedViewGetPropertyFields(klass);
1720
1721        int count = fields.length;
1722        for (int i = 0; i < count; i++) {
1723            final Field field = fields[i];
1724            try {
1725                Object fieldValue = field.get(obj);
1726
1727                sb.append(prefix);
1728                sb.append(field.getName());
1729                sb.append("=");
1730
1731                if (fieldValue != null) {
1732                    final String value = fieldValue.toString().replace("\n", "\\n");
1733                    sb.append(value);
1734                } else {
1735                    sb.append("null");
1736                }
1737                sb.append(' ');
1738            } catch (IllegalAccessException e) {
1739                //Exception IllegalAccess, it is OK here
1740                //we simply ignore this field
1741            }
1742        }
1743        return sb.toString();
1744    }
1745
1746    /**
1747     * Dump view info for id based instrument test generation
1748     * (and possibly further data analysis). The results are dumped
1749     * to the log.
1750     * @param tag for log
1751     * @param view for dump
1752     */
1753    public static void dumpCapturedView(String tag, Object view) {
1754        Class<?> klass = view.getClass();
1755        StringBuilder sb = new StringBuilder(klass.getName() + ": ");
1756        sb.append(capturedViewExportFields(view, klass, ""));
1757        sb.append(capturedViewExportMethods(view, klass, ""));
1758        Log.d(tag, sb.toString());
1759    }
1760}
1761