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