Home | History | Annotate | Download | only in view
      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) {
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    321         String name();
    322 
    323         /**
    324          * Indicates whether to output the flag when the test is true,
    325          * or false. Defaults to true.
    326          */
    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          */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    943     private static void profileViewAndChildren(final View view, BufferedWriter out)
    944             throws IOException {
    945         profileViewAndChildren(view, out, true);
    946     }
    947 
    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> {
   1032         T[] pre();
   1033         void run(T... data);
   1034         void post(T... data);
   1035     }
   1036 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   1238     private static boolean isRequestedView(View view, String className, int hashCode) {
   1239         return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
   1240     }
   1241 
   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 
   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 
   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 
   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 
   1351     private static void dumpViewProperties(Context context, Object view,
   1352             BufferedWriter out) throws IOException {
   1353 
   1354         dumpViewProperties(context, view, out, "");
   1355     }
   1356 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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      */
   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