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