• 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     /**
259      * Allows a View to inject custom children into HierarchyViewer. For example,
260      * WebView uses this to add its internal layer tree as a child to itself
261      * @hide
262      */
263     public interface HierarchyHandler {
264         /**
265          * Dumps custom children to hierarchy viewer.
266          * See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int)
267          * for the format
268          *
269          * An empty implementation should simply do nothing
270          *
271          * @param out The output writer
272          * @param level The indentation level
273          */
dumpViewHierarchyWithProperties(BufferedWriter out, int level)274         public void dumpViewHierarchyWithProperties(BufferedWriter out, int level);
275 
276         /**
277          * Returns a View to enable grabbing screenshots from custom children
278          * returned in dumpViewHierarchyWithProperties.
279          *
280          * @param className The className of the view to find
281          * @param hashCode The hashCode of the view to find
282          * @return the View to capture from, or null if not found
283          */
findHierarchyView(String className, int hashCode)284         public View findHierarchyView(String className, int hashCode);
285     }
286 
287     private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null;
288     private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null;
289 
290     // Maximum delay in ms after which we stop trying to capture a View's drawing
291     private static final int CAPTURE_TIMEOUT = 4000;
292 
293     private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
294     private static final String REMOTE_COMMAND_DUMP = "DUMP";
295     private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
296     private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
297     private static final String REMOTE_PROFILE = "PROFILE";
298     private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS";
299     private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST";
300 
301     private static HashMap<Class<?>, Field[]> sFieldsForClasses;
302     private static HashMap<Class<?>, Method[]> sMethodsForClasses;
303     private static HashMap<AccessibleObject, ExportedProperty> sAnnotations;
304 
305     /**
306      * @deprecated This enum is now unused
307      */
308     @Deprecated
309     public enum HierarchyTraceType {
310         INVALIDATE,
311         INVALIDATE_CHILD,
312         INVALIDATE_CHILD_IN_PARENT,
313         REQUEST_LAYOUT,
314         ON_LAYOUT,
315         ON_MEASURE,
316         DRAW,
317         BUILD_CACHE
318     }
319 
320     /**
321      * @deprecated This enum is now unused
322      */
323     @Deprecated
324     public enum RecyclerTraceType {
325         NEW_VIEW,
326         BIND_VIEW,
327         RECYCLE_FROM_ACTIVE_HEAP,
328         RECYCLE_FROM_SCRAP_HEAP,
329         MOVE_TO_SCRAP_HEAP,
330         MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
331     }
332 
333     /**
334      * Returns the number of instanciated Views.
335      *
336      * @return The number of Views instanciated in the current process.
337      *
338      * @hide
339      */
getViewInstanceCount()340     public static long getViewInstanceCount() {
341         return Debug.countInstancesOfClass(View.class);
342     }
343 
344     /**
345      * Returns the number of instanciated ViewAncestors.
346      *
347      * @return The number of ViewAncestors instanciated in the current process.
348      *
349      * @hide
350      */
getViewRootImplCount()351     public static long getViewRootImplCount() {
352         return Debug.countInstancesOfClass(ViewRootImpl.class);
353     }
354 
355     /**
356      * @deprecated This method is now unused and invoking it is a no-op
357      */
358     @Deprecated
359     @SuppressWarnings({ "UnusedParameters", "deprecation" })
trace(View view, RecyclerTraceType type, int... parameters)360     public static void trace(View view, RecyclerTraceType type, int... parameters) {
361     }
362 
363     /**
364      * @deprecated This method is now unused and invoking it is a no-op
365      */
366     @Deprecated
367     @SuppressWarnings("UnusedParameters")
startRecyclerTracing(String prefix, View view)368     public static void startRecyclerTracing(String prefix, View view) {
369     }
370 
371     /**
372      * @deprecated This method is now unused and invoking it is a no-op
373      */
374     @Deprecated
375     @SuppressWarnings("UnusedParameters")
stopRecyclerTracing()376     public static void stopRecyclerTracing() {
377     }
378 
379     /**
380      * @deprecated This method is now unused and invoking it is a no-op
381      */
382     @Deprecated
383     @SuppressWarnings({ "UnusedParameters", "deprecation" })
trace(View view, HierarchyTraceType type)384     public static void trace(View view, HierarchyTraceType type) {
385     }
386 
387     /**
388      * @deprecated This method is now unused and invoking it is a no-op
389      */
390     @Deprecated
391     @SuppressWarnings("UnusedParameters")
startHierarchyTracing(String prefix, View view)392     public static void startHierarchyTracing(String prefix, View view) {
393     }
394 
395     /**
396      * @deprecated This method is now unused and invoking it is a no-op
397      */
398     @Deprecated
stopHierarchyTracing()399     public static void stopHierarchyTracing() {
400     }
401 
dispatchCommand(View view, String command, String parameters, OutputStream clientStream)402     static void dispatchCommand(View view, String command, String parameters,
403             OutputStream clientStream) throws IOException {
404 
405         // Paranoid but safe...
406         view = view.getRootView();
407 
408         if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
409             dump(view, clientStream);
410         } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) {
411             captureLayers(view, new DataOutputStream(clientStream));
412         } else {
413             final String[] params = parameters.split(" ");
414             if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
415                 capture(view, clientStream, params[0]);
416             } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) {
417                 outputDisplayList(view, params[0]);
418             } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
419                 invalidate(view, params[0]);
420             } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
421                 requestLayout(view, params[0]);
422             } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
423                 profile(view, clientStream, params[0]);
424             }
425         }
426     }
427 
findView(View root, String parameter)428     private static View findView(View root, String parameter) {
429         // Look by type/hashcode
430         if (parameter.indexOf('@') != -1) {
431             final String[] ids = parameter.split("@");
432             final String className = ids[0];
433             final int hashCode = (int) Long.parseLong(ids[1], 16);
434 
435             View view = root.getRootView();
436             if (view instanceof ViewGroup) {
437                 return findView((ViewGroup) view, className, hashCode);
438             }
439         } else {
440             // Look by id
441             final int id = root.getResources().getIdentifier(parameter, null, null);
442             return root.getRootView().findViewById(id);
443         }
444 
445         return null;
446     }
447 
invalidate(View root, String parameter)448     private static void invalidate(View root, String parameter) {
449         final View view = findView(root, parameter);
450         if (view != null) {
451             view.postInvalidate();
452         }
453     }
454 
requestLayout(View root, String parameter)455     private static void requestLayout(View root, String parameter) {
456         final View view = findView(root, parameter);
457         if (view != null) {
458             root.post(new Runnable() {
459                 public void run() {
460                     view.requestLayout();
461                 }
462             });
463         }
464     }
465 
profile(View root, OutputStream clientStream, String parameter)466     private static void profile(View root, OutputStream clientStream, String parameter)
467             throws IOException {
468 
469         final View view = findView(root, parameter);
470         BufferedWriter out = null;
471         try {
472             out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
473 
474             if (view != null) {
475                 profileViewAndChildren(view, out);
476             } else {
477                 out.write("-1 -1 -1");
478                 out.newLine();
479             }
480             out.write("DONE.");
481             out.newLine();
482         } catch (Exception e) {
483             android.util.Log.w("View", "Problem profiling the view:", e);
484         } finally {
485             if (out != null) {
486                 out.close();
487             }
488         }
489     }
490 
profileViewAndChildren(final View view, BufferedWriter out)491     private static void profileViewAndChildren(final View view, BufferedWriter out)
492             throws IOException {
493         profileViewAndChildren(view, out, true);
494     }
495 
profileViewAndChildren(final View view, BufferedWriter out, boolean root)496     private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root)
497             throws IOException {
498 
499         long durationMeasure =
500                 (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0)
501                 ? profileViewOperation(view, new ViewOperation<Void>() {
502                             public Void[] pre() {
503                                 forceLayout(view);
504                                 return null;
505                             }
506 
507                             private void forceLayout(View view) {
508                                 view.forceLayout();
509                                 if (view instanceof ViewGroup) {
510                                     ViewGroup group = (ViewGroup) view;
511                                     final int count = group.getChildCount();
512                                     for (int i = 0; i < count; i++) {
513                                         forceLayout(group.getChildAt(i));
514                                     }
515                                 }
516                             }
517 
518                             public void run(Void... data) {
519                                 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
520                             }
521 
522                             public void post(Void... data) {
523                             }
524                         })
525                         : 0;
526         long durationLayout =
527                 (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0)
528                 ? profileViewOperation(view, new ViewOperation<Void>() {
529                             public Void[] pre() {
530                                 return null;
531                             }
532 
533                             public void run(Void... data) {
534                                 view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom);
535                             }
536 
537                             public void post(Void... data) {
538                             }
539                         }) : 0;
540         long durationDraw =
541                 (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0)
542                 ? profileViewOperation(view, new ViewOperation<Object>() {
543                             public Object[] pre() {
544                                 final DisplayMetrics metrics =
545                                         (view != null && view.getResources() != null) ?
546                                                 view.getResources().getDisplayMetrics() : null;
547                                 final Bitmap bitmap = metrics != null ?
548                                         Bitmap.createBitmap(metrics, metrics.widthPixels,
549                                                 metrics.heightPixels, Bitmap.Config.RGB_565) : null;
550                                 final Canvas canvas = bitmap != null ? new Canvas(bitmap) : null;
551                                 return new Object[] {
552                                         bitmap, canvas
553                                 };
554                             }
555 
556                             public void run(Object... data) {
557                                 if (data[1] != null) {
558                                     view.draw((Canvas) data[1]);
559                                 }
560                             }
561 
562                             public void post(Object... data) {
563                                 if (data[1] != null) {
564                                     ((Canvas) data[1]).setBitmap(null);
565                                 }
566                                 if (data[0] != null) {
567                                     ((Bitmap) data[0]).recycle();
568                                 }
569                             }
570                         }) : 0;
571         out.write(String.valueOf(durationMeasure));
572         out.write(' ');
573         out.write(String.valueOf(durationLayout));
574         out.write(' ');
575         out.write(String.valueOf(durationDraw));
576         out.newLine();
577         if (view instanceof ViewGroup) {
578             ViewGroup group = (ViewGroup) view;
579             final int count = group.getChildCount();
580             for (int i = 0; i < count; i++) {
581                 profileViewAndChildren(group.getChildAt(i), out, false);
582             }
583         }
584     }
585 
586     interface ViewOperation<T> {
pre()587         T[] pre();
run(T... data)588         void run(T... data);
post(T... data)589         void post(T... data);
590     }
591 
profileViewOperation(View view, final ViewOperation<T> operation)592     private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) {
593         final CountDownLatch latch = new CountDownLatch(1);
594         final long[] duration = new long[1];
595 
596         view.post(new Runnable() {
597             public void run() {
598                 try {
599                     T[] data = operation.pre();
600                     long start = Debug.threadCpuTimeNanos();
601                     //noinspection unchecked
602                     operation.run(data);
603                     duration[0] = Debug.threadCpuTimeNanos() - start;
604                     //noinspection unchecked
605                     operation.post(data);
606                 } finally {
607                     latch.countDown();
608                 }
609             }
610         });
611 
612         try {
613             if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) {
614                 Log.w("View", "Could not complete the profiling of the view " + view);
615                 return -1;
616             }
617         } catch (InterruptedException e) {
618             Log.w("View", "Could not complete the profiling of the view " + view);
619             Thread.currentThread().interrupt();
620             return -1;
621         }
622 
623         return duration[0];
624     }
625 
captureLayers(View root, final DataOutputStream clientStream)626     private static void captureLayers(View root, final DataOutputStream clientStream)
627             throws IOException {
628 
629         try {
630             Rect outRect = new Rect();
631             try {
632                 root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect);
633             } catch (RemoteException e) {
634                 // Ignore
635             }
636 
637             clientStream.writeInt(outRect.width());
638             clientStream.writeInt(outRect.height());
639 
640             captureViewLayer(root, clientStream, true);
641 
642             clientStream.write(2);
643         } finally {
644             clientStream.close();
645         }
646     }
647 
captureViewLayer(View view, DataOutputStream clientStream, boolean visible)648     private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
649             throws IOException {
650 
651         final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
652 
653         if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) {
654             final int id = view.getId();
655             String name = view.getClass().getSimpleName();
656             if (id != View.NO_ID) {
657                 name = resolveId(view.getContext(), id).toString();
658             }
659 
660             clientStream.write(1);
661             clientStream.writeUTF(name);
662             clientStream.writeByte(localVisible ? 1 : 0);
663 
664             int[] position = new int[2];
665             // XXX: Should happen on the UI thread
666             view.getLocationInWindow(position);
667 
668             clientStream.writeInt(position[0]);
669             clientStream.writeInt(position[1]);
670             clientStream.flush();
671 
672             Bitmap b = performViewCapture(view, true);
673             if (b != null) {
674                 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
675                         b.getHeight() * 2);
676                 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
677                 clientStream.writeInt(arrayOut.size());
678                 arrayOut.writeTo(clientStream);
679             }
680             clientStream.flush();
681         }
682 
683         if (view instanceof ViewGroup) {
684             ViewGroup group = (ViewGroup) view;
685             int count = group.getChildCount();
686 
687             for (int i = 0; i < count; i++) {
688                 captureViewLayer(group.getChildAt(i), clientStream, localVisible);
689             }
690         }
691     }
692 
outputDisplayList(View root, String parameter)693     private static void outputDisplayList(View root, String parameter) throws IOException {
694         final View view = findView(root, parameter);
695         view.getViewRootImpl().outputDisplayList(view);
696     }
697 
capture(View root, final OutputStream clientStream, String parameter)698     private static void capture(View root, final OutputStream clientStream, String parameter)
699             throws IOException {
700 
701         final View captureView = findView(root, parameter);
702         Bitmap b = performViewCapture(captureView, false);
703 
704         if (b == null) {
705             Log.w("View", "Failed to create capture bitmap!");
706             // Send an empty one so that it doesn't get stuck waiting for
707             // something.
708             b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(),
709                     1, 1, Bitmap.Config.ARGB_8888);
710         }
711 
712         BufferedOutputStream out = null;
713         try {
714             out = new BufferedOutputStream(clientStream, 32 * 1024);
715             b.compress(Bitmap.CompressFormat.PNG, 100, out);
716             out.flush();
717         } finally {
718             if (out != null) {
719                 out.close();
720             }
721             b.recycle();
722         }
723     }
724 
performViewCapture(final View captureView, final boolean skpiChildren)725     private static Bitmap performViewCapture(final View captureView, final boolean skpiChildren) {
726         if (captureView != null) {
727             final CountDownLatch latch = new CountDownLatch(1);
728             final Bitmap[] cache = new Bitmap[1];
729 
730             captureView.post(new Runnable() {
731                 public void run() {
732                     try {
733                         cache[0] = captureView.createSnapshot(
734                                 Bitmap.Config.ARGB_8888, 0, skpiChildren);
735                     } catch (OutOfMemoryError e) {
736                         Log.w("View", "Out of memory for bitmap");
737                     } finally {
738                         latch.countDown();
739                     }
740                 }
741             });
742 
743             try {
744                 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
745                 return cache[0];
746             } catch (InterruptedException e) {
747                 Log.w("View", "Could not complete the capture of the view " + captureView);
748                 Thread.currentThread().interrupt();
749             }
750         }
751 
752         return null;
753     }
754 
dump(View root, OutputStream clientStream)755     private static void dump(View root, OutputStream clientStream) throws IOException {
756         BufferedWriter out = null;
757         try {
758             out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
759             View view = root.getRootView();
760             if (view instanceof ViewGroup) {
761                 ViewGroup group = (ViewGroup) view;
762                 dumpViewHierarchyWithProperties(group.getContext(), group, out, 0);
763             }
764             out.write("DONE.");
765             out.newLine();
766         } catch (Exception e) {
767             android.util.Log.w("View", "Problem dumping the view:", e);
768         } finally {
769             if (out != null) {
770                 out.close();
771             }
772         }
773     }
774 
findView(ViewGroup group, String className, int hashCode)775     private static View findView(ViewGroup group, String className, int hashCode) {
776         if (isRequestedView(group, className, hashCode)) {
777             return group;
778         }
779 
780         final int count = group.getChildCount();
781         for (int i = 0; i < count; i++) {
782             final View view = group.getChildAt(i);
783             if (view instanceof ViewGroup) {
784                 final View found = findView((ViewGroup) view, className, hashCode);
785                 if (found != null) {
786                     return found;
787                 }
788             } else if (isRequestedView(view, className, hashCode)) {
789                 return view;
790             }
791             if (view instanceof HierarchyHandler) {
792                 final View found = ((HierarchyHandler)view)
793                         .findHierarchyView(className, hashCode);
794                 if (found != null) {
795                     return found;
796                 }
797             }
798         }
799 
800         return null;
801     }
802 
isRequestedView(View view, String className, int hashCode)803     private static boolean isRequestedView(View view, String className, int hashCode) {
804         return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
805     }
806 
dumpViewHierarchyWithProperties(Context context, ViewGroup group, BufferedWriter out, int level)807     private static void dumpViewHierarchyWithProperties(Context context, ViewGroup group,
808             BufferedWriter out, int level) {
809         if (!dumpViewWithProperties(context, group, out, level)) {
810             return;
811         }
812 
813         final int count = group.getChildCount();
814         for (int i = 0; i < count; i++) {
815             final View view = group.getChildAt(i);
816             if (view instanceof ViewGroup) {
817                 dumpViewHierarchyWithProperties(context, (ViewGroup) view, out, level + 1);
818             } else {
819                 dumpViewWithProperties(context, view, out, level + 1);
820             }
821         }
822         if (group instanceof HierarchyHandler) {
823             ((HierarchyHandler)group).dumpViewHierarchyWithProperties(out, level + 1);
824         }
825     }
826 
dumpViewWithProperties(Context context, View view, BufferedWriter out, int level)827     private static boolean dumpViewWithProperties(Context context, View view,
828             BufferedWriter out, int level) {
829 
830         try {
831             for (int i = 0; i < level; i++) {
832                 out.write(' ');
833             }
834             out.write(view.getClass().getName());
835             out.write('@');
836             out.write(Integer.toHexString(view.hashCode()));
837             out.write(' ');
838             dumpViewProperties(context, view, out);
839             out.newLine();
840         } catch (IOException e) {
841             Log.w("View", "Error while dumping hierarchy tree");
842             return false;
843         }
844         return true;
845     }
846 
getExportedPropertyFields(Class<?> klass)847     private static Field[] getExportedPropertyFields(Class<?> klass) {
848         if (sFieldsForClasses == null) {
849             sFieldsForClasses = new HashMap<Class<?>, Field[]>();
850         }
851         if (sAnnotations == null) {
852             sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
853         }
854 
855         final HashMap<Class<?>, Field[]> map = sFieldsForClasses;
856 
857         Field[] fields = map.get(klass);
858         if (fields != null) {
859             return fields;
860         }
861 
862         final ArrayList<Field> foundFields = new ArrayList<Field>();
863         fields = klass.getDeclaredFields();
864 
865         int count = fields.length;
866         for (int i = 0; i < count; i++) {
867             final Field field = fields[i];
868             if (field.isAnnotationPresent(ExportedProperty.class)) {
869                 field.setAccessible(true);
870                 foundFields.add(field);
871                 sAnnotations.put(field, field.getAnnotation(ExportedProperty.class));
872             }
873         }
874 
875         fields = foundFields.toArray(new Field[foundFields.size()]);
876         map.put(klass, fields);
877 
878         return fields;
879     }
880 
getExportedPropertyMethods(Class<?> klass)881     private static Method[] getExportedPropertyMethods(Class<?> klass) {
882         if (sMethodsForClasses == null) {
883             sMethodsForClasses = new HashMap<Class<?>, Method[]>(100);
884         }
885         if (sAnnotations == null) {
886             sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
887         }
888 
889         final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
890 
891         Method[] methods = map.get(klass);
892         if (methods != null) {
893             return methods;
894         }
895 
896         final ArrayList<Method> foundMethods = new ArrayList<Method>();
897         methods = klass.getDeclaredMethods();
898 
899         int count = methods.length;
900         for (int i = 0; i < count; i++) {
901             final Method method = methods[i];
902             if (method.getParameterTypes().length == 0 &&
903                     method.isAnnotationPresent(ExportedProperty.class) &&
904                     method.getReturnType() != Void.class) {
905                 method.setAccessible(true);
906                 foundMethods.add(method);
907                 sAnnotations.put(method, method.getAnnotation(ExportedProperty.class));
908             }
909         }
910 
911         methods = foundMethods.toArray(new Method[foundMethods.size()]);
912         map.put(klass, methods);
913 
914         return methods;
915     }
916 
dumpViewProperties(Context context, Object view, BufferedWriter out)917     private static void dumpViewProperties(Context context, Object view,
918             BufferedWriter out) throws IOException {
919 
920         dumpViewProperties(context, view, out, "");
921     }
922 
dumpViewProperties(Context context, Object view, BufferedWriter out, String prefix)923     private static void dumpViewProperties(Context context, Object view,
924             BufferedWriter out, String prefix) throws IOException {
925 
926         if (view == null) {
927             out.write(prefix + "=4,null ");
928             return;
929         }
930 
931         Class<?> klass = view.getClass();
932         do {
933             exportFields(context, view, out, klass, prefix);
934             exportMethods(context, view, out, klass, prefix);
935             klass = klass.getSuperclass();
936         } while (klass != Object.class);
937     }
938 
exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)939     private static void exportMethods(Context context, Object view, BufferedWriter out,
940             Class<?> klass, String prefix) throws IOException {
941 
942         final Method[] methods = getExportedPropertyMethods(klass);
943 
944         int count = methods.length;
945         for (int i = 0; i < count; i++) {
946             final Method method = methods[i];
947             //noinspection EmptyCatchBlock
948             try {
949                 // TODO: This should happen on the UI thread
950                 Object methodValue = method.invoke(view, (Object[]) null);
951                 final Class<?> returnType = method.getReturnType();
952                 final ExportedProperty property = sAnnotations.get(method);
953                 String categoryPrefix =
954                         property.category().length() != 0 ? property.category() + ":" : "";
955 
956                 if (returnType == int.class) {
957 
958                     if (property.resolveId() && context != null) {
959                         final int id = (Integer) methodValue;
960                         methodValue = resolveId(context, id);
961                     } else {
962                         final FlagToString[] flagsMapping = property.flagMapping();
963                         if (flagsMapping.length > 0) {
964                             final int intValue = (Integer) methodValue;
965                             final String valuePrefix =
966                                     categoryPrefix + prefix + method.getName() + '_';
967                             exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
968                         }
969 
970                         final IntToString[] mapping = property.mapping();
971                         if (mapping.length > 0) {
972                             final int intValue = (Integer) methodValue;
973                             boolean mapped = false;
974                             int mappingCount = mapping.length;
975                             for (int j = 0; j < mappingCount; j++) {
976                                 final IntToString mapper = mapping[j];
977                                 if (mapper.from() == intValue) {
978                                     methodValue = mapper.to();
979                                     mapped = true;
980                                     break;
981                                 }
982                             }
983 
984                             if (!mapped) {
985                                 methodValue = intValue;
986                             }
987                         }
988                     }
989                 } else if (returnType == int[].class) {
990                     final int[] array = (int[]) methodValue;
991                     final String valuePrefix = categoryPrefix + prefix + method.getName() + '_';
992                     final String suffix = "()";
993 
994                     exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
995 
996                     // Probably want to return here, same as for fields.
997                     return;
998                 } else if (!returnType.isPrimitive()) {
999                     if (property.deepExport()) {
1000                         dumpViewProperties(context, methodValue, out, prefix + property.prefix());
1001                         continue;
1002                     }
1003                 }
1004 
1005                 writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue);
1006             } catch (IllegalAccessException e) {
1007             } catch (InvocationTargetException e) {
1008             }
1009         }
1010     }
1011 
exportFields(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)1012     private static void exportFields(Context context, Object view, BufferedWriter out,
1013             Class<?> klass, String prefix) throws IOException {
1014 
1015         final Field[] fields = getExportedPropertyFields(klass);
1016 
1017         int count = fields.length;
1018         for (int i = 0; i < count; i++) {
1019             final Field field = fields[i];
1020 
1021             //noinspection EmptyCatchBlock
1022             try {
1023                 Object fieldValue = null;
1024                 final Class<?> type = field.getType();
1025                 final ExportedProperty property = sAnnotations.get(field);
1026                 String categoryPrefix =
1027                         property.category().length() != 0 ? property.category() + ":" : "";
1028 
1029                 if (type == int.class) {
1030 
1031                     if (property.resolveId() && context != null) {
1032                         final int id = field.getInt(view);
1033                         fieldValue = resolveId(context, id);
1034                     } else {
1035                         final FlagToString[] flagsMapping = property.flagMapping();
1036                         if (flagsMapping.length > 0) {
1037                             final int intValue = field.getInt(view);
1038                             final String valuePrefix =
1039                                     categoryPrefix + prefix + field.getName() + '_';
1040                             exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1041                         }
1042 
1043                         final IntToString[] mapping = property.mapping();
1044                         if (mapping.length > 0) {
1045                             final int intValue = field.getInt(view);
1046                             int mappingCount = mapping.length;
1047                             for (int j = 0; j < mappingCount; j++) {
1048                                 final IntToString mapped = mapping[j];
1049                                 if (mapped.from() == intValue) {
1050                                     fieldValue = mapped.to();
1051                                     break;
1052                                 }
1053                             }
1054 
1055                             if (fieldValue == null) {
1056                                 fieldValue = intValue;
1057                             }
1058                         }
1059                     }
1060                 } else if (type == int[].class) {
1061                     final int[] array = (int[]) field.get(view);
1062                     final String valuePrefix = categoryPrefix + prefix + field.getName() + '_';
1063                     final String suffix = "";
1064 
1065                     exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1066 
1067                     // We exit here!
1068                     return;
1069                 } else if (!type.isPrimitive()) {
1070                     if (property.deepExport()) {
1071                         dumpViewProperties(context, field.get(view), out, prefix +
1072                                 property.prefix());
1073                         continue;
1074                     }
1075                 }
1076 
1077                 if (fieldValue == null) {
1078                     fieldValue = field.get(view);
1079                 }
1080 
1081                 writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue);
1082             } catch (IllegalAccessException e) {
1083             }
1084         }
1085     }
1086 
writeEntry(BufferedWriter out, String prefix, String name, String suffix, Object value)1087     private static void writeEntry(BufferedWriter out, String prefix, String name,
1088             String suffix, Object value) throws IOException {
1089 
1090         out.write(prefix);
1091         out.write(name);
1092         out.write(suffix);
1093         out.write("=");
1094         writeValue(out, value);
1095         out.write(' ');
1096     }
1097 
exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, int intValue, String prefix)1098     private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
1099             int intValue, String prefix) throws IOException {
1100 
1101         final int count = mapping.length;
1102         for (int j = 0; j < count; j++) {
1103             final FlagToString flagMapping = mapping[j];
1104             final boolean ifTrue = flagMapping.outputIf();
1105             final int maskResult = intValue & flagMapping.mask();
1106             final boolean test = maskResult == flagMapping.equals();
1107             if ((test && ifTrue) || (!test && !ifTrue)) {
1108                 final String name = flagMapping.name();
1109                 final String value = "0x" + Integer.toHexString(maskResult);
1110                 writeEntry(out, prefix, name, "", value);
1111             }
1112         }
1113     }
1114 
exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix)1115     private static void exportUnrolledArray(Context context, BufferedWriter out,
1116             ExportedProperty property, int[] array, String prefix, String suffix)
1117             throws IOException {
1118 
1119         final IntToString[] indexMapping = property.indexMapping();
1120         final boolean hasIndexMapping = indexMapping.length > 0;
1121 
1122         final IntToString[] mapping = property.mapping();
1123         final boolean hasMapping = mapping.length > 0;
1124 
1125         final boolean resolveId = property.resolveId() && context != null;
1126         final int valuesCount = array.length;
1127 
1128         for (int j = 0; j < valuesCount; j++) {
1129             String name;
1130             String value = null;
1131 
1132             final int intValue = array[j];
1133 
1134             name = String.valueOf(j);
1135             if (hasIndexMapping) {
1136                 int mappingCount = indexMapping.length;
1137                 for (int k = 0; k < mappingCount; k++) {
1138                     final IntToString mapped = indexMapping[k];
1139                     if (mapped.from() == j) {
1140                         name = mapped.to();
1141                         break;
1142                     }
1143                 }
1144             }
1145 
1146             if (hasMapping) {
1147                 int mappingCount = mapping.length;
1148                 for (int k = 0; k < mappingCount; k++) {
1149                     final IntToString mapped = mapping[k];
1150                     if (mapped.from() == intValue) {
1151                         value = mapped.to();
1152                         break;
1153                     }
1154                 }
1155             }
1156 
1157             if (resolveId) {
1158                 if (value == null) value = (String) resolveId(context, intValue);
1159             } else {
1160                 value = String.valueOf(intValue);
1161             }
1162 
1163             writeEntry(out, prefix, name, suffix, value);
1164         }
1165     }
1166 
resolveId(Context context, int id)1167     static Object resolveId(Context context, int id) {
1168         Object fieldValue;
1169         final Resources resources = context.getResources();
1170         if (id >= 0) {
1171             try {
1172                 fieldValue = resources.getResourceTypeName(id) + '/' +
1173                         resources.getResourceEntryName(id);
1174             } catch (Resources.NotFoundException e) {
1175                 fieldValue = "id/0x" + Integer.toHexString(id);
1176             }
1177         } else {
1178             fieldValue = "NO_ID";
1179         }
1180         return fieldValue;
1181     }
1182 
writeValue(BufferedWriter out, Object value)1183     private static void writeValue(BufferedWriter out, Object value) throws IOException {
1184         if (value != null) {
1185             String output = "[EXCEPTION]";
1186             try {
1187                 output = value.toString().replace("\n", "\\n");
1188             } finally {
1189                 out.write(String.valueOf(output.length()));
1190                 out.write(",");
1191                 out.write(output);
1192             }
1193         } else {
1194             out.write("4,null");
1195         }
1196     }
1197 
capturedViewGetPropertyFields(Class<?> klass)1198     private static Field[] capturedViewGetPropertyFields(Class<?> klass) {
1199         if (mCapturedViewFieldsForClasses == null) {
1200             mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>();
1201         }
1202         final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses;
1203 
1204         Field[] fields = map.get(klass);
1205         if (fields != null) {
1206             return fields;
1207         }
1208 
1209         final ArrayList<Field> foundFields = new ArrayList<Field>();
1210         fields = klass.getFields();
1211 
1212         int count = fields.length;
1213         for (int i = 0; i < count; i++) {
1214             final Field field = fields[i];
1215             if (field.isAnnotationPresent(CapturedViewProperty.class)) {
1216                 field.setAccessible(true);
1217                 foundFields.add(field);
1218             }
1219         }
1220 
1221         fields = foundFields.toArray(new Field[foundFields.size()]);
1222         map.put(klass, fields);
1223 
1224         return fields;
1225     }
1226 
capturedViewGetPropertyMethods(Class<?> klass)1227     private static Method[] capturedViewGetPropertyMethods(Class<?> klass) {
1228         if (mCapturedViewMethodsForClasses == null) {
1229             mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>();
1230         }
1231         final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses;
1232 
1233         Method[] methods = map.get(klass);
1234         if (methods != null) {
1235             return methods;
1236         }
1237 
1238         final ArrayList<Method> foundMethods = new ArrayList<Method>();
1239         methods = klass.getMethods();
1240 
1241         int count = methods.length;
1242         for (int i = 0; i < count; i++) {
1243             final Method method = methods[i];
1244             if (method.getParameterTypes().length == 0 &&
1245                     method.isAnnotationPresent(CapturedViewProperty.class) &&
1246                     method.getReturnType() != Void.class) {
1247                 method.setAccessible(true);
1248                 foundMethods.add(method);
1249             }
1250         }
1251 
1252         methods = foundMethods.toArray(new Method[foundMethods.size()]);
1253         map.put(klass, methods);
1254 
1255         return methods;
1256     }
1257 
capturedViewExportMethods(Object obj, Class<?> klass, String prefix)1258     private static String capturedViewExportMethods(Object obj, Class<?> klass,
1259             String prefix) {
1260 
1261         if (obj == null) {
1262             return "null";
1263         }
1264 
1265         StringBuilder sb = new StringBuilder();
1266         final Method[] methods = capturedViewGetPropertyMethods(klass);
1267 
1268         int count = methods.length;
1269         for (int i = 0; i < count; i++) {
1270             final Method method = methods[i];
1271             try {
1272                 Object methodValue = method.invoke(obj, (Object[]) null);
1273                 final Class<?> returnType = method.getReturnType();
1274 
1275                 CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class);
1276                 if (property.retrieveReturn()) {
1277                     //we are interested in the second level data only
1278                     sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#"));
1279                 } else {
1280                     sb.append(prefix);
1281                     sb.append(method.getName());
1282                     sb.append("()=");
1283 
1284                     if (methodValue != null) {
1285                         final String value = methodValue.toString().replace("\n", "\\n");
1286                         sb.append(value);
1287                     } else {
1288                         sb.append("null");
1289                     }
1290                     sb.append("; ");
1291                 }
1292               } catch (IllegalAccessException e) {
1293                   //Exception IllegalAccess, it is OK here
1294                   //we simply ignore this method
1295               } catch (InvocationTargetException e) {
1296                   //Exception InvocationTarget, it is OK here
1297                   //we simply ignore this method
1298               }
1299         }
1300         return sb.toString();
1301     }
1302 
capturedViewExportFields(Object obj, Class<?> klass, String prefix)1303     private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) {
1304         if (obj == null) {
1305             return "null";
1306         }
1307 
1308         StringBuilder sb = new StringBuilder();
1309         final Field[] fields = capturedViewGetPropertyFields(klass);
1310 
1311         int count = fields.length;
1312         for (int i = 0; i < count; i++) {
1313             final Field field = fields[i];
1314             try {
1315                 Object fieldValue = field.get(obj);
1316 
1317                 sb.append(prefix);
1318                 sb.append(field.getName());
1319                 sb.append("=");
1320 
1321                 if (fieldValue != null) {
1322                     final String value = fieldValue.toString().replace("\n", "\\n");
1323                     sb.append(value);
1324                 } else {
1325                     sb.append("null");
1326                 }
1327                 sb.append(' ');
1328             } catch (IllegalAccessException e) {
1329                 //Exception IllegalAccess, it is OK here
1330                 //we simply ignore this field
1331             }
1332         }
1333         return sb.toString();
1334     }
1335 
1336     /**
1337      * Dump view info for id based instrument test generation
1338      * (and possibly further data analysis). The results are dumped
1339      * to the log.
1340      * @param tag for log
1341      * @param view for dump
1342      */
dumpCapturedView(String tag, Object view)1343     public static void dumpCapturedView(String tag, Object view) {
1344         Class<?> klass = view.getClass();
1345         StringBuilder sb = new StringBuilder(klass.getName() + ": ");
1346         sb.append(capturedViewExportFields(view, klass, ""));
1347         sb.append(capturedViewExportMethods(view, klass, ""));
1348         Log.d(tag, sb.toString());
1349     }
1350 }
1351