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