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          */
resolveId()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          */
mapping()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          */
indexMapping()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          */
flagMapping()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          */
deepExport()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          */
prefix()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          */
category()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          */
formatToHexString()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          */
hasAdjacentMapping()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          */
from()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          */
to()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          */
mask()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          */
equals()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          */
name()255         String name();
256 
257         /**
258          * Indicates whether to output the flag when the test is true,
259          * or false. Defaults to true.
260          */
outputIf()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          */
retrieveReturn()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          */
dumpViewHierarchyWithProperties(BufferedWriter out, int level)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          */
findHierarchyView(String className, int hashCode)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      */
getViewInstanceCount()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      */
getViewRootImplCount()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" })
trace(View view, RecyclerTraceType type, int... parameters)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")
startRecyclerTracing(String prefix, View view)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")
stopRecyclerTracing()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" })
trace(View view, HierarchyTraceType type)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")
startHierarchyTracing(String prefix, View view)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
stopHierarchyTracing()424     public static void stopHierarchyTracing() {
425     }
426 
dispatchCommand(View view, String command, String parameters, OutputStream clientStream)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 */
findView(View root, String parameter)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 
invalidate(View root, String parameter)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 
requestLayout(View root, String parameter)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 
profile(View root, OutputStream clientStream, String parameter)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 */
profileViewAndChildren(final View view, BufferedWriter out)520     public static void profileViewAndChildren(final View view, BufferedWriter out)
521             throws IOException {
522         profileViewAndChildren(view, out, true);
523     }
524 
profileViewAndChildren(final View view, BufferedWriter out, boolean root)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> {
pre()616         T[] pre();
run(T... data)617         void run(T... data);
post(T... data)618         void post(T... data);
619     }
620 
profileViewOperation(View view, final ViewOperation<T> operation)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 */
captureLayers(View root, final DataOutputStream clientStream)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 
captureViewLayer(View view, DataOutputStream clientStream, boolean visible)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 
outputDisplayList(View root, String parameter)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 */
outputDisplayList(View root, View target)734     public static void outputDisplayList(View root, View target) {
735         root.getViewRootImpl().outputDisplayList(target);
736     }
737 
capture(View root, final OutputStream clientStream, String parameter)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 */
capture(View root, final OutputStream clientStream, View captureView)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 
performViewCapture(final View captureView, final boolean skipChildren)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      */
dump(View root, boolean skipChildren, boolean includeProperties, OutputStream clientStream)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      */
dumpTheme(View view, OutputStream clientStream)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      */
getStyleAttributesDump(Resources resources, Resources.Theme theme)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 
findView(ViewGroup group, String className, int hashCode)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 
isRequestedView(View view, String className, int hashCode)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 
dumpViewHierarchy(Context context, ViewGroup group, BufferedWriter out, int level, boolean skipChildren, boolean includeProperties)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 
dumpView(Context context, View view, BufferedWriter out, int level, boolean includeProperties)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 
getExportedPropertyFields(Class<?> klass)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> declaredFields = new ArrayList();
1009         klass.getDeclaredFieldsUnchecked(false, declaredFields);
1010 
1011         final ArrayList<Field> foundFields = new ArrayList<Field>();
1012         final int count = declaredFields.size();
1013         for (int i = 0; i < count; i++) {
1014             final Field field = declaredFields.get(i);
1015 
1016             // Ensure the field type can be resolved.
1017             try {
1018                 field.getType();
1019             } catch (NoClassDefFoundError e) {
1020                 continue;
1021             }
1022 
1023             if (field.isAnnotationPresent(ExportedProperty.class)) {
1024                 field.setAccessible(true);
1025                 foundFields.add(field);
1026                 sAnnotations.put(field, field.getAnnotation(ExportedProperty.class));
1027             }
1028         }
1029 
1030         fields = foundFields.toArray(new Field[foundFields.size()]);
1031         map.put(klass, fields);
1032 
1033         return fields;
1034     }
1035 
getExportedPropertyMethods(Class<?> klass)1036     private static Method[] getExportedPropertyMethods(Class<?> klass) {
1037         if (sMethodsForClasses == null) {
1038             sMethodsForClasses = new HashMap<Class<?>, Method[]>(100);
1039         }
1040         if (sAnnotations == null) {
1041             sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
1042         }
1043 
1044         final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
1045 
1046         Method[] methods = map.get(klass);
1047         if (methods != null) {
1048             return methods;
1049         }
1050 
1051         final ArrayList<Method> declaredMethods = new ArrayList();
1052         klass.getDeclaredMethodsUnchecked(false, declaredMethods);
1053 
1054         final ArrayList<Method> foundMethods = new ArrayList<Method>();
1055         final int count = declaredMethods.size();
1056         for (int i = 0; i < count; i++) {
1057             final Method method = declaredMethods.get(i);
1058 
1059             // Ensure the method return and parameter types can be resolved.
1060             try {
1061                 method.getReturnType();
1062                 method.getParameterTypes();
1063             } catch (NoClassDefFoundError e) {
1064                 continue;
1065             }
1066 
1067             if (method.getParameterTypes().length == 0 &&
1068                     method.isAnnotationPresent(ExportedProperty.class) &&
1069                     method.getReturnType() != Void.class) {
1070                 method.setAccessible(true);
1071                 foundMethods.add(method);
1072                 sAnnotations.put(method, method.getAnnotation(ExportedProperty.class));
1073             }
1074         }
1075 
1076         methods = foundMethods.toArray(new Method[foundMethods.size()]);
1077         map.put(klass, methods);
1078 
1079         return methods;
1080     }
1081 
dumpViewProperties(Context context, Object view, BufferedWriter out)1082     private static void dumpViewProperties(Context context, Object view,
1083             BufferedWriter out) throws IOException {
1084 
1085         dumpViewProperties(context, view, out, "");
1086     }
1087 
dumpViewProperties(Context context, Object view, BufferedWriter out, String prefix)1088     private static void dumpViewProperties(Context context, Object view,
1089             BufferedWriter out, String prefix) throws IOException {
1090 
1091         if (view == null) {
1092             out.write(prefix + "=4,null ");
1093             return;
1094         }
1095 
1096         Class<?> klass = view.getClass();
1097         do {
1098             exportFields(context, view, out, klass, prefix);
1099             exportMethods(context, view, out, klass, prefix);
1100             klass = klass.getSuperclass();
1101         } while (klass != Object.class);
1102     }
1103 
callMethodOnAppropriateTheadBlocking(final Method method, final Object object)1104     private static Object callMethodOnAppropriateTheadBlocking(final Method method,
1105             final Object object) throws IllegalAccessException, InvocationTargetException,
1106             TimeoutException {
1107         if (!(object instanceof View)) {
1108             return method.invoke(object, (Object[]) null);
1109         }
1110 
1111         final View view = (View) object;
1112         Callable<Object> callable = new Callable<Object>() {
1113             @Override
1114             public Object call() throws IllegalAccessException, InvocationTargetException {
1115                 return method.invoke(view, (Object[]) null);
1116             }
1117         };
1118         FutureTask<Object> future = new FutureTask<Object>(callable);
1119         // Try to use the handler provided by the view
1120         Handler handler = view.getHandler();
1121         // Fall back on using the main thread
1122         if (handler == null) {
1123             handler = new Handler(android.os.Looper.getMainLooper());
1124         }
1125         handler.post(future);
1126         while (true) {
1127             try {
1128                 return future.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
1129             } catch (ExecutionException e) {
1130                 Throwable t = e.getCause();
1131                 if (t instanceof IllegalAccessException) {
1132                     throw (IllegalAccessException)t;
1133                 }
1134                 if (t instanceof InvocationTargetException) {
1135                     throw (InvocationTargetException)t;
1136                 }
1137                 throw new RuntimeException("Unexpected exception", t);
1138             } catch (InterruptedException e) {
1139                 // Call get again
1140             } catch (CancellationException e) {
1141                 throw new RuntimeException("Unexpected cancellation exception", e);
1142             }
1143         }
1144     }
1145 
formatIntToHexString(int value)1146     private static String formatIntToHexString(int value) {
1147         return "0x" + Integer.toHexString(value).toUpperCase();
1148     }
1149 
exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)1150     private static void exportMethods(Context context, Object view, BufferedWriter out,
1151             Class<?> klass, String prefix) throws IOException {
1152 
1153         final Method[] methods = getExportedPropertyMethods(klass);
1154         int count = methods.length;
1155         for (int i = 0; i < count; i++) {
1156             final Method method = methods[i];
1157             //noinspection EmptyCatchBlock
1158             try {
1159                 Object methodValue = callMethodOnAppropriateTheadBlocking(method, view);
1160                 final Class<?> returnType = method.getReturnType();
1161                 final ExportedProperty property = sAnnotations.get(method);
1162                 String categoryPrefix =
1163                         property.category().length() != 0 ? property.category() + ":" : "";
1164 
1165                 if (returnType == int.class) {
1166                     if (property.resolveId() && context != null) {
1167                         final int id = (Integer) methodValue;
1168                         methodValue = resolveId(context, id);
1169                     } else {
1170                         final FlagToString[] flagsMapping = property.flagMapping();
1171                         if (flagsMapping.length > 0) {
1172                             final int intValue = (Integer) methodValue;
1173                             final String valuePrefix =
1174                                     categoryPrefix + prefix + method.getName() + '_';
1175                             exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1176                         }
1177 
1178                         final IntToString[] mapping = property.mapping();
1179                         if (mapping.length > 0) {
1180                             final int intValue = (Integer) methodValue;
1181                             boolean mapped = false;
1182                             int mappingCount = mapping.length;
1183                             for (int j = 0; j < mappingCount; j++) {
1184                                 final IntToString mapper = mapping[j];
1185                                 if (mapper.from() == intValue) {
1186                                     methodValue = mapper.to();
1187                                     mapped = true;
1188                                     break;
1189                                 }
1190                             }
1191 
1192                             if (!mapped) {
1193                                 methodValue = intValue;
1194                             }
1195                         }
1196                     }
1197                 } else if (returnType == int[].class) {
1198                     final int[] array = (int[]) methodValue;
1199                     final String valuePrefix = categoryPrefix + prefix + method.getName() + '_';
1200                     final String suffix = "()";
1201 
1202                     exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1203 
1204                     continue;
1205                 } else if (returnType == String[].class) {
1206                     final String[] array = (String[]) methodValue;
1207                     if (property.hasAdjacentMapping() && array != null) {
1208                         for (int j = 0; j < array.length; j += 2) {
1209                             if (array[j] != null) {
1210                                 writeEntry(out, categoryPrefix + prefix, array[j], "()",
1211                                         array[j + 1] == null ? "null" : array[j + 1]);
1212                             }
1213 
1214                         }
1215                     }
1216 
1217                     continue;
1218                 } else if (!returnType.isPrimitive()) {
1219                     if (property.deepExport()) {
1220                         dumpViewProperties(context, methodValue, out, prefix + property.prefix());
1221                         continue;
1222                     }
1223                 }
1224 
1225                 writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue);
1226             } catch (IllegalAccessException e) {
1227             } catch (InvocationTargetException e) {
1228             } catch (TimeoutException e) {
1229             }
1230         }
1231     }
1232 
exportFields(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)1233     private static void exportFields(Context context, Object view, BufferedWriter out,
1234             Class<?> klass, String prefix) throws IOException {
1235 
1236         final Field[] fields = getExportedPropertyFields(klass);
1237 
1238         int count = fields.length;
1239         for (int i = 0; i < count; i++) {
1240             final Field field = fields[i];
1241 
1242             //noinspection EmptyCatchBlock
1243             try {
1244                 Object fieldValue = null;
1245                 final Class<?> type = field.getType();
1246                 final ExportedProperty property = sAnnotations.get(field);
1247                 String categoryPrefix =
1248                         property.category().length() != 0 ? property.category() + ":" : "";
1249 
1250                 if (type == int.class || type == byte.class) {
1251                     if (property.resolveId() && context != null) {
1252                         final int id = field.getInt(view);
1253                         fieldValue = resolveId(context, id);
1254                     } else {
1255                         final FlagToString[] flagsMapping = property.flagMapping();
1256                         if (flagsMapping.length > 0) {
1257                             final int intValue = field.getInt(view);
1258                             final String valuePrefix =
1259                                     categoryPrefix + prefix + field.getName() + '_';
1260                             exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1261                         }
1262 
1263                         final IntToString[] mapping = property.mapping();
1264                         if (mapping.length > 0) {
1265                             final int intValue = field.getInt(view);
1266                             int mappingCount = mapping.length;
1267                             for (int j = 0; j < mappingCount; j++) {
1268                                 final IntToString mapped = mapping[j];
1269                                 if (mapped.from() == intValue) {
1270                                     fieldValue = mapped.to();
1271                                     break;
1272                                 }
1273                             }
1274 
1275                             if (fieldValue == null) {
1276                                 fieldValue = intValue;
1277                             }
1278                         }
1279 
1280                         if (property.formatToHexString()) {
1281                             fieldValue = field.get(view);
1282                             if (type == int.class) {
1283                                 fieldValue = formatIntToHexString((Integer) fieldValue);
1284                             } else if (type == byte.class) {
1285                                 fieldValue = "0x" + Byte.toHexString((Byte) fieldValue, true);
1286                             }
1287                         }
1288                     }
1289                 } else if (type == int[].class) {
1290                     final int[] array = (int[]) field.get(view);
1291                     final String valuePrefix = categoryPrefix + prefix + field.getName() + '_';
1292                     final String suffix = "";
1293 
1294                     exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1295 
1296                     continue;
1297                 } else if (type == String[].class) {
1298                     final String[] array = (String[]) field.get(view);
1299                     if (property.hasAdjacentMapping() && array != null) {
1300                         for (int j = 0; j < array.length; j += 2) {
1301                             if (array[j] != null) {
1302                                 writeEntry(out, categoryPrefix + prefix, array[j], "",
1303                                         array[j + 1] == null ? "null" : array[j + 1]);
1304                             }
1305                         }
1306                     }
1307 
1308                     continue;
1309                 } else if (!type.isPrimitive()) {
1310                     if (property.deepExport()) {
1311                         dumpViewProperties(context, field.get(view), out, prefix +
1312                                 property.prefix());
1313                         continue;
1314                     }
1315                 }
1316 
1317                 if (fieldValue == null) {
1318                     fieldValue = field.get(view);
1319                 }
1320 
1321                 writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue);
1322             } catch (IllegalAccessException e) {
1323             }
1324         }
1325     }
1326 
writeEntry(BufferedWriter out, String prefix, String name, String suffix, Object value)1327     private static void writeEntry(BufferedWriter out, String prefix, String name,
1328             String suffix, Object value) throws IOException {
1329 
1330         out.write(prefix);
1331         out.write(name);
1332         out.write(suffix);
1333         out.write("=");
1334         writeValue(out, value);
1335         out.write(' ');
1336     }
1337 
exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, int intValue, String prefix)1338     private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
1339             int intValue, String prefix) throws IOException {
1340 
1341         final int count = mapping.length;
1342         for (int j = 0; j < count; j++) {
1343             final FlagToString flagMapping = mapping[j];
1344             final boolean ifTrue = flagMapping.outputIf();
1345             final int maskResult = intValue & flagMapping.mask();
1346             final boolean test = maskResult == flagMapping.equals();
1347             if ((test && ifTrue) || (!test && !ifTrue)) {
1348                 final String name = flagMapping.name();
1349                 final String value = formatIntToHexString(maskResult);
1350                 writeEntry(out, prefix, name, "", value);
1351             }
1352         }
1353     }
1354 
exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix)1355     private static void exportUnrolledArray(Context context, BufferedWriter out,
1356             ExportedProperty property, int[] array, String prefix, String suffix)
1357             throws IOException {
1358 
1359         final IntToString[] indexMapping = property.indexMapping();
1360         final boolean hasIndexMapping = indexMapping.length > 0;
1361 
1362         final IntToString[] mapping = property.mapping();
1363         final boolean hasMapping = mapping.length > 0;
1364 
1365         final boolean resolveId = property.resolveId() && context != null;
1366         final int valuesCount = array.length;
1367 
1368         for (int j = 0; j < valuesCount; j++) {
1369             String name;
1370             String value = null;
1371 
1372             final int intValue = array[j];
1373 
1374             name = String.valueOf(j);
1375             if (hasIndexMapping) {
1376                 int mappingCount = indexMapping.length;
1377                 for (int k = 0; k < mappingCount; k++) {
1378                     final IntToString mapped = indexMapping[k];
1379                     if (mapped.from() == j) {
1380                         name = mapped.to();
1381                         break;
1382                     }
1383                 }
1384             }
1385 
1386             if (hasMapping) {
1387                 int mappingCount = mapping.length;
1388                 for (int k = 0; k < mappingCount; k++) {
1389                     final IntToString mapped = mapping[k];
1390                     if (mapped.from() == intValue) {
1391                         value = mapped.to();
1392                         break;
1393                     }
1394                 }
1395             }
1396 
1397             if (resolveId) {
1398                 if (value == null) value = (String) resolveId(context, intValue);
1399             } else {
1400                 value = String.valueOf(intValue);
1401             }
1402 
1403             writeEntry(out, prefix, name, suffix, value);
1404         }
1405     }
1406 
resolveId(Context context, int id)1407     static Object resolveId(Context context, int id) {
1408         Object fieldValue;
1409         final Resources resources = context.getResources();
1410         if (id >= 0) {
1411             try {
1412                 fieldValue = resources.getResourceTypeName(id) + '/' +
1413                         resources.getResourceEntryName(id);
1414             } catch (Resources.NotFoundException e) {
1415                 fieldValue = "id/" + formatIntToHexString(id);
1416             }
1417         } else {
1418             fieldValue = "NO_ID";
1419         }
1420         return fieldValue;
1421     }
1422 
writeValue(BufferedWriter out, Object value)1423     private static void writeValue(BufferedWriter out, Object value) throws IOException {
1424         if (value != null) {
1425             String output = "[EXCEPTION]";
1426             try {
1427                 output = value.toString().replace("\n", "\\n");
1428             } finally {
1429                 out.write(String.valueOf(output.length()));
1430                 out.write(",");
1431                 out.write(output);
1432             }
1433         } else {
1434             out.write("4,null");
1435         }
1436     }
1437 
capturedViewGetPropertyFields(Class<?> klass)1438     private static Field[] capturedViewGetPropertyFields(Class<?> klass) {
1439         if (mCapturedViewFieldsForClasses == null) {
1440             mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>();
1441         }
1442         final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses;
1443 
1444         Field[] fields = map.get(klass);
1445         if (fields != null) {
1446             return fields;
1447         }
1448 
1449         final ArrayList<Field> foundFields = new ArrayList<Field>();
1450         fields = klass.getFields();
1451 
1452         int count = fields.length;
1453         for (int i = 0; i < count; i++) {
1454             final Field field = fields[i];
1455             if (field.isAnnotationPresent(CapturedViewProperty.class)) {
1456                 field.setAccessible(true);
1457                 foundFields.add(field);
1458             }
1459         }
1460 
1461         fields = foundFields.toArray(new Field[foundFields.size()]);
1462         map.put(klass, fields);
1463 
1464         return fields;
1465     }
1466 
capturedViewGetPropertyMethods(Class<?> klass)1467     private static Method[] capturedViewGetPropertyMethods(Class<?> klass) {
1468         if (mCapturedViewMethodsForClasses == null) {
1469             mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>();
1470         }
1471         final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses;
1472 
1473         Method[] methods = map.get(klass);
1474         if (methods != null) {
1475             return methods;
1476         }
1477 
1478         final ArrayList<Method> foundMethods = new ArrayList<Method>();
1479         methods = klass.getMethods();
1480 
1481         int count = methods.length;
1482         for (int i = 0; i < count; i++) {
1483             final Method method = methods[i];
1484             if (method.getParameterTypes().length == 0 &&
1485                     method.isAnnotationPresent(CapturedViewProperty.class) &&
1486                     method.getReturnType() != Void.class) {
1487                 method.setAccessible(true);
1488                 foundMethods.add(method);
1489             }
1490         }
1491 
1492         methods = foundMethods.toArray(new Method[foundMethods.size()]);
1493         map.put(klass, methods);
1494 
1495         return methods;
1496     }
1497 
capturedViewExportMethods(Object obj, Class<?> klass, String prefix)1498     private static String capturedViewExportMethods(Object obj, Class<?> klass,
1499             String prefix) {
1500 
1501         if (obj == null) {
1502             return "null";
1503         }
1504 
1505         StringBuilder sb = new StringBuilder();
1506         final Method[] methods = capturedViewGetPropertyMethods(klass);
1507 
1508         int count = methods.length;
1509         for (int i = 0; i < count; i++) {
1510             final Method method = methods[i];
1511             try {
1512                 Object methodValue = method.invoke(obj, (Object[]) null);
1513                 final Class<?> returnType = method.getReturnType();
1514 
1515                 CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class);
1516                 if (property.retrieveReturn()) {
1517                     //we are interested in the second level data only
1518                     sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#"));
1519                 } else {
1520                     sb.append(prefix);
1521                     sb.append(method.getName());
1522                     sb.append("()=");
1523 
1524                     if (methodValue != null) {
1525                         final String value = methodValue.toString().replace("\n", "\\n");
1526                         sb.append(value);
1527                     } else {
1528                         sb.append("null");
1529                     }
1530                     sb.append("; ");
1531                 }
1532             } catch (IllegalAccessException e) {
1533                 //Exception IllegalAccess, it is OK here
1534                 //we simply ignore this method
1535             } catch (InvocationTargetException e) {
1536                 //Exception InvocationTarget, it is OK here
1537                 //we simply ignore this method
1538             }
1539         }
1540         return sb.toString();
1541     }
1542 
capturedViewExportFields(Object obj, Class<?> klass, String prefix)1543     private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) {
1544         if (obj == null) {
1545             return "null";
1546         }
1547 
1548         StringBuilder sb = new StringBuilder();
1549         final Field[] fields = capturedViewGetPropertyFields(klass);
1550 
1551         int count = fields.length;
1552         for (int i = 0; i < count; i++) {
1553             final Field field = fields[i];
1554             try {
1555                 Object fieldValue = field.get(obj);
1556 
1557                 sb.append(prefix);
1558                 sb.append(field.getName());
1559                 sb.append("=");
1560 
1561                 if (fieldValue != null) {
1562                     final String value = fieldValue.toString().replace("\n", "\\n");
1563                     sb.append(value);
1564                 } else {
1565                     sb.append("null");
1566                 }
1567                 sb.append(' ');
1568             } catch (IllegalAccessException e) {
1569                 //Exception IllegalAccess, it is OK here
1570                 //we simply ignore this field
1571             }
1572         }
1573         return sb.toString();
1574     }
1575 
1576     /**
1577      * Dump view info for id based instrument test generation
1578      * (and possibly further data analysis). The results are dumped
1579      * to the log.
1580      * @param tag for log
1581      * @param view for dump
1582      */
dumpCapturedView(String tag, Object view)1583     public static void dumpCapturedView(String tag, Object view) {
1584         Class<?> klass = view.getClass();
1585         StringBuilder sb = new StringBuilder(klass.getName() + ": ");
1586         sb.append(capturedViewExportFields(view, klass, ""));
1587         sb.append(capturedViewExportMethods(view, klass, ""));
1588         Log.d(tag, sb.toString());
1589     }
1590 
1591     /**
1592      * Invoke a particular method on given view.
1593      * The given method is always invoked on the UI thread. The caller thread will stall until the
1594      * method invocation is complete. Returns an object equal to the result of the method
1595      * invocation, null if the method is declared to return void
1596      * @throws Exception if the method invocation caused any exception
1597      * @hide
1598      */
invokeViewMethod(final View view, final Method method, final Object[] args)1599     public static Object invokeViewMethod(final View view, final Method method,
1600             final Object[] args) {
1601         final CountDownLatch latch = new CountDownLatch(1);
1602         final AtomicReference<Object> result = new AtomicReference<Object>();
1603         final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
1604 
1605         view.post(new Runnable() {
1606             @Override
1607             public void run() {
1608                 try {
1609                     result.set(method.invoke(view, args));
1610                 } catch (InvocationTargetException e) {
1611                     exception.set(e.getCause());
1612                 } catch (Exception e) {
1613                     exception.set(e);
1614                 }
1615 
1616                 latch.countDown();
1617             }
1618         });
1619 
1620         try {
1621             latch.await();
1622         } catch (InterruptedException e) {
1623             throw new RuntimeException(e);
1624         }
1625 
1626         if (exception.get() != null) {
1627             throw new RuntimeException(exception.get());
1628         }
1629 
1630         return result.get();
1631     }
1632 
1633     /**
1634      * @hide
1635      */
setLayoutParameter(final View view, final String param, final int value)1636     public static void setLayoutParameter(final View view, final String param, final int value)
1637             throws NoSuchFieldException, IllegalAccessException {
1638         final ViewGroup.LayoutParams p = view.getLayoutParams();
1639         final Field f = p.getClass().getField(param);
1640         if (f.getType() != int.class) {
1641             throw new RuntimeException("Only integer layout parameters can be set. Field "
1642                     + param + " is of type " + f.getType().getSimpleName());
1643         }
1644 
1645         f.set(p, Integer.valueOf(value));
1646 
1647         view.post(new Runnable() {
1648             @Override
1649             public void run() {
1650                 view.setLayoutParams(p);
1651             }
1652         });
1653     }
1654 }