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