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