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