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