ViewDebug.java revision 236092a36216c79507ec19eb207831810caced19
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view;
18
19import android.util.Config;
20import android.util.Log;
21import android.util.DisplayMetrics;
22import android.content.res.Resources;
23import android.content.Context;
24import android.graphics.Bitmap;
25import android.graphics.Canvas;
26import android.os.Environment;
27import android.os.Debug;
28
29import java.io.File;
30import java.io.BufferedWriter;
31import java.io.FileWriter;
32import java.io.IOException;
33import java.io.FileOutputStream;
34import java.io.DataOutputStream;
35import java.io.OutputStreamWriter;
36import java.io.BufferedOutputStream;
37import java.io.OutputStream;
38import java.util.List;
39import java.util.LinkedList;
40import java.util.ArrayList;
41import java.util.HashMap;
42import java.util.concurrent.CountDownLatch;
43import java.util.concurrent.TimeUnit;
44import java.lang.annotation.Target;
45import java.lang.annotation.ElementType;
46import java.lang.annotation.Retention;
47import java.lang.annotation.RetentionPolicy;
48import java.lang.reflect.Field;
49import java.lang.reflect.Method;
50import java.lang.reflect.InvocationTargetException;
51import java.lang.reflect.AccessibleObject;
52
53/**
54 * Various debugging/tracing tools related to {@link View} and the view hierarchy.
55 */
56public class ViewDebug {
57    /**
58     * Log tag used to log errors related to the consistency of the view hierarchy.
59     *
60     * @hide
61     */
62    public static final String CONSISTENCY_LOG_TAG = "ViewConsistency";
63
64    /**
65     * Flag indicating the consistency check should check layout-related properties.
66     *
67     * @hide
68     */
69    public static final int CONSISTENCY_LAYOUT = 0x1;
70
71    /**
72     * Flag indicating the consistency check should check drawing-related properties.
73     *
74     * @hide
75     */
76    public static final int CONSISTENCY_DRAWING = 0x2;
77
78    /**
79     * Enables or disables view hierarchy tracing. Any invoker of
80     * {@link #trace(View, android.view.ViewDebug.HierarchyTraceType)} should first
81     * check that this value is set to true as not to affect performance.
82     */
83    public static final boolean TRACE_HIERARCHY = false;
84
85    /**
86     * Enables or disables view recycler tracing. Any invoker of
87     * {@link #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])} should first
88     * check that this value is set to true as not to affect performance.
89     */
90    public static final boolean TRACE_RECYCLER = false;
91
92    /**
93     * The system property of dynamic switch for capturing view information
94     * when it is set, we dump interested fields and methods for the view on focus
95     */
96    static final String SYSTEM_PROPERTY_CAPTURE_VIEW = "debug.captureview";
97
98    /**
99     * The system property of dynamic switch for capturing event information
100     * when it is set, we log key events, touch/motion and trackball events
101     */
102    static final String SYSTEM_PROPERTY_CAPTURE_EVENT = "debug.captureevent";
103
104    /**
105     * Profiles drawing times in the events log.
106     *
107     * @hide
108     */
109    @Debug.DebugProperty
110    public static boolean profileDrawing = false;
111
112    /**
113     * Profiles layout times in the events log.
114     *
115     * @hide
116     */
117    @Debug.DebugProperty
118    public static boolean profileLayout = false;
119
120    /**
121     * Profiles real fps (times between draws) and displays the result.
122     *
123     * @hide
124     */
125    @Debug.DebugProperty
126    public static boolean showFps = false;
127
128    /**
129     * <p>Enables or disables views consistency check. Even when this property is enabled,
130     * view consistency checks happen only if {@link android.util.Config#DEBUG} is set
131     * to true. The value of this property can be configured externally in one of the
132     * following files:</p>
133     * <ul>
134     *  <li>/system/debug.prop</li>
135     *  <li>/debug.prop</li>
136     *  <li>/data/debug.prop</li>
137     * </ul>
138     * @hide
139     */
140    @Debug.DebugProperty
141    public static boolean consistencyCheckEnabled = false;
142
143    static {
144        if (Config.DEBUG) {
145	        Debug.setFieldsOn(ViewDebug.class, true);
146	    }
147    }
148
149    /**
150     * This annotation can be used to mark fields and methods to be dumped by
151     * the view server. Only non-void methods with no arguments can be annotated
152     * by this annotation.
153     */
154    @Target({ ElementType.FIELD, ElementType.METHOD })
155    @Retention(RetentionPolicy.RUNTIME)
156    public @interface ExportedProperty {
157        /**
158         * When resolveId is true, and if the annotated field/method return value
159         * is an int, the value is converted to an Android's resource name.
160         *
161         * @return true if the property's value must be transformed into an Android
162         *         resource name, false otherwise
163         */
164        boolean resolveId() default false;
165
166        /**
167         * A mapping can be defined to map int values to specific strings. For
168         * instance, View.getVisibility() returns 0, 4 or 8. However, these values
169         * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
170         * these human readable values:
171         *
172         * <pre>
173         * @ViewDebug.ExportedProperty(mapping = {
174         *     @ViewDebug.IntToString(from = 0, to = "VISIBLE"),
175         *     @ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
176         *     @ViewDebug.IntToString(from = 8, to = "GONE")
177         * })
178         * public int getVisibility() { ...
179         * <pre>
180         *
181         * @return An array of int to String mappings
182         *
183         * @see android.view.ViewDebug.IntToString
184         */
185        IntToString[] mapping() default { };
186
187        /**
188         * A mapping can be defined to map array indices to specific strings.
189         * A mapping can be used to see human readable values for the indices
190         * of an array:
191         *
192         * <pre>
193         * @ViewDebug.ExportedProperty(indexMapping = {
194         *     @ViewDebug.IntToString(from = 0, to = "INVALID"),
195         *     @ViewDebug.IntToString(from = 1, to = "FIRST"),
196         *     @ViewDebug.IntToString(from = 2, to = "SECOND")
197         * })
198         * private int[] mElements;
199         * <pre>
200         *
201         * @return An array of int to String mappings
202         *
203         * @see android.view.ViewDebug.IntToString
204         * @see #mapping()
205         */
206        IntToString[] indexMapping() default { };
207
208        /**
209         * A flags mapping can be defined to map flags encoded in an integer to
210         * specific strings. A mapping can be used to see human readable values
211         * for the flags of an integer:
212         *
213         * <pre>
214         * @ViewDebug.ExportedProperty(flagMapping = {
215         *     @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED, name = "ENABLED"),
216         *     @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED, name = "DISABLED"),
217         * })
218         * private int mFlags;
219         * <pre>
220         *
221         * A specified String is output when the following is true:
222         *
223         * @return An array of int to String mappings
224         */
225        FlagToString[] flagMapping() default { };
226
227        /**
228         * When deep export is turned on, this property is not dumped. Instead, the
229         * properties contained in this property are dumped. Each child property
230         * is prefixed with the name of this property.
231         *
232         * @return true if the properties of this property should be dumped
233         *
234         * @see #prefix()
235         */
236        boolean deepExport() default false;
237
238        /**
239         * The prefix to use on child properties when deep export is enabled
240         *
241         * @return a prefix as a String
242         *
243         * @see #deepExport()
244         */
245        String prefix() default "";
246    }
247
248    /**
249     * Defines a mapping from an int value to a String. Such a mapping can be used
250     * in a @ExportedProperty to provide more meaningful values to the end user.
251     *
252     * @see android.view.ViewDebug.ExportedProperty
253     */
254    @Target({ ElementType.TYPE })
255    @Retention(RetentionPolicy.RUNTIME)
256    public @interface IntToString {
257        /**
258         * The original int value to map to a String.
259         *
260         * @return An arbitrary int value.
261         */
262        int from();
263
264        /**
265         * The String to use in place of the original int value.
266         *
267         * @return An arbitrary non-null String.
268         */
269        String to();
270    }
271
272    /**
273     * Defines a mapping from an flag to a String. Such a mapping can be used
274     * in a @ExportedProperty to provide more meaningful values to the end user.
275     *
276     * @see android.view.ViewDebug.ExportedProperty
277     */
278    @Target({ ElementType.TYPE })
279    @Retention(RetentionPolicy.RUNTIME)
280    public @interface FlagToString {
281        /**
282         * The mask to apply to the original value.
283         *
284         * @return An arbitrary int value.
285         */
286        int mask();
287
288        /**
289         * The value to compare to the result of:
290         * <code>original value &amp; {@link #mask()}</code>.
291         *
292         * @return An arbitrary value.
293         */
294        int equals();
295
296        /**
297         * The String to use in place of the original int value.
298         *
299         * @return An arbitrary non-null String.
300         */
301        String name();
302
303        /**
304         * Indicates whether to output the flag when the test is true,
305         * or false. Defaults to true.
306         */
307        boolean outputIf() default true;
308    }
309
310    /**
311     * This annotation can be used to mark fields and methods to be dumped when
312     * the view is captured. Methods with this annotation must have no arguments
313     * and must return a valid type of data.
314     */
315    @Target({ ElementType.FIELD, ElementType.METHOD })
316    @Retention(RetentionPolicy.RUNTIME)
317    public @interface CapturedViewProperty {
318        /**
319         * When retrieveReturn is true, we need to retrieve second level methods
320         * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
321         * we will set retrieveReturn = true on the annotation of
322         * myView.getFirstLevelMethod()
323         * @return true if we need the second level methods
324         */
325        boolean retrieveReturn() default false;
326    }
327
328    private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null;
329    private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null;
330
331    // Maximum delay in ms after which we stop trying to capture a View's drawing
332    private static final int CAPTURE_TIMEOUT = 4000;
333
334    private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
335    private static final String REMOTE_COMMAND_DUMP = "DUMP";
336    private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
337    private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
338    private static final String REMOTE_PROFILE = "PROFILE";
339
340    private static HashMap<Class<?>, Field[]> sFieldsForClasses;
341    private static HashMap<Class<?>, Method[]> sMethodsForClasses;
342    private static HashMap<AccessibleObject, ExportedProperty> sAnnotations;
343
344    /**
345     * Defines the type of hierarhcy trace to output to the hierarchy traces file.
346     */
347    public enum HierarchyTraceType {
348        INVALIDATE,
349        INVALIDATE_CHILD,
350        INVALIDATE_CHILD_IN_PARENT,
351        REQUEST_LAYOUT,
352        ON_LAYOUT,
353        ON_MEASURE,
354        DRAW,
355        BUILD_CACHE
356    }
357
358    private static BufferedWriter sHierarchyTraces;
359    private static ViewRoot sHierarhcyRoot;
360    private static String sHierarchyTracePrefix;
361
362    /**
363     * Defines the type of recycler trace to output to the recycler traces file.
364     */
365    public enum RecyclerTraceType {
366        NEW_VIEW,
367        BIND_VIEW,
368        RECYCLE_FROM_ACTIVE_HEAP,
369        RECYCLE_FROM_SCRAP_HEAP,
370        MOVE_TO_ACTIVE_HEAP,
371        MOVE_TO_SCRAP_HEAP,
372        MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
373    }
374
375    private static class RecyclerTrace {
376        public int view;
377        public RecyclerTraceType type;
378        public int position;
379        public int indexOnScreen;
380    }
381
382    private static View sRecyclerOwnerView;
383    private static List<View> sRecyclerViews;
384    private static List<RecyclerTrace> sRecyclerTraces;
385    private static String sRecyclerTracePrefix;
386
387    /**
388     * Returns the number of instanciated Views.
389     *
390     * @return The number of Views instanciated in the current process.
391     *
392     * @hide
393     */
394    public static long getViewInstanceCount() {
395        return View.sInstanceCount;
396    }
397
398    /**
399     * Returns the number of instanciated ViewRoots.
400     *
401     * @return The number of ViewRoots instanciated in the current process.
402     *
403     * @hide
404     */
405    public static long getViewRootInstanceCount() {
406        return ViewRoot.getInstanceCount();
407    }
408
409    /**
410     * Outputs a trace to the currently opened recycler traces. The trace records the type of
411     * recycler action performed on the supplied view as well as a number of parameters.
412     *
413     * @param view the view to trace
414     * @param type the type of the trace
415     * @param parameters parameters depending on the type of the trace
416     */
417    public static void trace(View view, RecyclerTraceType type, int... parameters) {
418        if (sRecyclerOwnerView == null || sRecyclerViews == null) {
419            return;
420        }
421
422        if (!sRecyclerViews.contains(view)) {
423            sRecyclerViews.add(view);
424        }
425
426        final int index = sRecyclerViews.indexOf(view);
427
428        RecyclerTrace trace = new RecyclerTrace();
429        trace.view = index;
430        trace.type = type;
431        trace.position = parameters[0];
432        trace.indexOnScreen = parameters[1];
433
434        sRecyclerTraces.add(trace);
435    }
436
437    /**
438     * Starts tracing the view recycler of the specified view. The trace is identified by a prefix,
439     * used to build the traces files names: <code>/EXTERNAL/view-recycler/PREFIX.traces</code> and
440     * <code>/EXTERNAL/view-recycler/PREFIX.recycler</code>.
441     *
442     * Only one view recycler can be traced at the same time. After calling this method, any
443     * other invocation will result in a <code>IllegalStateException</code> unless
444     * {@link #stopRecyclerTracing()} is invoked before.
445     *
446     * Traces files are created only after {@link #stopRecyclerTracing()} is invoked.
447     *
448     * This method will return immediately if TRACE_RECYCLER is false.
449     *
450     * @param prefix the traces files name prefix
451     * @param view the view whose recycler must be traced
452     *
453     * @see #stopRecyclerTracing()
454     * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
455     */
456    public static void startRecyclerTracing(String prefix, View view) {
457        //noinspection PointlessBooleanExpression,ConstantConditions
458        if (!TRACE_RECYCLER) {
459            return;
460        }
461
462        if (sRecyclerOwnerView != null) {
463            throw new IllegalStateException("You must call stopRecyclerTracing() before running" +
464                " a new trace!");
465        }
466
467        sRecyclerTracePrefix = prefix;
468        sRecyclerOwnerView = view;
469        sRecyclerViews = new ArrayList<View>();
470        sRecyclerTraces = new LinkedList<RecyclerTrace>();
471    }
472
473    /**
474     * Stops the current view recycer tracing.
475     *
476     * Calling this method creates the file <code>/EXTERNAL/view-recycler/PREFIX.traces</code>
477     * containing all the traces (or method calls) relative to the specified view's recycler.
478     *
479     * Calling this method creates the file <code>/EXTERNAL/view-recycler/PREFIX.recycler</code>
480     * containing all of the views used by the recycler of the view supplied to
481     * {@link #startRecyclerTracing(String, View)}.
482     *
483     * This method will return immediately if TRACE_RECYCLER is false.
484     *
485     * @see #startRecyclerTracing(String, View)
486     * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
487     */
488    public static void stopRecyclerTracing() {
489        //noinspection PointlessBooleanExpression,ConstantConditions
490        if (!TRACE_RECYCLER) {
491            return;
492        }
493
494        if (sRecyclerOwnerView == null || sRecyclerViews == null) {
495            throw new IllegalStateException("You must call startRecyclerTracing() before" +
496                " stopRecyclerTracing()!");
497        }
498
499        File recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/");
500        //noinspection ResultOfMethodCallIgnored
501        recyclerDump.mkdirs();
502
503        recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".recycler");
504        try {
505            final BufferedWriter out = new BufferedWriter(new FileWriter(recyclerDump), 8 * 1024);
506
507            for (View view : sRecyclerViews) {
508                final String name = view.getClass().getName();
509                out.write(name);
510                out.newLine();
511            }
512
513            out.close();
514        } catch (IOException e) {
515            Log.e("View", "Could not dump recycler content");
516            return;
517        }
518
519        recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/");
520        recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces");
521        try {
522            final FileOutputStream file = new FileOutputStream(recyclerDump);
523            final DataOutputStream out = new DataOutputStream(file);
524
525            for (RecyclerTrace trace : sRecyclerTraces) {
526                out.writeInt(trace.view);
527                out.writeInt(trace.type.ordinal());
528                out.writeInt(trace.position);
529                out.writeInt(trace.indexOnScreen);
530                out.flush();
531            }
532
533            out.close();
534        } catch (IOException e) {
535            Log.e("View", "Could not dump recycler traces");
536            return;
537        }
538
539        sRecyclerViews.clear();
540        sRecyclerViews = null;
541
542        sRecyclerTraces.clear();
543        sRecyclerTraces = null;
544
545        sRecyclerOwnerView = null;
546    }
547
548    /**
549     * Outputs a trace to the currently opened traces file. The trace contains the class name
550     * and instance's hashcode of the specified view as well as the supplied trace type.
551     *
552     * @param view the view to trace
553     * @param type the type of the trace
554     */
555    public static void trace(View view, HierarchyTraceType type) {
556        if (sHierarchyTraces == null) {
557            return;
558        }
559
560        try {
561            sHierarchyTraces.write(type.name());
562            sHierarchyTraces.write(' ');
563            sHierarchyTraces.write(view.getClass().getName());
564            sHierarchyTraces.write('@');
565            sHierarchyTraces.write(Integer.toHexString(view.hashCode()));
566            sHierarchyTraces.newLine();
567        } catch (IOException e) {
568            Log.w("View", "Error while dumping trace of type " + type + " for view " + view);
569        }
570    }
571
572    /**
573     * Starts tracing the view hierarchy of the specified view. The trace is identified by a prefix,
574     * used to build the traces files names: <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code> and
575     * <code>/EXTERNAL/view-hierarchy/PREFIX.tree</code>.
576     *
577     * Only one view hierarchy can be traced at the same time. After calling this method, any
578     * other invocation will result in a <code>IllegalStateException</code> unless
579     * {@link #stopHierarchyTracing()} is invoked before.
580     *
581     * Calling this method creates the file <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code>
582     * containing all the traces (or method calls) relative to the specified view's hierarchy.
583     *
584     * This method will return immediately if TRACE_HIERARCHY is false.
585     *
586     * @param prefix the traces files name prefix
587     * @param view the view whose hierarchy must be traced
588     *
589     * @see #stopHierarchyTracing()
590     * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
591     */
592    public static void startHierarchyTracing(String prefix, View view) {
593        //noinspection PointlessBooleanExpression,ConstantConditions
594        if (!TRACE_HIERARCHY) {
595            return;
596        }
597
598        if (sHierarhcyRoot != null) {
599            throw new IllegalStateException("You must call stopHierarchyTracing() before running" +
600                " a new trace!");
601        }
602
603        File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/");
604        //noinspection ResultOfMethodCallIgnored
605        hierarchyDump.mkdirs();
606
607        hierarchyDump = new File(hierarchyDump, prefix + ".traces");
608        sHierarchyTracePrefix = prefix;
609
610        try {
611            sHierarchyTraces = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
612        } catch (IOException e) {
613            Log.e("View", "Could not dump view hierarchy");
614            return;
615        }
616
617        sHierarhcyRoot = (ViewRoot) view.getRootView().getParent();
618    }
619
620    /**
621     * Stops the current view hierarchy tracing. This method closes the file
622     * <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code>.
623     *
624     * Calling this method creates the file <code>/EXTERNAL/view-hierarchy/PREFIX.tree</code>
625     * containing the view hierarchy of the view supplied to
626     * {@link #startHierarchyTracing(String, View)}.
627     *
628     * This method will return immediately if TRACE_HIERARCHY is false.
629     *
630     * @see #startHierarchyTracing(String, View)
631     * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
632     */
633    public static void stopHierarchyTracing() {
634        //noinspection PointlessBooleanExpression,ConstantConditions
635        if (!TRACE_HIERARCHY) {
636            return;
637        }
638
639        if (sHierarhcyRoot == null || sHierarchyTraces == null) {
640            throw new IllegalStateException("You must call startHierarchyTracing() before" +
641                " stopHierarchyTracing()!");
642        }
643
644        try {
645            sHierarchyTraces.close();
646        } catch (IOException e) {
647            Log.e("View", "Could not write view traces");
648        }
649        sHierarchyTraces = null;
650
651        File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/");
652        //noinspection ResultOfMethodCallIgnored
653        hierarchyDump.mkdirs();
654        hierarchyDump = new File(hierarchyDump, sHierarchyTracePrefix + ".tree");
655
656        BufferedWriter out;
657        try {
658            out = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
659        } catch (IOException e) {
660            Log.e("View", "Could not dump view hierarchy");
661            return;
662        }
663
664        View view = sHierarhcyRoot.getView();
665        if (view instanceof ViewGroup) {
666            ViewGroup group = (ViewGroup) view;
667            dumpViewHierarchy(group, out, 0);
668            try {
669                out.close();
670            } catch (IOException e) {
671                Log.e("View", "Could not dump view hierarchy");
672            }
673        }
674
675        sHierarhcyRoot = null;
676    }
677
678    static void dispatchCommand(View view, String command, String parameters,
679            OutputStream clientStream) throws IOException {
680
681        // Paranoid but safe...
682        view = view.getRootView();
683
684        if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
685            dump(view, clientStream);
686        } else {
687            final String[] params = parameters.split(" ");
688            if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
689                capture(view, clientStream, params[0]);
690            } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
691                invalidate(view, params[0]);
692            } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
693                requestLayout(view, params[0]);
694            } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
695                profile(view, clientStream, params[0]);
696            }
697        }
698    }
699
700    private static View findView(View root, String parameter) {
701        // Look by type/hashcode
702        if (parameter.indexOf('@') != -1) {
703            final String[] ids = parameter.split("@");
704            final String className = ids[0];
705            final int hashCode = (int) Long.parseLong(ids[1], 16);
706
707            View view = root.getRootView();
708            if (view instanceof ViewGroup) {
709                return findView((ViewGroup) view, className, hashCode);
710            }
711        } else {
712            // Look by id
713            final int id = root.getResources().getIdentifier(parameter, null, null);
714            return root.getRootView().findViewById(id);
715        }
716
717        return null;
718    }
719
720    private static void invalidate(View root, String parameter) {
721        final View view = findView(root, parameter);
722        if (view != null) {
723            view.postInvalidate();
724        }
725    }
726
727    private static void requestLayout(View root, String parameter) {
728        final View view = findView(root, parameter);
729        if (view != null) {
730            root.post(new Runnable() {
731                public void run() {
732                    view.requestLayout();
733                }
734            });
735        }
736    }
737
738    private static void profile(View root, OutputStream clientStream, String parameter)
739            throws IOException {
740
741        final View view = findView(root, parameter);
742        BufferedWriter out = null;
743        try {
744            out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
745
746            if (view != null) {
747                final long durationMeasure = profileViewOperation(view, new ViewOperation<Void>() {
748                    public Void[] pre() {
749                        forceLayout(view);
750                        return null;
751                    }
752
753                    private void forceLayout(View view) {
754                        view.forceLayout();
755                        if (view instanceof ViewGroup) {
756                            ViewGroup group = (ViewGroup) view;
757                            final int count = group.getChildCount();
758                            for (int i = 0; i < count; i++) {
759                                forceLayout(group.getChildAt(i));
760                            }
761                        }
762                    }
763
764                    public void run(Void... data) {
765                        view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
766                    }
767
768                    public void post(Void... data) {
769                    }
770                });
771
772                final long durationLayout = profileViewOperation(view, new ViewOperation<Void>() {
773                    public Void[] pre() {
774                        return null;
775                    }
776
777                    public void run(Void... data) {
778                        view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom);
779                    }
780
781                    public void post(Void... data) {
782                    }
783                });
784
785                final long durationDraw = profileViewOperation(view, new ViewOperation<Object>() {
786                    public Object[] pre() {
787                        final DisplayMetrics metrics = view.getResources().getDisplayMetrics();
788                        final Bitmap bitmap = Bitmap.createBitmap(metrics.widthPixels,
789                                metrics.heightPixels, Bitmap.Config.RGB_565);
790                        final Canvas canvas = new Canvas(bitmap);
791                        return new Object[] { bitmap, canvas };
792                    }
793
794                    public void run(Object... data) {
795                        view.draw((Canvas) data[1]);
796                    }
797
798                    public void post(Object... data) {
799                        ((Bitmap) data[0]).recycle();
800                    }
801                });
802
803                out.write(String.valueOf(durationMeasure));
804                out.write(' ');
805                out.write(String.valueOf(durationLayout));
806                out.write(' ');
807                out.write(String.valueOf(durationDraw));
808                out.newLine();
809            } else {
810                out.write("-1 -1 -1");
811                out.newLine();
812            }
813        } catch (Exception e) {
814            android.util.Log.w("View", "Problem profiling the view:", e);
815        } finally {
816            if (out != null) {
817                out.close();
818            }
819        }
820    }
821
822    interface ViewOperation<T> {
823        T[] pre();
824        void run(T... data);
825        void post(T... data);
826    }
827
828    private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) {
829        final CountDownLatch latch = new CountDownLatch(1);
830        final long[] duration = new long[1];
831
832        view.post(new Runnable() {
833            public void run() {
834                try {
835                    T[] data = operation.pre();
836                    long start = Debug.threadCpuTimeNanos();
837                    operation.run(data);
838                    duration[0] = Debug.threadCpuTimeNanos() - start;
839                    operation.post(data);
840                } finally {
841                    latch.countDown();
842                }
843            }
844        });
845
846        try {
847            latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
848        } catch (InterruptedException e) {
849            Log.w("View", "Could not complete the profiling of the view " + view);
850            Thread.currentThread().interrupt();
851            return -1;
852        }
853
854        return duration[0];
855    }
856
857    private static void capture(View root, final OutputStream clientStream, String parameter)
858            throws IOException {
859
860        final View captureView = findView(root, parameter);
861
862        if (captureView != null) {
863            final CountDownLatch latch = new CountDownLatch(1);
864            final Bitmap[] cache = new Bitmap[1];
865
866            root.post(new Runnable() {
867                public void run() {
868                    try {
869                        cache[0] = captureView.createSnapshot(
870                                Bitmap.Config.ARGB_8888, 0);
871                    } catch (OutOfMemoryError e) {
872                        try {
873                            cache[0] = captureView.createSnapshot(
874                                    Bitmap.Config.ARGB_4444, 0);
875                        } catch (OutOfMemoryError e2) {
876                            Log.w("View", "Out of memory for bitmap");
877                        }
878                    } finally {
879                        latch.countDown();
880                    }
881                }
882            });
883
884            try {
885                latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
886
887                if (cache[0] != null) {
888                    BufferedOutputStream out = null;
889                    try {
890                        out = new BufferedOutputStream(clientStream, 32 * 1024);
891                        cache[0].compress(Bitmap.CompressFormat.PNG, 100, out);
892                        out.flush();
893                    } finally {
894                        if (out != null) {
895                            out.close();
896                        }
897                        cache[0].recycle();
898                    }
899                } else {
900                    Log.w("View", "Failed to create capture bitmap!");
901                    clientStream.close();
902                }
903            } catch (InterruptedException e) {
904                Log.w("View", "Could not complete the capture of the view " + captureView);
905                Thread.currentThread().interrupt();
906            }
907        }
908    }
909
910    private static void dump(View root, OutputStream clientStream) throws IOException {
911        BufferedWriter out = null;
912        try {
913            out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
914            View view = root.getRootView();
915            if (view instanceof ViewGroup) {
916                ViewGroup group = (ViewGroup) view;
917                dumpViewHierarchyWithProperties(group.getContext(), group, out, 0);
918            }
919            out.write("DONE.");
920            out.newLine();
921        } catch (Exception e) {
922            android.util.Log.w("View", "Problem dumping the view:", e);
923        } finally {
924            if (out != null) {
925                out.close();
926            }
927        }
928    }
929
930    private static View findView(ViewGroup group, String className, int hashCode) {
931        if (isRequestedView(group, className, hashCode)) {
932            return group;
933        }
934
935        final int count = group.getChildCount();
936        for (int i = 0; i < count; i++) {
937            final View view = group.getChildAt(i);
938            if (view instanceof ViewGroup) {
939                final View found = findView((ViewGroup) view, className, hashCode);
940                if (found != null) {
941                    return found;
942                }
943            } else if (isRequestedView(view, className, hashCode)) {
944                return view;
945            }
946        }
947
948        return null;
949    }
950
951    private static boolean isRequestedView(View view, String className, int hashCode) {
952        return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
953    }
954
955    private static void dumpViewHierarchyWithProperties(Context context, ViewGroup group,
956            BufferedWriter out, int level) {
957        if (!dumpViewWithProperties(context, group, out, level)) {
958            return;
959        }
960
961        final int count = group.getChildCount();
962        for (int i = 0; i < count; i++) {
963            final View view = group.getChildAt(i);
964            if (view instanceof ViewGroup) {
965                dumpViewHierarchyWithProperties(context, (ViewGroup) view, out, level + 1);
966            } else {
967                dumpViewWithProperties(context, view, out, level + 1);
968            }
969        }
970    }
971
972    private static boolean dumpViewWithProperties(Context context, View view,
973            BufferedWriter out, int level) {
974
975        try {
976            for (int i = 0; i < level; i++) {
977                out.write(' ');
978            }
979            out.write(view.getClass().getName());
980            out.write('@');
981            out.write(Integer.toHexString(view.hashCode()));
982            out.write(' ');
983            dumpViewProperties(context, view, out);
984            out.newLine();
985        } catch (IOException e) {
986            Log.w("View", "Error while dumping hierarchy tree");
987            return false;
988        }
989        return true;
990    }
991
992    private static Field[] getExportedPropertyFields(Class<?> klass) {
993        if (sFieldsForClasses == null) {
994            sFieldsForClasses = new HashMap<Class<?>, Field[]>();
995        }
996        if (sAnnotations == null) {
997            sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
998        }
999
1000        final HashMap<Class<?>, Field[]> map = sFieldsForClasses;
1001        final HashMap<AccessibleObject, ExportedProperty> annotations = sAnnotations;
1002
1003        Field[] fields = map.get(klass);
1004        if (fields != null) {
1005            return fields;
1006        }
1007
1008        final ArrayList<Field> foundFields = new ArrayList<Field>();
1009        fields = klass.getDeclaredFields();
1010
1011        int count = fields.length;
1012        for (int i = 0; i < count; i++) {
1013            final Field field = fields[i];
1014            if (field.isAnnotationPresent(ExportedProperty.class)) {
1015                field.setAccessible(true);
1016                foundFields.add(field);
1017                annotations.put(field, field.getAnnotation(ExportedProperty.class));
1018            }
1019        }
1020
1021        fields = foundFields.toArray(new Field[foundFields.size()]);
1022        map.put(klass, fields);
1023
1024        return fields;
1025    }
1026
1027    private static Method[] getExportedPropertyMethods(Class<?> klass) {
1028        if (sMethodsForClasses == null) {
1029            sMethodsForClasses = new HashMap<Class<?>, Method[]>(100);
1030        }
1031        if (sAnnotations == null) {
1032            sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
1033        }
1034
1035        final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
1036        final HashMap<AccessibleObject, ExportedProperty> annotations = sAnnotations;
1037
1038        Method[] methods = map.get(klass);
1039        if (methods != null) {
1040            return methods;
1041        }
1042
1043        final ArrayList<Method> foundMethods = new ArrayList<Method>();
1044        methods = klass.getDeclaredMethods();
1045
1046        int count = methods.length;
1047        for (int i = 0; i < count; i++) {
1048            final Method method = methods[i];
1049            if (method.getParameterTypes().length == 0 &&
1050                    method.isAnnotationPresent(ExportedProperty.class) &&
1051                    method.getReturnType() != Void.class) {
1052                method.setAccessible(true);
1053                foundMethods.add(method);
1054                annotations.put(method, method.getAnnotation(ExportedProperty.class));
1055            }
1056        }
1057
1058        methods = foundMethods.toArray(new Method[foundMethods.size()]);
1059        map.put(klass, methods);
1060
1061        return methods;
1062    }
1063
1064    private static void dumpViewProperties(Context context, Object view,
1065            BufferedWriter out) throws IOException {
1066
1067        dumpViewProperties(context, view, out, "");
1068    }
1069
1070    private static void dumpViewProperties(Context context, Object view,
1071            BufferedWriter out, String prefix) throws IOException {
1072
1073        Class<?> klass = view.getClass();
1074
1075        do {
1076            exportFields(context, view, out, klass, prefix);
1077            exportMethods(context, view, out, klass, prefix);
1078            klass = klass.getSuperclass();
1079        } while (klass != Object.class);
1080    }
1081
1082    private static void exportMethods(Context context, Object view, BufferedWriter out,
1083            Class<?> klass, String prefix) throws IOException {
1084
1085        final Method[] methods = getExportedPropertyMethods(klass);
1086
1087        int count = methods.length;
1088        for (int i = 0; i < count; i++) {
1089            final Method method = methods[i];
1090            //noinspection EmptyCatchBlock
1091            try {
1092                // TODO: This should happen on the UI thread
1093                Object methodValue = method.invoke(view, (Object[]) null);
1094                final Class<?> returnType = method.getReturnType();
1095
1096                if (returnType == int.class) {
1097                    final ExportedProperty property = sAnnotations.get(method);
1098                    if (property.resolveId() && context != null) {
1099                        final int id = (Integer) methodValue;
1100                        methodValue = resolveId(context, id);
1101                    } else {
1102                        final FlagToString[] flagsMapping = property.flagMapping();
1103                        if (flagsMapping.length > 0) {
1104                            final int intValue = (Integer) methodValue;
1105                            final String valuePrefix = prefix + method.getName() + '_';
1106                            exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1107                        }
1108
1109                        final IntToString[] mapping = property.mapping();
1110                        if (mapping.length > 0) {
1111                            final int intValue = (Integer) methodValue;
1112                            boolean mapped = false;
1113                            int mappingCount = mapping.length;
1114                            for (int j = 0; j < mappingCount; j++) {
1115                                final IntToString mapper = mapping[j];
1116                                if (mapper.from() == intValue) {
1117                                    methodValue = mapper.to();
1118                                    mapped = true;
1119                                    break;
1120                                }
1121                            }
1122
1123                            if (!mapped) {
1124                                methodValue = intValue;
1125                            }
1126                        }
1127                    }
1128                } else if (returnType == int[].class) {
1129                    final ExportedProperty property = sAnnotations.get(method);
1130                    final int[] array = (int[]) methodValue;
1131                    final String valuePrefix = prefix + method.getName() + '_';
1132                    final String suffix = "()";
1133
1134                    exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1135                } else if (!returnType.isPrimitive()) {
1136                    final ExportedProperty property = sAnnotations.get(method);
1137                    if (property.deepExport()) {
1138                        dumpViewProperties(context, methodValue, out, prefix + property.prefix());
1139                        continue;
1140                    }
1141                }
1142
1143                writeEntry(out, prefix, method.getName(), "()", methodValue);
1144            } catch (IllegalAccessException e) {
1145            } catch (InvocationTargetException e) {
1146            }
1147        }
1148    }
1149
1150    private static void exportFields(Context context, Object view, BufferedWriter out,
1151            Class<?> klass, String prefix) throws IOException {
1152
1153        final Field[] fields = getExportedPropertyFields(klass);
1154
1155        int count = fields.length;
1156        for (int i = 0; i < count; i++) {
1157            final Field field = fields[i];
1158
1159            //noinspection EmptyCatchBlock
1160            try {
1161                Object fieldValue = null;
1162                final Class<?> type = field.getType();
1163
1164                if (type == int.class) {
1165                    final ExportedProperty property = sAnnotations.get(field);
1166                    if (property.resolveId() && context != null) {
1167                        final int id = field.getInt(view);
1168                        fieldValue = resolveId(context, id);
1169                    } else {
1170                        final FlagToString[] flagsMapping = property.flagMapping();
1171                        if (flagsMapping.length > 0) {
1172                            final int intValue = field.getInt(view);
1173                            final String valuePrefix = prefix + field.getName() + '_';
1174                            exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1175                        }
1176
1177                        final IntToString[] mapping = property.mapping();
1178                        if (mapping.length > 0) {
1179                            final int intValue = field.getInt(view);
1180                            int mappingCount = mapping.length;
1181                            for (int j = 0; j < mappingCount; j++) {
1182                                final IntToString mapped = mapping[j];
1183                                if (mapped.from() == intValue) {
1184                                    fieldValue = mapped.to();
1185                                    break;
1186                                }
1187                            }
1188
1189                            if (fieldValue == null) {
1190                                fieldValue = intValue;
1191                            }
1192                        }
1193                    }
1194                } else if (type == int[].class) {
1195                    final ExportedProperty property = sAnnotations.get(field);
1196                    final int[] array = (int[]) field.get(view);
1197                    final String valuePrefix = prefix + field.getName() + '_';
1198                    final String suffix = "";
1199
1200                    exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1201
1202                    // We exit here!
1203                    return;
1204                } else if (!type.isPrimitive()) {
1205                    final ExportedProperty property = sAnnotations.get(field);
1206                    if (property.deepExport()) {
1207                        dumpViewProperties(context, field.get(view), out,
1208                                prefix + property.prefix());
1209                        continue;
1210                    }
1211                }
1212
1213                if (fieldValue == null) {
1214                    fieldValue = field.get(view);
1215                }
1216
1217                writeEntry(out, prefix, field.getName(), "", fieldValue);
1218            } catch (IllegalAccessException e) {
1219            }
1220        }
1221    }
1222
1223    private static void writeEntry(BufferedWriter out, String prefix, String name,
1224            String suffix, Object value) throws IOException {
1225
1226        out.write(prefix);
1227        out.write(name);
1228        out.write(suffix);
1229        out.write("=");
1230        writeValue(out, value);
1231        out.write(' ');
1232    }
1233
1234    private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
1235            int intValue, String prefix) throws IOException {
1236
1237        final int count = mapping.length;
1238        for (int j = 0; j < count; j++) {
1239            final FlagToString flagMapping = mapping[j];
1240            final boolean ifTrue = flagMapping.outputIf();
1241            final int maskResult = intValue & flagMapping.mask();
1242            final boolean test = maskResult == flagMapping.equals();
1243            if ((test && ifTrue) || (!test && !ifTrue)) {
1244                final String name = flagMapping.name();
1245                final String value = "0x" + Integer.toHexString(maskResult);
1246                writeEntry(out, prefix, name, "", value);
1247            }
1248        }
1249    }
1250
1251    private static void exportUnrolledArray(Context context, BufferedWriter out,
1252            ExportedProperty property, int[] array, String prefix, String suffix)
1253            throws IOException {
1254
1255        final IntToString[] indexMapping = property.indexMapping();
1256        final boolean hasIndexMapping = indexMapping.length > 0;
1257
1258        final IntToString[] mapping = property.mapping();
1259        final boolean hasMapping = mapping.length > 0;
1260
1261        final boolean resolveId = property.resolveId() && context != null;
1262        final int valuesCount = array.length;
1263
1264        for (int j = 0; j < valuesCount; j++) {
1265            String name;
1266            String value = null;
1267
1268            final int intValue = array[j];
1269
1270            name = String.valueOf(j);
1271            if (hasIndexMapping) {
1272                int mappingCount = indexMapping.length;
1273                for (int k = 0; k < mappingCount; k++) {
1274                    final IntToString mapped = indexMapping[k];
1275                    if (mapped.from() == j) {
1276                        name = mapped.to();
1277                        break;
1278                    }
1279                }
1280            }
1281
1282            if (hasMapping) {
1283                int mappingCount = mapping.length;
1284                for (int k = 0; k < mappingCount; k++) {
1285                    final IntToString mapped = mapping[k];
1286                    if (mapped.from() == intValue) {
1287                        value = mapped.to();
1288                        break;
1289                    }
1290                }
1291            }
1292
1293            if (resolveId) {
1294                if (value == null) value = (String) resolveId(context, intValue);
1295            } else {
1296                value = String.valueOf(intValue);
1297            }
1298
1299            writeEntry(out, prefix, name, suffix, value);
1300        }
1301    }
1302
1303    static Object resolveId(Context context, int id) {
1304        Object fieldValue;
1305        final Resources resources = context.getResources();
1306        if (id >= 0) {
1307            try {
1308                fieldValue = resources.getResourceTypeName(id) + '/' +
1309                        resources.getResourceEntryName(id);
1310            } catch (Resources.NotFoundException e) {
1311                fieldValue = "id/0x" + Integer.toHexString(id);
1312            }
1313        } else {
1314            fieldValue = "NO_ID";
1315        }
1316        return fieldValue;
1317    }
1318
1319    private static void writeValue(BufferedWriter out, Object value) throws IOException {
1320        if (value != null) {
1321            String output = value.toString().replace("\n", "\\n");
1322            out.write(String.valueOf(output.length()));
1323            out.write(",");
1324            out.write(output);
1325        } else {
1326            out.write("4,null");
1327        }
1328    }
1329
1330    private static void dumpViewHierarchy(ViewGroup group, BufferedWriter out, int level) {
1331        if (!dumpView(group, out, level)) {
1332            return;
1333        }
1334
1335        final int count = group.getChildCount();
1336        for (int i = 0; i < count; i++) {
1337            final View view = group.getChildAt(i);
1338            if (view instanceof ViewGroup) {
1339                dumpViewHierarchy((ViewGroup) view, out, level + 1);
1340            } else {
1341                dumpView(view, out, level + 1);
1342            }
1343        }
1344    }
1345
1346    private static boolean dumpView(Object view, BufferedWriter out, int level) {
1347        try {
1348            for (int i = 0; i < level; i++) {
1349                out.write(' ');
1350            }
1351            out.write(view.getClass().getName());
1352            out.write('@');
1353            out.write(Integer.toHexString(view.hashCode()));
1354            out.newLine();
1355        } catch (IOException e) {
1356            Log.w("View", "Error while dumping hierarchy tree");
1357            return false;
1358        }
1359        return true;
1360    }
1361
1362    private static Field[] capturedViewGetPropertyFields(Class<?> klass) {
1363        if (mCapturedViewFieldsForClasses == null) {
1364            mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>();
1365        }
1366        final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses;
1367
1368        Field[] fields = map.get(klass);
1369        if (fields != null) {
1370            return fields;
1371        }
1372
1373        final ArrayList<Field> foundFields = new ArrayList<Field>();
1374        fields = klass.getFields();
1375
1376        int count = fields.length;
1377        for (int i = 0; i < count; i++) {
1378            final Field field = fields[i];
1379            if (field.isAnnotationPresent(CapturedViewProperty.class)) {
1380                field.setAccessible(true);
1381                foundFields.add(field);
1382            }
1383        }
1384
1385        fields = foundFields.toArray(new Field[foundFields.size()]);
1386        map.put(klass, fields);
1387
1388        return fields;
1389    }
1390
1391    private static Method[] capturedViewGetPropertyMethods(Class<?> klass) {
1392        if (mCapturedViewMethodsForClasses == null) {
1393            mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>();
1394        }
1395        final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses;
1396
1397        Method[] methods = map.get(klass);
1398        if (methods != null) {
1399            return methods;
1400        }
1401
1402        final ArrayList<Method> foundMethods = new ArrayList<Method>();
1403        methods = klass.getMethods();
1404
1405        int count = methods.length;
1406        for (int i = 0; i < count; i++) {
1407            final Method method = methods[i];
1408            if (method.getParameterTypes().length == 0 &&
1409                    method.isAnnotationPresent(CapturedViewProperty.class) &&
1410                    method.getReturnType() != Void.class) {
1411                method.setAccessible(true);
1412                foundMethods.add(method);
1413            }
1414        }
1415
1416        methods = foundMethods.toArray(new Method[foundMethods.size()]);
1417        map.put(klass, methods);
1418
1419        return methods;
1420    }
1421
1422    private static String capturedViewExportMethods(Object obj, Class<?> klass,
1423            String prefix) {
1424
1425        if (obj == null) {
1426            return "null";
1427        }
1428
1429        StringBuilder sb = new StringBuilder();
1430        final Method[] methods = capturedViewGetPropertyMethods(klass);
1431
1432        int count = methods.length;
1433        for (int i = 0; i < count; i++) {
1434            final Method method = methods[i];
1435            try {
1436                Object methodValue = method.invoke(obj, (Object[]) null);
1437                final Class<?> returnType = method.getReturnType();
1438
1439                CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class);
1440                if (property.retrieveReturn()) {
1441                    //we are interested in the second level data only
1442                    sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#"));
1443                } else {
1444                    sb.append(prefix);
1445                    sb.append(method.getName());
1446                    sb.append("()=");
1447
1448                    if (methodValue != null) {
1449                        final String value = methodValue.toString().replace("\n", "\\n");
1450                        sb.append(value);
1451                    } else {
1452                        sb.append("null");
1453                    }
1454                    sb.append("; ");
1455                }
1456              } catch (IllegalAccessException e) {
1457                  //Exception IllegalAccess, it is OK here
1458                  //we simply ignore this method
1459              } catch (InvocationTargetException e) {
1460                  //Exception InvocationTarget, it is OK here
1461                  //we simply ignore this method
1462              }
1463        }
1464        return sb.toString();
1465    }
1466
1467    private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) {
1468
1469        if (obj == null) {
1470            return "null";
1471        }
1472
1473        StringBuilder sb = new StringBuilder();
1474        final Field[] fields = capturedViewGetPropertyFields(klass);
1475
1476        int count = fields.length;
1477        for (int i = 0; i < count; i++) {
1478            final Field field = fields[i];
1479            try {
1480                Object fieldValue = field.get(obj);
1481
1482                sb.append(prefix);
1483                sb.append(field.getName());
1484                sb.append("=");
1485
1486                if (fieldValue != null) {
1487                    final String value = fieldValue.toString().replace("\n", "\\n");
1488                    sb.append(value);
1489                } else {
1490                    sb.append("null");
1491                }
1492                sb.append(' ');
1493            } catch (IllegalAccessException e) {
1494                //Exception IllegalAccess, it is OK here
1495                //we simply ignore this field
1496            }
1497        }
1498        return sb.toString();
1499    }
1500
1501    /**
1502     * Dump view info for id based instrument test generation
1503     * (and possibly further data analysis). The results are dumped
1504     * to the log.
1505     * @param tag for log
1506     * @param view for dump
1507     */
1508    public static void dumpCapturedView(String tag, Object view) {
1509        Class<?> klass = view.getClass();
1510        StringBuilder sb = new StringBuilder(klass.getName() + ": ");
1511        sb.append(capturedViewExportFields(view, klass, ""));
1512        sb.append(capturedViewExportMethods(view, klass, ""));
1513        Log.d(tag, sb.toString());
1514    }
1515}
1516