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