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