• 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.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Bitmap;
22 import android.graphics.Canvas;
23 import android.graphics.Rect;
24 import android.os.Debug;
25 import android.os.RemoteException;
26 import android.util.DisplayMetrics;
27 import android.util.Log;
28 
29 import java.io.BufferedOutputStream;
30 import java.io.BufferedWriter;
31 import java.io.ByteArrayOutputStream;
32 import java.io.DataOutputStream;
33 import java.io.IOException;
34 import java.io.OutputStream;
35 import java.io.OutputStreamWriter;
36 import java.lang.annotation.ElementType;
37 import java.lang.annotation.Retention;
38 import java.lang.annotation.RetentionPolicy;
39 import java.lang.annotation.Target;
40 import java.lang.reflect.AccessibleObject;
41 import java.lang.reflect.Field;
42 import java.lang.reflect.InvocationTargetException;
43 import java.lang.reflect.Method;
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.concurrent.CountDownLatch;
47 import java.util.concurrent.TimeUnit;
48 
49 /**
50  * Various debugging/tracing tools related to {@link View} and the view hierarchy.
51  */
52 public class ViewDebug {
53     /**
54      * @deprecated This flag is now unused
55      */
56     @Deprecated
57     public static final boolean TRACE_HIERARCHY = false;
58 
59     /**
60      * @deprecated This flag is now unused
61      */
62     @Deprecated
63     public static final boolean TRACE_RECYCLER = false;
64 
65     /**
66      * Enables detailed logging of drag/drop operations.
67      * @hide
68      */
69     public static final boolean DEBUG_DRAG = false;
70 
71     /**
72      * This annotation can be used to mark fields and methods to be dumped by
73      * the view server. Only non-void methods with no arguments can be annotated
74      * by this annotation.
75      */
76     @Target({ ElementType.FIELD, ElementType.METHOD })
77     @Retention(RetentionPolicy.RUNTIME)
78     public @interface ExportedProperty {
79         /**
80          * When resolveId is true, and if the annotated field/method return value
81          * is an int, the value is converted to an Android's resource name.
82          *
83          * @return true if the property's value must be transformed into an Android
84          *         resource name, false otherwise
85          */
resolveId()86         boolean resolveId() default false;
87 
88         /**
89          * A mapping can be defined to map int values to specific strings. For
90          * instance, View.getVisibility() returns 0, 4 or 8. However, these values
91          * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
92          * these human readable values:
93          *
94          * <pre>
95          * @ViewDebug.ExportedProperty(mapping = {
96          *     @ViewDebug.IntToString(from = 0, to = "VISIBLE"),
97          *     @ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
98          *     @ViewDebug.IntToString(from = 8, to = "GONE")
99          * })
100          * public int getVisibility() { ...
101          * <pre>
102          *
103          * @return An array of int to String mappings
104          *
105          * @see android.view.ViewDebug.IntToString
106          */
mapping()107         IntToString[] mapping() default { };
108 
109         /**
110          * A mapping can be defined to map array indices to specific strings.
111          * A mapping can be used to see human readable values for the indices
112          * of an array:
113          *
114          * <pre>
115          * @ViewDebug.ExportedProperty(indexMapping = {
116          *     @ViewDebug.IntToString(from = 0, to = "INVALID"),
117          *     @ViewDebug.IntToString(from = 1, to = "FIRST"),
118          *     @ViewDebug.IntToString(from = 2, to = "SECOND")
119          * })
120          * private int[] mElements;
121          * <pre>
122          *
123          * @return An array of int to String mappings
124          *
125          * @see android.view.ViewDebug.IntToString
126          * @see #mapping()
127          */
indexMapping()128         IntToString[] indexMapping() default { };
129 
130         /**
131          * A flags mapping can be defined to map flags encoded in an integer to
132          * specific strings. A mapping can be used to see human readable values
133          * for the flags of an integer:
134          *
135          * <pre>
136          * @ViewDebug.ExportedProperty(flagMapping = {
137          *     @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED, name = "ENABLED"),
138          *     @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED, name = "DISABLED"),
139          * })
140          * private int mFlags;
141          * <pre>
142          *
143          * A specified String is output when the following is true:
144          *
145          * @return An array of int to String mappings
146          */
flagMapping()147         FlagToString[] flagMapping() default { };
148 
149         /**
150          * When deep export is turned on, this property is not dumped. Instead, the
151          * properties contained in this property are dumped. Each child property
152          * is prefixed with the name of this property.
153          *
154          * @return true if the properties of this property should be dumped
155          *
156          * @see #prefix()
157          */
deepExport()158         boolean deepExport() default false;
159 
160         /**
161          * The prefix to use on child properties when deep export is enabled
162          *
163          * @return a prefix as a String
164          *
165          * @see #deepExport()
166          */
prefix()167         String prefix() default "";
168 
169         /**
170          * Specifies the category the property falls into, such as measurement,
171          * layout, drawing, etc.
172          *
173          * @return the category as String
174          */
category()175         String category() default "";
176     }
177 
178     /**
179      * Defines a mapping from an int value to a String. Such a mapping can be used
180      * in an @ExportedProperty to provide more meaningful values to the end user.
181      *
182      * @see android.view.ViewDebug.ExportedProperty
183      */
184     @Target({ ElementType.TYPE })
185     @Retention(RetentionPolicy.RUNTIME)
186     public @interface IntToString {
187         /**
188          * The original int value to map to a String.
189          *
190          * @return An arbitrary int value.
191          */
from()192         int from();
193 
194         /**
195          * The String to use in place of the original int value.
196          *
197          * @return An arbitrary non-null String.
198          */
to()199         String to();
200     }
201 
202     /**
203      * Defines a mapping from a flag to a String. Such a mapping can be used
204      * in an @ExportedProperty to provide more meaningful values to the end user.
205      *
206      * @see android.view.ViewDebug.ExportedProperty
207      */
208     @Target({ ElementType.TYPE })
209     @Retention(RetentionPolicy.RUNTIME)
210     public @interface FlagToString {
211         /**
212          * The mask to apply to the original value.
213          *
214          * @return An arbitrary int value.
215          */
mask()216         int mask();
217 
218         /**
219          * The value to compare to the result of:
220          * <code>original value &amp; {@link #mask()}</code>.
221          *
222          * @return An arbitrary value.
223          */
equals()224         int equals();
225 
226         /**
227          * The String to use in place of the original int value.
228          *
229          * @return An arbitrary non-null String.
230          */
name()231         String name();
232 
233         /**
234          * Indicates whether to output the flag when the test is true,
235          * or false. Defaults to true.
236          */
outputIf()237         boolean outputIf() default true;
238     }
239 
240     /**
241      * This annotation can be used to mark fields and methods to be dumped when
242      * the view is captured. Methods with this annotation must have no arguments
243      * and must return a valid type of data.
244      */
245     @Target({ ElementType.FIELD, ElementType.METHOD })
246     @Retention(RetentionPolicy.RUNTIME)
247     public @interface CapturedViewProperty {
248         /**
249          * When retrieveReturn is true, we need to retrieve second level methods
250          * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
251          * we will set retrieveReturn = true on the annotation of
252          * myView.getFirstLevelMethod()
253          * @return true if we need the second level methods
254          */
retrieveReturn()255         boolean retrieveReturn() default false;
256     }
257 
258     private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null;
259     private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null;
260 
261     // Maximum delay in ms after which we stop trying to capture a View's drawing
262     private static final int CAPTURE_TIMEOUT = 4000;
263 
264     private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
265     private static final String REMOTE_COMMAND_DUMP = "DUMP";
266     private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
267     private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
268     private static final String REMOTE_PROFILE = "PROFILE";
269     private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS";
270     private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST";
271 
272     private static HashMap<Class<?>, Field[]> sFieldsForClasses;
273     private static HashMap<Class<?>, Method[]> sMethodsForClasses;
274     private static HashMap<AccessibleObject, ExportedProperty> sAnnotations;
275 
276     /**
277      * @deprecated This enum is now unused
278      */
279     @Deprecated
280     public enum HierarchyTraceType {
281         INVALIDATE,
282         INVALIDATE_CHILD,
283         INVALIDATE_CHILD_IN_PARENT,
284         REQUEST_LAYOUT,
285         ON_LAYOUT,
286         ON_MEASURE,
287         DRAW,
288         BUILD_CACHE
289     }
290 
291     /**
292      * @deprecated This enum is now unused
293      */
294     @Deprecated
295     public enum RecyclerTraceType {
296         NEW_VIEW,
297         BIND_VIEW,
298         RECYCLE_FROM_ACTIVE_HEAP,
299         RECYCLE_FROM_SCRAP_HEAP,
300         MOVE_TO_SCRAP_HEAP,
301         MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
302     }
303 
304     /**
305      * Returns the number of instanciated Views.
306      *
307      * @return The number of Views instanciated in the current process.
308      *
309      * @hide
310      */
getViewInstanceCount()311     public static long getViewInstanceCount() {
312         return Debug.countInstancesOfClass(View.class);
313     }
314 
315     /**
316      * Returns the number of instanciated ViewAncestors.
317      *
318      * @return The number of ViewAncestors instanciated in the current process.
319      *
320      * @hide
321      */
getViewRootImplCount()322     public static long getViewRootImplCount() {
323         return Debug.countInstancesOfClass(ViewRootImpl.class);
324     }
325 
326     /**
327      * @deprecated This method is now unused and invoking it is a no-op
328      */
329     @Deprecated
330     @SuppressWarnings({ "UnusedParameters", "deprecation" })
trace(View view, RecyclerTraceType type, int... parameters)331     public static void trace(View view, RecyclerTraceType type, int... parameters) {
332     }
333 
334     /**
335      * @deprecated This method is now unused and invoking it is a no-op
336      */
337     @Deprecated
338     @SuppressWarnings("UnusedParameters")
startRecyclerTracing(String prefix, View view)339     public static void startRecyclerTracing(String prefix, View view) {
340     }
341 
342     /**
343      * @deprecated This method is now unused and invoking it is a no-op
344      */
345     @Deprecated
346     @SuppressWarnings("UnusedParameters")
stopRecyclerTracing()347     public static void stopRecyclerTracing() {
348     }
349 
350     /**
351      * @deprecated This method is now unused and invoking it is a no-op
352      */
353     @Deprecated
354     @SuppressWarnings({ "UnusedParameters", "deprecation" })
trace(View view, HierarchyTraceType type)355     public static void trace(View view, HierarchyTraceType type) {
356     }
357 
358     /**
359      * @deprecated This method is now unused and invoking it is a no-op
360      */
361     @Deprecated
362     @SuppressWarnings("UnusedParameters")
startHierarchyTracing(String prefix, View view)363     public static void startHierarchyTracing(String prefix, View view) {
364     }
365 
366     /**
367      * @deprecated This method is now unused and invoking it is a no-op
368      */
369     @Deprecated
stopHierarchyTracing()370     public static void stopHierarchyTracing() {
371     }
372 
dispatchCommand(View view, String command, String parameters, OutputStream clientStream)373     static void dispatchCommand(View view, String command, String parameters,
374             OutputStream clientStream) throws IOException {
375 
376         // Paranoid but safe...
377         view = view.getRootView();
378 
379         if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
380             dump(view, clientStream);
381         } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) {
382             captureLayers(view, new DataOutputStream(clientStream));
383         } else {
384             final String[] params = parameters.split(" ");
385             if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
386                 capture(view, clientStream, params[0]);
387             } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) {
388                 outputDisplayList(view, params[0]);
389             } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
390                 invalidate(view, params[0]);
391             } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
392                 requestLayout(view, params[0]);
393             } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
394                 profile(view, clientStream, params[0]);
395             }
396         }
397     }
398 
findView(View root, String parameter)399     private static View findView(View root, String parameter) {
400         // Look by type/hashcode
401         if (parameter.indexOf('@') != -1) {
402             final String[] ids = parameter.split("@");
403             final String className = ids[0];
404             final int hashCode = (int) Long.parseLong(ids[1], 16);
405 
406             View view = root.getRootView();
407             if (view instanceof ViewGroup) {
408                 return findView((ViewGroup) view, className, hashCode);
409             }
410         } else {
411             // Look by id
412             final int id = root.getResources().getIdentifier(parameter, null, null);
413             return root.getRootView().findViewById(id);
414         }
415 
416         return null;
417     }
418 
invalidate(View root, String parameter)419     private static void invalidate(View root, String parameter) {
420         final View view = findView(root, parameter);
421         if (view != null) {
422             view.postInvalidate();
423         }
424     }
425 
requestLayout(View root, String parameter)426     private static void requestLayout(View root, String parameter) {
427         final View view = findView(root, parameter);
428         if (view != null) {
429             root.post(new Runnable() {
430                 public void run() {
431                     view.requestLayout();
432                 }
433             });
434         }
435     }
436 
profile(View root, OutputStream clientStream, String parameter)437     private static void profile(View root, OutputStream clientStream, String parameter)
438             throws IOException {
439 
440         final View view = findView(root, parameter);
441         BufferedWriter out = null;
442         try {
443             out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
444 
445             if (view != null) {
446                 profileViewAndChildren(view, out);
447             } else {
448                 out.write("-1 -1 -1");
449                 out.newLine();
450             }
451             out.write("DONE.");
452             out.newLine();
453         } catch (Exception e) {
454             android.util.Log.w("View", "Problem profiling the view:", e);
455         } finally {
456             if (out != null) {
457                 out.close();
458             }
459         }
460     }
461 
profileViewAndChildren(final View view, BufferedWriter out)462     private static void profileViewAndChildren(final View view, BufferedWriter out)
463             throws IOException {
464         profileViewAndChildren(view, out, true);
465     }
466 
profileViewAndChildren(final View view, BufferedWriter out, boolean root)467     private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root)
468             throws IOException {
469 
470         long durationMeasure =
471                 (root || (view.mPrivateFlags & View.MEASURED_DIMENSION_SET) != 0) ? profileViewOperation(
472                         view, new ViewOperation<Void>() {
473                             public Void[] pre() {
474                                 forceLayout(view);
475                                 return null;
476                             }
477 
478                             private void forceLayout(View view) {
479                                 view.forceLayout();
480                                 if (view instanceof ViewGroup) {
481                                     ViewGroup group = (ViewGroup) view;
482                                     final int count = group.getChildCount();
483                                     for (int i = 0; i < count; i++) {
484                                         forceLayout(group.getChildAt(i));
485                                     }
486                                 }
487                             }
488 
489                             public void run(Void... data) {
490                                 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
491                             }
492 
493                             public void post(Void... data) {
494                             }
495                         })
496                         : 0;
497         long durationLayout =
498                 (root || (view.mPrivateFlags & View.LAYOUT_REQUIRED) != 0) ? profileViewOperation(
499                         view, new ViewOperation<Void>() {
500                             public Void[] pre() {
501                                 return null;
502                             }
503 
504                             public void run(Void... data) {
505                                 view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom);
506                             }
507 
508                             public void post(Void... data) {
509                             }
510                         }) : 0;
511         long durationDraw =
512                 (root || !view.willNotDraw() || (view.mPrivateFlags & View.DRAWN) != 0) ? profileViewOperation(
513                         view,
514                         new ViewOperation<Object>() {
515                             public Object[] pre() {
516                                 final DisplayMetrics metrics =
517                                         (view != null && view.getResources() != null) ?
518                                                 view.getResources().getDisplayMetrics() : null;
519                                 final Bitmap bitmap = metrics != null ?
520                                         Bitmap.createBitmap(metrics.widthPixels,
521                                                 metrics.heightPixels, Bitmap.Config.RGB_565) : null;
522                                 final Canvas canvas = bitmap != null ? new Canvas(bitmap) : null;
523                                 return new Object[] {
524                                         bitmap, canvas
525                                 };
526                             }
527 
528                             public void run(Object... data) {
529                                 if (data[1] != null) {
530                                     view.draw((Canvas) data[1]);
531                                 }
532                             }
533 
534                             public void post(Object... data) {
535                                 if (data[1] != null) {
536                                     ((Canvas) data[1]).setBitmap(null);
537                                 }
538                                 if (data[0] != null) {
539                                     ((Bitmap) data[0]).recycle();
540                                 }
541                             }
542                         }) : 0;
543         out.write(String.valueOf(durationMeasure));
544         out.write(' ');
545         out.write(String.valueOf(durationLayout));
546         out.write(' ');
547         out.write(String.valueOf(durationDraw));
548         out.newLine();
549         if (view instanceof ViewGroup) {
550             ViewGroup group = (ViewGroup) view;
551             final int count = group.getChildCount();
552             for (int i = 0; i < count; i++) {
553                 profileViewAndChildren(group.getChildAt(i), out, false);
554             }
555         }
556     }
557 
558     interface ViewOperation<T> {
pre()559         T[] pre();
run(T... data)560         void run(T... data);
post(T... data)561         void post(T... data);
562     }
563 
profileViewOperation(View view, final ViewOperation<T> operation)564     private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) {
565         final CountDownLatch latch = new CountDownLatch(1);
566         final long[] duration = new long[1];
567 
568         view.post(new Runnable() {
569             public void run() {
570                 try {
571                     T[] data = operation.pre();
572                     long start = Debug.threadCpuTimeNanos();
573                     //noinspection unchecked
574                     operation.run(data);
575                     duration[0] = Debug.threadCpuTimeNanos() - start;
576                     //noinspection unchecked
577                     operation.post(data);
578                 } finally {
579                     latch.countDown();
580                 }
581             }
582         });
583 
584         try {
585             if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) {
586                 Log.w("View", "Could not complete the profiling of the view " + view);
587                 return -1;
588             }
589         } catch (InterruptedException e) {
590             Log.w("View", "Could not complete the profiling of the view " + view);
591             Thread.currentThread().interrupt();
592             return -1;
593         }
594 
595         return duration[0];
596     }
597 
captureLayers(View root, final DataOutputStream clientStream)598     private static void captureLayers(View root, final DataOutputStream clientStream)
599             throws IOException {
600 
601         try {
602             Rect outRect = new Rect();
603             try {
604                 root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect);
605             } catch (RemoteException e) {
606                 // Ignore
607             }
608 
609             clientStream.writeInt(outRect.width());
610             clientStream.writeInt(outRect.height());
611 
612             captureViewLayer(root, clientStream, true);
613 
614             clientStream.write(2);
615         } finally {
616             clientStream.close();
617         }
618     }
619 
captureViewLayer(View view, DataOutputStream clientStream, boolean visible)620     private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
621             throws IOException {
622 
623         final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
624 
625         if ((view.mPrivateFlags & View.SKIP_DRAW) != View.SKIP_DRAW) {
626             final int id = view.getId();
627             String name = view.getClass().getSimpleName();
628             if (id != View.NO_ID) {
629                 name = resolveId(view.getContext(), id).toString();
630             }
631 
632             clientStream.write(1);
633             clientStream.writeUTF(name);
634             clientStream.writeByte(localVisible ? 1 : 0);
635 
636             int[] position = new int[2];
637             // XXX: Should happen on the UI thread
638             view.getLocationInWindow(position);
639 
640             clientStream.writeInt(position[0]);
641             clientStream.writeInt(position[1]);
642             clientStream.flush();
643 
644             Bitmap b = performViewCapture(view, true);
645             if (b != null) {
646                 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
647                         b.getHeight() * 2);
648                 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
649                 clientStream.writeInt(arrayOut.size());
650                 arrayOut.writeTo(clientStream);
651             }
652             clientStream.flush();
653         }
654 
655         if (view instanceof ViewGroup) {
656             ViewGroup group = (ViewGroup) view;
657             int count = group.getChildCount();
658 
659             for (int i = 0; i < count; i++) {
660                 captureViewLayer(group.getChildAt(i), clientStream, localVisible);
661             }
662         }
663     }
664 
outputDisplayList(View root, String parameter)665     private static void outputDisplayList(View root, String parameter) throws IOException {
666         final View view = findView(root, parameter);
667         view.getViewRootImpl().outputDisplayList(view);
668     }
669 
capture(View root, final OutputStream clientStream, String parameter)670     private static void capture(View root, final OutputStream clientStream, String parameter)
671             throws IOException {
672 
673         final View captureView = findView(root, parameter);
674         Bitmap b = performViewCapture(captureView, false);
675 
676         if (b == null) {
677             Log.w("View", "Failed to create capture bitmap!");
678             // Send an empty one so that it doesn't get stuck waiting for
679             // something.
680             b = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
681         }
682 
683         BufferedOutputStream out = null;
684         try {
685             out = new BufferedOutputStream(clientStream, 32 * 1024);
686             b.compress(Bitmap.CompressFormat.PNG, 100, out);
687             out.flush();
688         } finally {
689             if (out != null) {
690                 out.close();
691             }
692             b.recycle();
693         }
694     }
695 
performViewCapture(final View captureView, final boolean skpiChildren)696     private static Bitmap performViewCapture(final View captureView, final boolean skpiChildren) {
697         if (captureView != null) {
698             final CountDownLatch latch = new CountDownLatch(1);
699             final Bitmap[] cache = new Bitmap[1];
700 
701             captureView.post(new Runnable() {
702                 public void run() {
703                     try {
704                         cache[0] = captureView.createSnapshot(
705                                 Bitmap.Config.ARGB_8888, 0, skpiChildren);
706                     } catch (OutOfMemoryError e) {
707                         Log.w("View", "Out of memory for bitmap");
708                     } finally {
709                         latch.countDown();
710                     }
711                 }
712             });
713 
714             try {
715                 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
716                 return cache[0];
717             } catch (InterruptedException e) {
718                 Log.w("View", "Could not complete the capture of the view " + captureView);
719                 Thread.currentThread().interrupt();
720             }
721         }
722 
723         return null;
724     }
725 
dump(View root, OutputStream clientStream)726     private static void dump(View root, OutputStream clientStream) throws IOException {
727         BufferedWriter out = null;
728         try {
729             out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
730             View view = root.getRootView();
731             if (view instanceof ViewGroup) {
732                 ViewGroup group = (ViewGroup) view;
733                 dumpViewHierarchyWithProperties(group.getContext(), group, out, 0);
734             }
735             out.write("DONE.");
736             out.newLine();
737         } catch (Exception e) {
738             android.util.Log.w("View", "Problem dumping the view:", e);
739         } finally {
740             if (out != null) {
741                 out.close();
742             }
743         }
744     }
745 
findView(ViewGroup group, String className, int hashCode)746     private static View findView(ViewGroup group, String className, int hashCode) {
747         if (isRequestedView(group, className, hashCode)) {
748             return group;
749         }
750 
751         final int count = group.getChildCount();
752         for (int i = 0; i < count; i++) {
753             final View view = group.getChildAt(i);
754             if (view instanceof ViewGroup) {
755                 final View found = findView((ViewGroup) view, className, hashCode);
756                 if (found != null) {
757                     return found;
758                 }
759             } else if (isRequestedView(view, className, hashCode)) {
760                 return view;
761             }
762         }
763 
764         return null;
765     }
766 
isRequestedView(View view, String className, int hashCode)767     private static boolean isRequestedView(View view, String className, int hashCode) {
768         return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
769     }
770 
dumpViewHierarchyWithProperties(Context context, ViewGroup group, BufferedWriter out, int level)771     private static void dumpViewHierarchyWithProperties(Context context, ViewGroup group,
772             BufferedWriter out, int level) {
773         if (!dumpViewWithProperties(context, group, out, level)) {
774             return;
775         }
776 
777         final int count = group.getChildCount();
778         for (int i = 0; i < count; i++) {
779             final View view = group.getChildAt(i);
780             if (view instanceof ViewGroup) {
781                 dumpViewHierarchyWithProperties(context, (ViewGroup) view, out, level + 1);
782             } else {
783                 dumpViewWithProperties(context, view, out, level + 1);
784             }
785         }
786     }
787 
dumpViewWithProperties(Context context, View view, BufferedWriter out, int level)788     private static boolean dumpViewWithProperties(Context context, View view,
789             BufferedWriter out, int level) {
790 
791         try {
792             for (int i = 0; i < level; i++) {
793                 out.write(' ');
794             }
795             out.write(view.getClass().getName());
796             out.write('@');
797             out.write(Integer.toHexString(view.hashCode()));
798             out.write(' ');
799             dumpViewProperties(context, view, out);
800             out.newLine();
801         } catch (IOException e) {
802             Log.w("View", "Error while dumping hierarchy tree");
803             return false;
804         }
805         return true;
806     }
807 
getExportedPropertyFields(Class<?> klass)808     private static Field[] getExportedPropertyFields(Class<?> klass) {
809         if (sFieldsForClasses == null) {
810             sFieldsForClasses = new HashMap<Class<?>, Field[]>();
811         }
812         if (sAnnotations == null) {
813             sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
814         }
815 
816         final HashMap<Class<?>, Field[]> map = sFieldsForClasses;
817 
818         Field[] fields = map.get(klass);
819         if (fields != null) {
820             return fields;
821         }
822 
823         final ArrayList<Field> foundFields = new ArrayList<Field>();
824         fields = klass.getDeclaredFields();
825 
826         int count = fields.length;
827         for (int i = 0; i < count; i++) {
828             final Field field = fields[i];
829             if (field.isAnnotationPresent(ExportedProperty.class)) {
830                 field.setAccessible(true);
831                 foundFields.add(field);
832                 sAnnotations.put(field, field.getAnnotation(ExportedProperty.class));
833             }
834         }
835 
836         fields = foundFields.toArray(new Field[foundFields.size()]);
837         map.put(klass, fields);
838 
839         return fields;
840     }
841 
getExportedPropertyMethods(Class<?> klass)842     private static Method[] getExportedPropertyMethods(Class<?> klass) {
843         if (sMethodsForClasses == null) {
844             sMethodsForClasses = new HashMap<Class<?>, Method[]>(100);
845         }
846         if (sAnnotations == null) {
847             sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
848         }
849 
850         final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
851 
852         Method[] methods = map.get(klass);
853         if (methods != null) {
854             return methods;
855         }
856 
857         final ArrayList<Method> foundMethods = new ArrayList<Method>();
858         methods = klass.getDeclaredMethods();
859 
860         int count = methods.length;
861         for (int i = 0; i < count; i++) {
862             final Method method = methods[i];
863             if (method.getParameterTypes().length == 0 &&
864                     method.isAnnotationPresent(ExportedProperty.class) &&
865                     method.getReturnType() != Void.class) {
866                 method.setAccessible(true);
867                 foundMethods.add(method);
868                 sAnnotations.put(method, method.getAnnotation(ExportedProperty.class));
869             }
870         }
871 
872         methods = foundMethods.toArray(new Method[foundMethods.size()]);
873         map.put(klass, methods);
874 
875         return methods;
876     }
877 
dumpViewProperties(Context context, Object view, BufferedWriter out)878     private static void dumpViewProperties(Context context, Object view,
879             BufferedWriter out) throws IOException {
880 
881         dumpViewProperties(context, view, out, "");
882     }
883 
dumpViewProperties(Context context, Object view, BufferedWriter out, String prefix)884     private static void dumpViewProperties(Context context, Object view,
885             BufferedWriter out, String prefix) throws IOException {
886 
887         Class<?> klass = view.getClass();
888 
889         do {
890             exportFields(context, view, out, klass, prefix);
891             exportMethods(context, view, out, klass, prefix);
892             klass = klass.getSuperclass();
893         } while (klass != Object.class);
894     }
895 
exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)896     private static void exportMethods(Context context, Object view, BufferedWriter out,
897             Class<?> klass, String prefix) throws IOException {
898 
899         final Method[] methods = getExportedPropertyMethods(klass);
900 
901         int count = methods.length;
902         for (int i = 0; i < count; i++) {
903             final Method method = methods[i];
904             //noinspection EmptyCatchBlock
905             try {
906                 // TODO: This should happen on the UI thread
907                 Object methodValue = method.invoke(view, (Object[]) null);
908                 final Class<?> returnType = method.getReturnType();
909                 final ExportedProperty property = sAnnotations.get(method);
910                 String categoryPrefix =
911                         property.category().length() != 0 ? property.category() + ":" : "";
912 
913                 if (returnType == int.class) {
914 
915                     if (property.resolveId() && context != null) {
916                         final int id = (Integer) methodValue;
917                         methodValue = resolveId(context, id);
918                     } else {
919                         final FlagToString[] flagsMapping = property.flagMapping();
920                         if (flagsMapping.length > 0) {
921                             final int intValue = (Integer) methodValue;
922                             final String valuePrefix =
923                                     categoryPrefix + prefix + method.getName() + '_';
924                             exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
925                         }
926 
927                         final IntToString[] mapping = property.mapping();
928                         if (mapping.length > 0) {
929                             final int intValue = (Integer) methodValue;
930                             boolean mapped = false;
931                             int mappingCount = mapping.length;
932                             for (int j = 0; j < mappingCount; j++) {
933                                 final IntToString mapper = mapping[j];
934                                 if (mapper.from() == intValue) {
935                                     methodValue = mapper.to();
936                                     mapped = true;
937                                     break;
938                                 }
939                             }
940 
941                             if (!mapped) {
942                                 methodValue = intValue;
943                             }
944                         }
945                     }
946                 } else if (returnType == int[].class) {
947                     final int[] array = (int[]) methodValue;
948                     final String valuePrefix = categoryPrefix + prefix + method.getName() + '_';
949                     final String suffix = "()";
950 
951                     exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
952 
953                     // Probably want to return here, same as for fields.
954                     return;
955                 } else if (!returnType.isPrimitive()) {
956                     if (property.deepExport()) {
957                         dumpViewProperties(context, methodValue, out, prefix + property.prefix());
958                         continue;
959                     }
960                 }
961 
962                 writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue);
963             } catch (IllegalAccessException e) {
964             } catch (InvocationTargetException e) {
965             }
966         }
967     }
968 
exportFields(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)969     private static void exportFields(Context context, Object view, BufferedWriter out,
970             Class<?> klass, String prefix) throws IOException {
971 
972         final Field[] fields = getExportedPropertyFields(klass);
973 
974         int count = fields.length;
975         for (int i = 0; i < count; i++) {
976             final Field field = fields[i];
977 
978             //noinspection EmptyCatchBlock
979             try {
980                 Object fieldValue = null;
981                 final Class<?> type = field.getType();
982                 final ExportedProperty property = sAnnotations.get(field);
983                 String categoryPrefix =
984                         property.category().length() != 0 ? property.category() + ":" : "";
985 
986                 if (type == int.class) {
987 
988                     if (property.resolveId() && context != null) {
989                         final int id = field.getInt(view);
990                         fieldValue = resolveId(context, id);
991                     } else {
992                         final FlagToString[] flagsMapping = property.flagMapping();
993                         if (flagsMapping.length > 0) {
994                             final int intValue = field.getInt(view);
995                             final String valuePrefix =
996                                     categoryPrefix + prefix + field.getName() + '_';
997                             exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
998                         }
999 
1000                         final IntToString[] mapping = property.mapping();
1001                         if (mapping.length > 0) {
1002                             final int intValue = field.getInt(view);
1003                             int mappingCount = mapping.length;
1004                             for (int j = 0; j < mappingCount; j++) {
1005                                 final IntToString mapped = mapping[j];
1006                                 if (mapped.from() == intValue) {
1007                                     fieldValue = mapped.to();
1008                                     break;
1009                                 }
1010                             }
1011 
1012                             if (fieldValue == null) {
1013                                 fieldValue = intValue;
1014                             }
1015                         }
1016                     }
1017                 } else if (type == int[].class) {
1018                     final int[] array = (int[]) field.get(view);
1019                     final String valuePrefix = categoryPrefix + prefix + field.getName() + '_';
1020                     final String suffix = "";
1021 
1022                     exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1023 
1024                     // We exit here!
1025                     return;
1026                 } else if (!type.isPrimitive()) {
1027                     if (property.deepExport()) {
1028                         dumpViewProperties(context, field.get(view), out, prefix
1029                                 + property.prefix());
1030                         continue;
1031                     }
1032                 }
1033 
1034                 if (fieldValue == null) {
1035                     fieldValue = field.get(view);
1036                 }
1037 
1038                 writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue);
1039             } catch (IllegalAccessException e) {
1040             }
1041         }
1042     }
1043 
writeEntry(BufferedWriter out, String prefix, String name, String suffix, Object value)1044     private static void writeEntry(BufferedWriter out, String prefix, String name,
1045             String suffix, Object value) throws IOException {
1046 
1047         out.write(prefix);
1048         out.write(name);
1049         out.write(suffix);
1050         out.write("=");
1051         writeValue(out, value);
1052         out.write(' ');
1053     }
1054 
exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, int intValue, String prefix)1055     private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
1056             int intValue, String prefix) throws IOException {
1057 
1058         final int count = mapping.length;
1059         for (int j = 0; j < count; j++) {
1060             final FlagToString flagMapping = mapping[j];
1061             final boolean ifTrue = flagMapping.outputIf();
1062             final int maskResult = intValue & flagMapping.mask();
1063             final boolean test = maskResult == flagMapping.equals();
1064             if ((test && ifTrue) || (!test && !ifTrue)) {
1065                 final String name = flagMapping.name();
1066                 final String value = "0x" + Integer.toHexString(maskResult);
1067                 writeEntry(out, prefix, name, "", value);
1068             }
1069         }
1070     }
1071 
exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix)1072     private static void exportUnrolledArray(Context context, BufferedWriter out,
1073             ExportedProperty property, int[] array, String prefix, String suffix)
1074             throws IOException {
1075 
1076         final IntToString[] indexMapping = property.indexMapping();
1077         final boolean hasIndexMapping = indexMapping.length > 0;
1078 
1079         final IntToString[] mapping = property.mapping();
1080         final boolean hasMapping = mapping.length > 0;
1081 
1082         final boolean resolveId = property.resolveId() && context != null;
1083         final int valuesCount = array.length;
1084 
1085         for (int j = 0; j < valuesCount; j++) {
1086             String name;
1087             String value = null;
1088 
1089             final int intValue = array[j];
1090 
1091             name = String.valueOf(j);
1092             if (hasIndexMapping) {
1093                 int mappingCount = indexMapping.length;
1094                 for (int k = 0; k < mappingCount; k++) {
1095                     final IntToString mapped = indexMapping[k];
1096                     if (mapped.from() == j) {
1097                         name = mapped.to();
1098                         break;
1099                     }
1100                 }
1101             }
1102 
1103             if (hasMapping) {
1104                 int mappingCount = mapping.length;
1105                 for (int k = 0; k < mappingCount; k++) {
1106                     final IntToString mapped = mapping[k];
1107                     if (mapped.from() == intValue) {
1108                         value = mapped.to();
1109                         break;
1110                     }
1111                 }
1112             }
1113 
1114             if (resolveId) {
1115                 if (value == null) value = (String) resolveId(context, intValue);
1116             } else {
1117                 value = String.valueOf(intValue);
1118             }
1119 
1120             writeEntry(out, prefix, name, suffix, value);
1121         }
1122     }
1123 
resolveId(Context context, int id)1124     static Object resolveId(Context context, int id) {
1125         Object fieldValue;
1126         final Resources resources = context.getResources();
1127         if (id >= 0) {
1128             try {
1129                 fieldValue = resources.getResourceTypeName(id) + '/' +
1130                         resources.getResourceEntryName(id);
1131             } catch (Resources.NotFoundException e) {
1132                 fieldValue = "id/0x" + Integer.toHexString(id);
1133             }
1134         } else {
1135             fieldValue = "NO_ID";
1136         }
1137         return fieldValue;
1138     }
1139 
writeValue(BufferedWriter out, Object value)1140     private static void writeValue(BufferedWriter out, Object value) throws IOException {
1141         if (value != null) {
1142             String output = value.toString().replace("\n", "\\n");
1143             out.write(String.valueOf(output.length()));
1144             out.write(",");
1145             out.write(output);
1146         } else {
1147             out.write("4,null");
1148         }
1149     }
1150 
capturedViewGetPropertyFields(Class<?> klass)1151     private static Field[] capturedViewGetPropertyFields(Class<?> klass) {
1152         if (mCapturedViewFieldsForClasses == null) {
1153             mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>();
1154         }
1155         final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses;
1156 
1157         Field[] fields = map.get(klass);
1158         if (fields != null) {
1159             return fields;
1160         }
1161 
1162         final ArrayList<Field> foundFields = new ArrayList<Field>();
1163         fields = klass.getFields();
1164 
1165         int count = fields.length;
1166         for (int i = 0; i < count; i++) {
1167             final Field field = fields[i];
1168             if (field.isAnnotationPresent(CapturedViewProperty.class)) {
1169                 field.setAccessible(true);
1170                 foundFields.add(field);
1171             }
1172         }
1173 
1174         fields = foundFields.toArray(new Field[foundFields.size()]);
1175         map.put(klass, fields);
1176 
1177         return fields;
1178     }
1179 
capturedViewGetPropertyMethods(Class<?> klass)1180     private static Method[] capturedViewGetPropertyMethods(Class<?> klass) {
1181         if (mCapturedViewMethodsForClasses == null) {
1182             mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>();
1183         }
1184         final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses;
1185 
1186         Method[] methods = map.get(klass);
1187         if (methods != null) {
1188             return methods;
1189         }
1190 
1191         final ArrayList<Method> foundMethods = new ArrayList<Method>();
1192         methods = klass.getMethods();
1193 
1194         int count = methods.length;
1195         for (int i = 0; i < count; i++) {
1196             final Method method = methods[i];
1197             if (method.getParameterTypes().length == 0 &&
1198                     method.isAnnotationPresent(CapturedViewProperty.class) &&
1199                     method.getReturnType() != Void.class) {
1200                 method.setAccessible(true);
1201                 foundMethods.add(method);
1202             }
1203         }
1204 
1205         methods = foundMethods.toArray(new Method[foundMethods.size()]);
1206         map.put(klass, methods);
1207 
1208         return methods;
1209     }
1210 
capturedViewExportMethods(Object obj, Class<?> klass, String prefix)1211     private static String capturedViewExportMethods(Object obj, Class<?> klass,
1212             String prefix) {
1213 
1214         if (obj == null) {
1215             return "null";
1216         }
1217 
1218         StringBuilder sb = new StringBuilder();
1219         final Method[] methods = capturedViewGetPropertyMethods(klass);
1220 
1221         int count = methods.length;
1222         for (int i = 0; i < count; i++) {
1223             final Method method = methods[i];
1224             try {
1225                 Object methodValue = method.invoke(obj, (Object[]) null);
1226                 final Class<?> returnType = method.getReturnType();
1227 
1228                 CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class);
1229                 if (property.retrieveReturn()) {
1230                     //we are interested in the second level data only
1231                     sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#"));
1232                 } else {
1233                     sb.append(prefix);
1234                     sb.append(method.getName());
1235                     sb.append("()=");
1236 
1237                     if (methodValue != null) {
1238                         final String value = methodValue.toString().replace("\n", "\\n");
1239                         sb.append(value);
1240                     } else {
1241                         sb.append("null");
1242                     }
1243                     sb.append("; ");
1244                 }
1245               } catch (IllegalAccessException e) {
1246                   //Exception IllegalAccess, it is OK here
1247                   //we simply ignore this method
1248               } catch (InvocationTargetException e) {
1249                   //Exception InvocationTarget, it is OK here
1250                   //we simply ignore this method
1251               }
1252         }
1253         return sb.toString();
1254     }
1255 
capturedViewExportFields(Object obj, Class<?> klass, String prefix)1256     private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) {
1257         if (obj == null) {
1258             return "null";
1259         }
1260 
1261         StringBuilder sb = new StringBuilder();
1262         final Field[] fields = capturedViewGetPropertyFields(klass);
1263 
1264         int count = fields.length;
1265         for (int i = 0; i < count; i++) {
1266             final Field field = fields[i];
1267             try {
1268                 Object fieldValue = field.get(obj);
1269 
1270                 sb.append(prefix);
1271                 sb.append(field.getName());
1272                 sb.append("=");
1273 
1274                 if (fieldValue != null) {
1275                     final String value = fieldValue.toString().replace("\n", "\\n");
1276                     sb.append(value);
1277                 } else {
1278                     sb.append("null");
1279                 }
1280                 sb.append(' ');
1281             } catch (IllegalAccessException e) {
1282                 //Exception IllegalAccess, it is OK here
1283                 //we simply ignore this field
1284             }
1285         }
1286         return sb.toString();
1287     }
1288 
1289     /**
1290      * Dump view info for id based instrument test generation
1291      * (and possibly further data analysis). The results are dumped
1292      * to the log.
1293      * @param tag for log
1294      * @param view for dump
1295      */
dumpCapturedView(String tag, Object view)1296     public static void dumpCapturedView(String tag, Object view) {
1297         Class<?> klass = view.getClass();
1298         StringBuilder sb = new StringBuilder(klass.getName() + ": ");
1299         sb.append(capturedViewExportFields(view, klass, ""));
1300         sb.append(capturedViewExportMethods(view, klass, ""));
1301         Log.d(tag, sb.toString());
1302     }
1303 }
1304