• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.view;
18 
19 import android.util.Config;
20 import android.util.Log;
21 import android.util.DisplayMetrics;
22 import android.content.res.Resources;
23 import android.content.Context;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.os.Environment;
27 import android.os.Debug;
28 
29 import java.io.File;
30 import java.io.BufferedWriter;
31 import java.io.FileWriter;
32 import java.io.IOException;
33 import java.io.FileOutputStream;
34 import java.io.DataOutputStream;
35 import java.io.OutputStreamWriter;
36 import java.io.BufferedOutputStream;
37 import java.io.OutputStream;
38 import java.util.List;
39 import java.util.LinkedList;
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.concurrent.CountDownLatch;
43 import java.util.concurrent.TimeUnit;
44 import java.lang.annotation.Target;
45 import java.lang.annotation.ElementType;
46 import java.lang.annotation.Retention;
47 import java.lang.annotation.RetentionPolicy;
48 import java.lang.reflect.Field;
49 import java.lang.reflect.Method;
50 import java.lang.reflect.InvocationTargetException;
51 import java.lang.reflect.AccessibleObject;
52 
53 /**
54  * Various debugging/tracing tools related to {@link View} and the view hierarchy.
55  */
56 public 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) {
Debug.setFieldsOn(ViewDebug.class, true)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          */
resolveId()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          */
mapping()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          */
indexMapping()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          */
flagMapping()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          */
deepExport()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          */
prefix()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          */
from()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          */
to()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          */
mask()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          */
equals()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          */
name()301         String name();
302 
303         /**
304          * Indicates whether to output the flag when the test is true,
305          * or false. Defaults to true.
306          */
outputIf()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          */
retrieveReturn()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      */
getViewInstanceCount()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      */
getViewRootInstanceCount()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      */
trace(View view, RecyclerTraceType type, int... parameters)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      */
startRecyclerTracing(String prefix, View view)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      */
stopRecyclerTracing()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      */
trace(View view, HierarchyTraceType type)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      */
startHierarchyTracing(String prefix, View view)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      */
stopHierarchyTracing()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 
dispatchCommand(View view, String command, String parameters, OutputStream clientStream)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 
findView(View root, String parameter)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 = Integer.parseInt(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 
invalidate(View root, String parameter)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 
requestLayout(View root, String parameter)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 
profile(View root, OutputStream clientStream, String parameter)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> {
pre()823         T[] pre();
run(T... data)824         void run(T... data);
post(T... data)825         void post(T... data);
826     }
827 
profileViewOperation(View view, final ViewOperation<T> operation)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 
capture(View root, final OutputStream clientStream, String parameter)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 
dump(View root, OutputStream clientStream)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 
findView(ViewGroup group, String className, int hashCode)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 
isRequestedView(View view, String className, int hashCode)951     private static boolean isRequestedView(View view, String className, int hashCode) {
952         return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
953     }
954 
dumpViewHierarchyWithProperties(Context context, ViewGroup group, BufferedWriter out, int level)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 
dumpViewWithProperties(Context context, View view, BufferedWriter out, int level)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 
getExportedPropertyFields(Class<?> klass)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 
getExportedPropertyMethods(Class<?> klass)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 
dumpViewProperties(Context context, Object view, BufferedWriter out)1064     private static void dumpViewProperties(Context context, Object view,
1065             BufferedWriter out) throws IOException {
1066 
1067         dumpViewProperties(context, view, out, "");
1068     }
1069 
dumpViewProperties(Context context, Object view, BufferedWriter out, String prefix)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 
exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)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 
exportFields(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)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 
writeEntry(BufferedWriter out, String prefix, String name, String suffix, Object value)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 
exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, int intValue, String prefix)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 
exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix)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 
resolveId(Context context, int id)1303     private 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 
writeValue(BufferedWriter out, Object value)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 
dumpViewHierarchy(ViewGroup group, BufferedWriter out, int level)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 
dumpView(Object view, BufferedWriter out, int level)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 
capturedViewGetPropertyFields(Class<?> klass)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 
capturedViewGetPropertyMethods(Class<?> klass)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 
capturedViewExportMethods(Object obj, Class<?> klass, String prefix)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 
capturedViewExportFields(Object obj, Class<?> klass, String prefix)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      */
dumpCapturedView(String tag, Object view)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