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