• 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.Environment;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.os.ParcelFileDescriptor;
29 import android.os.RemoteException;
30 import android.os.SystemClock;
31 import android.util.DisplayMetrics;
32 import android.util.Log;
33 import android.util.Printer;
34 
35 import java.io.BufferedOutputStream;
36 import java.io.BufferedWriter;
37 import java.io.ByteArrayOutputStream;
38 import java.io.DataOutputStream;
39 import java.io.File;
40 import java.io.FileDescriptor;
41 import java.io.FileOutputStream;
42 import java.io.FileWriter;
43 import java.io.IOException;
44 import java.io.OutputStream;
45 import java.io.OutputStreamWriter;
46 import java.lang.annotation.ElementType;
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.lang.annotation.Target;
50 import java.lang.reflect.AccessibleObject;
51 import java.lang.reflect.Field;
52 import java.lang.reflect.InvocationTargetException;
53 import java.lang.reflect.Method;
54 import java.nio.ByteBuffer;
55 import java.nio.ByteOrder;
56 import java.nio.channels.FileChannel;
57 import java.util.ArrayList;
58 import java.util.HashMap;
59 import java.util.LinkedList;
60 import java.util.List;
61 import java.util.Map;
62 import java.util.concurrent.CountDownLatch;
63 import java.util.concurrent.TimeUnit;
64 
65 /**
66  * Various debugging/tracing tools related to {@link View} and the view hierarchy.
67  */
68 public class ViewDebug {
69     /**
70      * Log tag used to log errors related to the consistency of the view hierarchy.
71      *
72      * @hide
73      */
74     public static final String CONSISTENCY_LOG_TAG = "ViewConsistency";
75 
76     /**
77      * Flag indicating the consistency check should check layout-related properties.
78      *
79      * @hide
80      */
81     public static final int CONSISTENCY_LAYOUT = 0x1;
82 
83     /**
84      * Flag indicating the consistency check should check drawing-related properties.
85      *
86      * @hide
87      */
88     public static final int CONSISTENCY_DRAWING = 0x2;
89 
90     /**
91      * Enables or disables view hierarchy tracing. Any invoker of
92      * {@link #trace(View, android.view.ViewDebug.HierarchyTraceType)} should first
93      * check that this value is set to true as not to affect performance.
94      */
95     public static final boolean TRACE_HIERARCHY = false;
96 
97     /**
98      * Enables or disables view recycler tracing. Any invoker of
99      * {@link #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])} should first
100      * check that this value is set to true as not to affect performance.
101      */
102     public static final boolean TRACE_RECYCLER = false;
103 
104     /**
105      * Profiles drawing times in the events log.
106      *
107      * @hide
108      */
109     public static final boolean DEBUG_PROFILE_DRAWING = false;
110 
111     /**
112      * Profiles layout times in the events log.
113      *
114      * @hide
115      */
116     public static final boolean DEBUG_PROFILE_LAYOUT = false;
117 
118     /**
119      * Enables detailed logging of drag/drop operations.
120      * @hide
121      */
122     public static final boolean DEBUG_DRAG = false;
123 
124     /**
125      * Enables logging of factors that affect the latency and responsiveness of an application.
126      *
127      * Logs the relative difference between the time an event was created and the time it
128      * was delivered.
129      *
130      * Logs the time spent waiting for Surface.lockCanvas() or eglSwapBuffers().
131      * This is time that the event loop spends blocked and unresponsive.  Ideally, drawing
132      * and animations should be perfectly synchronized with VSYNC so that swap buffers
133      * is instantaneous.
134      *
135      * Logs the time spent in ViewRoot.performTraversals() or ViewRoot.draw().
136      * @hide
137      */
138     public static final boolean DEBUG_LATENCY = false;
139 
140     /**
141      * <p>Enables or disables views consistency check. Even when this property is enabled,
142      * view consistency checks happen only if {@link false} is set
143      * to true. The value of this property can be configured externally in one of the
144      * following files:</p>
145      * <ul>
146      *  <li>/system/debug.prop</li>
147      *  <li>/debug.prop</li>
148      *  <li>/data/debug.prop</li>
149      * </ul>
150      * @hide
151      */
152     @Debug.DebugProperty
153     public static boolean consistencyCheckEnabled = false;
154 
155     /**
156      * This annotation can be used to mark fields and methods to be dumped by
157      * the view server. Only non-void methods with no arguments can be annotated
158      * by this annotation.
159      */
160     @Target({ ElementType.FIELD, ElementType.METHOD })
161     @Retention(RetentionPolicy.RUNTIME)
162     public @interface ExportedProperty {
163         /**
164          * When resolveId is true, and if the annotated field/method return value
165          * is an int, the value is converted to an Android's resource name.
166          *
167          * @return true if the property's value must be transformed into an Android
168          *         resource name, false otherwise
169          */
resolveId()170         boolean resolveId() default false;
171 
172         /**
173          * A mapping can be defined to map int values to specific strings. For
174          * instance, View.getVisibility() returns 0, 4 or 8. However, these values
175          * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
176          * these human readable values:
177          *
178          * <pre>
179          * @ViewDebug.ExportedProperty(mapping = {
180          *     @ViewDebug.IntToString(from = 0, to = "VISIBLE"),
181          *     @ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
182          *     @ViewDebug.IntToString(from = 8, to = "GONE")
183          * })
184          * public int getVisibility() { ...
185          * <pre>
186          *
187          * @return An array of int to String mappings
188          *
189          * @see android.view.ViewDebug.IntToString
190          */
mapping()191         IntToString[] mapping() default { };
192 
193         /**
194          * A mapping can be defined to map array indices to specific strings.
195          * A mapping can be used to see human readable values for the indices
196          * of an array:
197          *
198          * <pre>
199          * @ViewDebug.ExportedProperty(indexMapping = {
200          *     @ViewDebug.IntToString(from = 0, to = "INVALID"),
201          *     @ViewDebug.IntToString(from = 1, to = "FIRST"),
202          *     @ViewDebug.IntToString(from = 2, to = "SECOND")
203          * })
204          * private int[] mElements;
205          * <pre>
206          *
207          * @return An array of int to String mappings
208          *
209          * @see android.view.ViewDebug.IntToString
210          * @see #mapping()
211          */
indexMapping()212         IntToString[] indexMapping() default { };
213 
214         /**
215          * A flags mapping can be defined to map flags encoded in an integer to
216          * specific strings. A mapping can be used to see human readable values
217          * for the flags of an integer:
218          *
219          * <pre>
220          * @ViewDebug.ExportedProperty(flagMapping = {
221          *     @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED, name = "ENABLED"),
222          *     @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED, name = "DISABLED"),
223          * })
224          * private int mFlags;
225          * <pre>
226          *
227          * A specified String is output when the following is true:
228          *
229          * @return An array of int to String mappings
230          */
flagMapping()231         FlagToString[] flagMapping() default { };
232 
233         /**
234          * When deep export is turned on, this property is not dumped. Instead, the
235          * properties contained in this property are dumped. Each child property
236          * is prefixed with the name of this property.
237          *
238          * @return true if the properties of this property should be dumped
239          *
240          * @see #prefix()
241          */
deepExport()242         boolean deepExport() default false;
243 
244         /**
245          * The prefix to use on child properties when deep export is enabled
246          *
247          * @return a prefix as a String
248          *
249          * @see #deepExport()
250          */
prefix()251         String prefix() default "";
252 
253         /**
254          * Specifies the category the property falls into, such as measurement,
255          * layout, drawing, etc.
256          *
257          * @return the category as String
258          */
category()259         String category() default "";
260     }
261 
262     /**
263      * Defines a mapping from an int value to a String. Such a mapping can be used
264      * in a @ExportedProperty to provide more meaningful values to the end user.
265      *
266      * @see android.view.ViewDebug.ExportedProperty
267      */
268     @Target({ ElementType.TYPE })
269     @Retention(RetentionPolicy.RUNTIME)
270     public @interface IntToString {
271         /**
272          * The original int value to map to a String.
273          *
274          * @return An arbitrary int value.
275          */
from()276         int from();
277 
278         /**
279          * The String to use in place of the original int value.
280          *
281          * @return An arbitrary non-null String.
282          */
to()283         String to();
284     }
285 
286     /**
287      * Defines a mapping from an flag to a String. Such a mapping can be used
288      * in a @ExportedProperty to provide more meaningful values to the end user.
289      *
290      * @see android.view.ViewDebug.ExportedProperty
291      */
292     @Target({ ElementType.TYPE })
293     @Retention(RetentionPolicy.RUNTIME)
294     public @interface FlagToString {
295         /**
296          * The mask to apply to the original value.
297          *
298          * @return An arbitrary int value.
299          */
mask()300         int mask();
301 
302         /**
303          * The value to compare to the result of:
304          * <code>original value &amp; {@link #mask()}</code>.
305          *
306          * @return An arbitrary value.
307          */
equals()308         int equals();
309 
310         /**
311          * The String to use in place of the original int value.
312          *
313          * @return An arbitrary non-null String.
314          */
name()315         String name();
316 
317         /**
318          * Indicates whether to output the flag when the test is true,
319          * or false. Defaults to true.
320          */
outputIf()321         boolean outputIf() default true;
322     }
323 
324     /**
325      * This annotation can be used to mark fields and methods to be dumped when
326      * the view is captured. Methods with this annotation must have no arguments
327      * and must return a valid type of data.
328      */
329     @Target({ ElementType.FIELD, ElementType.METHOD })
330     @Retention(RetentionPolicy.RUNTIME)
331     public @interface CapturedViewProperty {
332         /**
333          * When retrieveReturn is true, we need to retrieve second level methods
334          * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
335          * we will set retrieveReturn = true on the annotation of
336          * myView.getFirstLevelMethod()
337          * @return true if we need the second level methods
338          */
retrieveReturn()339         boolean retrieveReturn() default false;
340     }
341 
342     private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null;
343     private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null;
344 
345     // Maximum delay in ms after which we stop trying to capture a View's drawing
346     private static final int CAPTURE_TIMEOUT = 4000;
347 
348     private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
349     private static final String REMOTE_COMMAND_DUMP = "DUMP";
350     private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
351     private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
352     private static final String REMOTE_PROFILE = "PROFILE";
353     private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS";
354     private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST";
355 
356     private static HashMap<Class<?>, Field[]> sFieldsForClasses;
357     private static HashMap<Class<?>, Method[]> sMethodsForClasses;
358     private static HashMap<AccessibleObject, ExportedProperty> sAnnotations;
359 
360     /**
361      * Defines the type of hierarhcy trace to output to the hierarchy traces file.
362      */
363     public enum HierarchyTraceType {
364         INVALIDATE,
365         INVALIDATE_CHILD,
366         INVALIDATE_CHILD_IN_PARENT,
367         REQUEST_LAYOUT,
368         ON_LAYOUT,
369         ON_MEASURE,
370         DRAW,
371         BUILD_CACHE
372     }
373 
374     private static BufferedWriter sHierarchyTraces;
375     private static ViewRootImpl sHierarhcyRoot;
376     private static String sHierarchyTracePrefix;
377 
378     /**
379      * Defines the type of recycler trace to output to the recycler traces file.
380      */
381     public enum RecyclerTraceType {
382         NEW_VIEW,
383         BIND_VIEW,
384         RECYCLE_FROM_ACTIVE_HEAP,
385         RECYCLE_FROM_SCRAP_HEAP,
386         MOVE_TO_SCRAP_HEAP,
387         MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
388     }
389 
390     private static class RecyclerTrace {
391         public int view;
392         public RecyclerTraceType type;
393         public int position;
394         public int indexOnScreen;
395     }
396 
397     private static View sRecyclerOwnerView;
398     private static List<View> sRecyclerViews;
399     private static List<RecyclerTrace> sRecyclerTraces;
400     private static String sRecyclerTracePrefix;
401 
402     private static final ThreadLocal<LooperProfiler> sLooperProfilerStorage =
403             new ThreadLocal<LooperProfiler>();
404 
405     /**
406      * Returns the number of instanciated Views.
407      *
408      * @return The number of Views instanciated in the current process.
409      *
410      * @hide
411      */
getViewInstanceCount()412     public static long getViewInstanceCount() {
413         return Debug.countInstancesOfClass(View.class);
414     }
415 
416     /**
417      * Returns the number of instanciated ViewAncestors.
418      *
419      * @return The number of ViewAncestors instanciated in the current process.
420      *
421      * @hide
422      */
getViewRootImplCount()423     public static long getViewRootImplCount() {
424         return Debug.countInstancesOfClass(ViewRootImpl.class);
425     }
426 
427     /**
428      * Starts profiling the looper associated with the current thread.
429      * You must call {@link #stopLooperProfiling} to end profiling
430      * and obtain the traces. Both methods must be invoked on the
431      * same thread.
432      *
433      * @hide
434      */
startLooperProfiling(String path, FileDescriptor fileDescriptor)435     public static void startLooperProfiling(String path, FileDescriptor fileDescriptor) {
436         if (sLooperProfilerStorage.get() == null) {
437             LooperProfiler profiler = new LooperProfiler(path, fileDescriptor);
438             sLooperProfilerStorage.set(profiler);
439             Looper.myLooper().setMessageLogging(profiler);
440         }
441     }
442 
443     /**
444      * Stops profiling the looper associated with the current thread.
445      *
446      * @see #startLooperProfiling(String, java.io.FileDescriptor)
447      *
448      * @hide
449      */
stopLooperProfiling()450     public static void stopLooperProfiling() {
451         LooperProfiler profiler = sLooperProfilerStorage.get();
452         if (profiler != null) {
453             sLooperProfilerStorage.remove();
454             Looper.myLooper().setMessageLogging(null);
455             profiler.save();
456         }
457     }
458 
459     private static class LooperProfiler implements Looper.Profiler, Printer {
460         private static final String LOG_TAG = "LooperProfiler";
461 
462         private static final int TRACE_VERSION_NUMBER = 3;
463         private static final int ACTION_EXIT_METHOD = 0x1;
464         private static final int HEADER_SIZE = 32;
465         private static final String HEADER_MAGIC = "SLOW";
466         private static final short HEADER_RECORD_SIZE = (short) 14;
467 
468         private final long mTraceWallStart;
469         private final long mTraceThreadStart;
470 
471         private final ArrayList<Entry> mTraces = new ArrayList<Entry>(512);
472 
473         private final HashMap<String, Integer> mTraceNames = new HashMap<String, Integer>(32);
474         private int mTraceId = 0;
475 
476         private final String mPath;
477         private ParcelFileDescriptor mFileDescriptor;
478 
LooperProfiler(String path, FileDescriptor fileDescriptor)479         LooperProfiler(String path, FileDescriptor fileDescriptor) {
480             mPath = path;
481             try {
482                 mFileDescriptor = ParcelFileDescriptor.dup(fileDescriptor);
483             } catch (IOException e) {
484                 Log.e(LOG_TAG, "Could not write trace file " + mPath, e);
485                 throw new RuntimeException(e);
486             }
487             mTraceWallStart = SystemClock.currentTimeMicro();
488             mTraceThreadStart = SystemClock.currentThreadTimeMicro();
489         }
490 
491         @Override
println(String x)492         public void println(String x) {
493             // Ignore messages
494         }
495 
496         @Override
profile(Message message, long wallStart, long wallTime, long threadStart, long threadTime)497         public void profile(Message message, long wallStart, long wallTime,
498                 long threadStart, long threadTime) {
499             Entry entry = new Entry();
500             entry.traceId = getTraceId(message);
501             entry.wallStart = wallStart;
502             entry.wallTime = wallTime;
503             entry.threadStart = threadStart;
504             entry.threadTime = threadTime;
505 
506             mTraces.add(entry);
507         }
508 
getTraceId(Message message)509         private int getTraceId(Message message) {
510             String name = message.getTarget().getMessageName(message);
511             Integer traceId = mTraceNames.get(name);
512             if (traceId == null) {
513                 traceId = mTraceId++ << 4;
514                 mTraceNames.put(name, traceId);
515             }
516             return traceId;
517         }
518 
save()519         void save() {
520             // Don't block the UI thread
521             new Thread(new Runnable() {
522                 @Override
523                 public void run() {
524                     saveTraces();
525                 }
526             }, "LooperProfiler[" + mPath + "]").start();
527         }
528 
saveTraces()529         private void saveTraces() {
530             FileOutputStream fos = new FileOutputStream(mFileDescriptor.getFileDescriptor());
531             DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos));
532 
533             try {
534                 writeHeader(out, mTraceWallStart, mTraceNames, mTraces);
535                 out.flush();
536 
537                 writeTraces(fos, out.size(), mTraceWallStart, mTraceThreadStart, mTraces);
538 
539                 Log.d(LOG_TAG, "Looper traces ready: " + mPath);
540             } catch (IOException e) {
541                 Log.e(LOG_TAG, "Could not write trace file " + mPath, e);
542             } finally {
543                 try {
544                     out.close();
545                 } catch (IOException e) {
546                     Log.e(LOG_TAG, "Could not write trace file " + mPath, e);
547                 }
548                 try {
549                     mFileDescriptor.close();
550                 } catch (IOException e) {
551                     Log.e(LOG_TAG, "Could not write trace file " + mPath, e);
552                 }
553             }
554         }
555 
writeTraces(FileOutputStream out, long offset, long wallStart, long threadStart, ArrayList<Entry> entries)556         private static void writeTraces(FileOutputStream out, long offset, long wallStart,
557                 long threadStart, ArrayList<Entry> entries) throws IOException {
558 
559             FileChannel channel = out.getChannel();
560 
561             // Header
562             ByteBuffer buffer = ByteBuffer.allocateDirect(HEADER_SIZE);
563             buffer.put(HEADER_MAGIC.getBytes());
564             buffer = buffer.order(ByteOrder.LITTLE_ENDIAN);
565             buffer.putShort((short) TRACE_VERSION_NUMBER);    // version
566             buffer.putShort((short) HEADER_SIZE);             // offset to data
567             buffer.putLong(wallStart);                        // start time in usec
568             buffer.putShort(HEADER_RECORD_SIZE);              // size of a record in bytes
569             // padding to 32 bytes
570             for (int i = 0; i < HEADER_SIZE - 18; i++) {
571                 buffer.put((byte) 0);
572             }
573 
574             buffer.flip();
575             channel.position(offset).write(buffer);
576 
577             buffer = ByteBuffer.allocateDirect(14).order(ByteOrder.LITTLE_ENDIAN);
578             for (Entry entry : entries) {
579                 buffer.putShort((short) 1);   // we simulate only one thread
580                 buffer.putInt(entry.traceId); // entering method
581                 buffer.putInt((int) (entry.threadStart - threadStart));
582                 buffer.putInt((int) (entry.wallStart - wallStart));
583 
584                 buffer.flip();
585                 channel.write(buffer);
586                 buffer.clear();
587 
588                 buffer.putShort((short) 1);
589                 buffer.putInt(entry.traceId | ACTION_EXIT_METHOD); // exiting method
590                 buffer.putInt((int) (entry.threadStart + entry.threadTime - threadStart));
591                 buffer.putInt((int) (entry.wallStart + entry.wallTime - wallStart));
592 
593                 buffer.flip();
594                 channel.write(buffer);
595                 buffer.clear();
596             }
597 
598             channel.close();
599         }
600 
writeHeader(DataOutputStream out, long start, HashMap<String, Integer> names, ArrayList<Entry> entries)601         private static void writeHeader(DataOutputStream out, long start,
602                 HashMap<String, Integer> names, ArrayList<Entry> entries) throws IOException {
603 
604             Entry last = entries.get(entries.size() - 1);
605             long wallTotal = (last.wallStart + last.wallTime) - start;
606 
607             startSection("version", out);
608             addValue(null, Integer.toString(TRACE_VERSION_NUMBER), out);
609             addValue("data-file-overflow", "false", out);
610             addValue("clock", "dual", out);
611             addValue("elapsed-time-usec", Long.toString(wallTotal), out);
612             addValue("num-method-calls", Integer.toString(entries.size()), out);
613             addValue("clock-call-overhead-nsec", "1", out);
614             addValue("vm", "dalvik", out);
615 
616             startSection("threads", out);
617             addThreadId(1, "main", out);
618 
619             startSection("methods", out);
620             addMethods(names, out);
621 
622             startSection("end", out);
623         }
624 
addMethods(HashMap<String, Integer> names, DataOutputStream out)625         private static void addMethods(HashMap<String, Integer> names, DataOutputStream out)
626                 throws IOException {
627 
628             for (Map.Entry<String, Integer> name : names.entrySet()) {
629                 out.writeBytes(String.format("0x%08x\tEventQueue\t%s\t()V\tLooper\t-2\n",
630                         name.getValue(), name.getKey()));
631             }
632         }
633 
addThreadId(int id, String name, DataOutputStream out)634         private static void addThreadId(int id, String name, DataOutputStream out)
635                 throws IOException {
636 
637             out.writeBytes(Integer.toString(id) + '\t' + name + '\n');
638         }
639 
addValue(String name, String value, DataOutputStream out)640         private static void addValue(String name, String value, DataOutputStream out)
641                 throws IOException {
642 
643             if (name != null) {
644                 out.writeBytes(name + "=");
645             }
646             out.writeBytes(value + '\n');
647         }
648 
startSection(String name, DataOutputStream out)649         private static void startSection(String name, DataOutputStream out) throws IOException {
650             out.writeBytes("*" + name + '\n');
651         }
652 
653         static class Entry {
654             int traceId;
655             long wallStart;
656             long wallTime;
657             long threadStart;
658             long threadTime;
659         }
660     }
661 
662     /**
663      * Outputs a trace to the currently opened recycler traces. The trace records the type of
664      * recycler action performed on the supplied view as well as a number of parameters.
665      *
666      * @param view the view to trace
667      * @param type the type of the trace
668      * @param parameters parameters depending on the type of the trace
669      */
trace(View view, RecyclerTraceType type, int... parameters)670     public static void trace(View view, RecyclerTraceType type, int... parameters) {
671         if (sRecyclerOwnerView == null || sRecyclerViews == null) {
672             return;
673         }
674 
675         if (!sRecyclerViews.contains(view)) {
676             sRecyclerViews.add(view);
677         }
678 
679         final int index = sRecyclerViews.indexOf(view);
680 
681         RecyclerTrace trace = new RecyclerTrace();
682         trace.view = index;
683         trace.type = type;
684         trace.position = parameters[0];
685         trace.indexOnScreen = parameters[1];
686 
687         sRecyclerTraces.add(trace);
688     }
689 
690     /**
691      * Starts tracing the view recycler of the specified view. The trace is identified by a prefix,
692      * used to build the traces files names: <code>/EXTERNAL/view-recycler/PREFIX.traces</code> and
693      * <code>/EXTERNAL/view-recycler/PREFIX.recycler</code>.
694      *
695      * Only one view recycler can be traced at the same time. After calling this method, any
696      * other invocation will result in a <code>IllegalStateException</code> unless
697      * {@link #stopRecyclerTracing()} is invoked before.
698      *
699      * Traces files are created only after {@link #stopRecyclerTracing()} is invoked.
700      *
701      * This method will return immediately if TRACE_RECYCLER is false.
702      *
703      * @param prefix the traces files name prefix
704      * @param view the view whose recycler must be traced
705      *
706      * @see #stopRecyclerTracing()
707      * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
708      */
startRecyclerTracing(String prefix, View view)709     public static void startRecyclerTracing(String prefix, View view) {
710         //noinspection PointlessBooleanExpression,ConstantConditions
711         if (!TRACE_RECYCLER) {
712             return;
713         }
714 
715         if (sRecyclerOwnerView != null) {
716             throw new IllegalStateException("You must call stopRecyclerTracing() before running" +
717                 " a new trace!");
718         }
719 
720         sRecyclerTracePrefix = prefix;
721         sRecyclerOwnerView = view;
722         sRecyclerViews = new ArrayList<View>();
723         sRecyclerTraces = new LinkedList<RecyclerTrace>();
724     }
725 
726     /**
727      * Stops the current view recycer tracing.
728      *
729      * Calling this method creates the file <code>/EXTERNAL/view-recycler/PREFIX.traces</code>
730      * containing all the traces (or method calls) relative to the specified view's recycler.
731      *
732      * Calling this method creates the file <code>/EXTERNAL/view-recycler/PREFIX.recycler</code>
733      * containing all of the views used by the recycler of the view supplied to
734      * {@link #startRecyclerTracing(String, View)}.
735      *
736      * This method will return immediately if TRACE_RECYCLER is false.
737      *
738      * @see #startRecyclerTracing(String, View)
739      * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
740      */
stopRecyclerTracing()741     public static void stopRecyclerTracing() {
742         //noinspection PointlessBooleanExpression,ConstantConditions
743         if (!TRACE_RECYCLER) {
744             return;
745         }
746 
747         if (sRecyclerOwnerView == null || sRecyclerViews == null) {
748             throw new IllegalStateException("You must call startRecyclerTracing() before" +
749                 " stopRecyclerTracing()!");
750         }
751 
752         File recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/");
753         //noinspection ResultOfMethodCallIgnored
754         recyclerDump.mkdirs();
755 
756         recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".recycler");
757         try {
758             final BufferedWriter out = new BufferedWriter(new FileWriter(recyclerDump), 8 * 1024);
759 
760             for (View view : sRecyclerViews) {
761                 final String name = view.getClass().getName();
762                 out.write(name);
763                 out.newLine();
764             }
765 
766             out.close();
767         } catch (IOException e) {
768             Log.e("View", "Could not dump recycler content");
769             return;
770         }
771 
772         recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/");
773         recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces");
774         try {
775             if (recyclerDump.exists()) {
776                 //noinspection ResultOfMethodCallIgnored
777                 recyclerDump.delete();
778             }
779             final FileOutputStream file = new FileOutputStream(recyclerDump);
780             final DataOutputStream out = new DataOutputStream(file);
781 
782             for (RecyclerTrace trace : sRecyclerTraces) {
783                 out.writeInt(trace.view);
784                 out.writeInt(trace.type.ordinal());
785                 out.writeInt(trace.position);
786                 out.writeInt(trace.indexOnScreen);
787                 out.flush();
788             }
789 
790             out.close();
791         } catch (IOException e) {
792             Log.e("View", "Could not dump recycler traces");
793             return;
794         }
795 
796         sRecyclerViews.clear();
797         sRecyclerViews = null;
798 
799         sRecyclerTraces.clear();
800         sRecyclerTraces = null;
801 
802         sRecyclerOwnerView = null;
803     }
804 
805     /**
806      * Outputs a trace to the currently opened traces file. The trace contains the class name
807      * and instance's hashcode of the specified view as well as the supplied trace type.
808      *
809      * @param view the view to trace
810      * @param type the type of the trace
811      */
trace(View view, HierarchyTraceType type)812     public static void trace(View view, HierarchyTraceType type) {
813         if (sHierarchyTraces == null) {
814             return;
815         }
816 
817         try {
818             sHierarchyTraces.write(type.name());
819             sHierarchyTraces.write(' ');
820             sHierarchyTraces.write(view.getClass().getName());
821             sHierarchyTraces.write('@');
822             sHierarchyTraces.write(Integer.toHexString(view.hashCode()));
823             sHierarchyTraces.newLine();
824         } catch (IOException e) {
825             Log.w("View", "Error while dumping trace of type " + type + " for view " + view);
826         }
827     }
828 
829     /**
830      * Starts tracing the view hierarchy of the specified view. The trace is identified by a prefix,
831      * used to build the traces files names: <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code> and
832      * <code>/EXTERNAL/view-hierarchy/PREFIX.tree</code>.
833      *
834      * Only one view hierarchy can be traced at the same time. After calling this method, any
835      * other invocation will result in a <code>IllegalStateException</code> unless
836      * {@link #stopHierarchyTracing()} is invoked before.
837      *
838      * Calling this method creates the file <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code>
839      * containing all the traces (or method calls) relative to the specified view's hierarchy.
840      *
841      * This method will return immediately if TRACE_HIERARCHY is false.
842      *
843      * @param prefix the traces files name prefix
844      * @param view the view whose hierarchy must be traced
845      *
846      * @see #stopHierarchyTracing()
847      * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
848      */
startHierarchyTracing(String prefix, View view)849     public static void startHierarchyTracing(String prefix, View view) {
850         //noinspection PointlessBooleanExpression,ConstantConditions
851         if (!TRACE_HIERARCHY) {
852             return;
853         }
854 
855         if (sHierarhcyRoot != null) {
856             throw new IllegalStateException("You must call stopHierarchyTracing() before running" +
857                 " a new trace!");
858         }
859 
860         File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/");
861         //noinspection ResultOfMethodCallIgnored
862         hierarchyDump.mkdirs();
863 
864         hierarchyDump = new File(hierarchyDump, prefix + ".traces");
865         sHierarchyTracePrefix = prefix;
866 
867         try {
868             sHierarchyTraces = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
869         } catch (IOException e) {
870             Log.e("View", "Could not dump view hierarchy");
871             return;
872         }
873 
874         sHierarhcyRoot = (ViewRootImpl) view.getRootView().getParent();
875     }
876 
877     /**
878      * Stops the current view hierarchy tracing. This method closes the file
879      * <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code>.
880      *
881      * Calling this method creates the file <code>/EXTERNAL/view-hierarchy/PREFIX.tree</code>
882      * containing the view hierarchy of the view supplied to
883      * {@link #startHierarchyTracing(String, View)}.
884      *
885      * This method will return immediately if TRACE_HIERARCHY is false.
886      *
887      * @see #startHierarchyTracing(String, View)
888      * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
889      */
stopHierarchyTracing()890     public static void stopHierarchyTracing() {
891         //noinspection PointlessBooleanExpression,ConstantConditions
892         if (!TRACE_HIERARCHY) {
893             return;
894         }
895 
896         if (sHierarhcyRoot == null || sHierarchyTraces == null) {
897             throw new IllegalStateException("You must call startHierarchyTracing() before" +
898                 " stopHierarchyTracing()!");
899         }
900 
901         try {
902             sHierarchyTraces.close();
903         } catch (IOException e) {
904             Log.e("View", "Could not write view traces");
905         }
906         sHierarchyTraces = null;
907 
908         File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/");
909         //noinspection ResultOfMethodCallIgnored
910         hierarchyDump.mkdirs();
911         hierarchyDump = new File(hierarchyDump, sHierarchyTracePrefix + ".tree");
912 
913         BufferedWriter out;
914         try {
915             out = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
916         } catch (IOException e) {
917             Log.e("View", "Could not dump view hierarchy");
918             return;
919         }
920 
921         View view = sHierarhcyRoot.getView();
922         if (view instanceof ViewGroup) {
923             ViewGroup group = (ViewGroup) view;
924             dumpViewHierarchy(group, out, 0);
925             try {
926                 out.close();
927             } catch (IOException e) {
928                 Log.e("View", "Could not dump view hierarchy");
929             }
930         }
931 
932         sHierarhcyRoot = null;
933     }
934 
dispatchCommand(View view, String command, String parameters, OutputStream clientStream)935     static void dispatchCommand(View view, String command, String parameters,
936             OutputStream clientStream) throws IOException {
937 
938         // Paranoid but safe...
939         view = view.getRootView();
940 
941         if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
942             dump(view, clientStream);
943         } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) {
944             captureLayers(view, new DataOutputStream(clientStream));
945         } else {
946             final String[] params = parameters.split(" ");
947             if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
948                 capture(view, clientStream, params[0]);
949             } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) {
950                 outputDisplayList(view, params[0]);
951             } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
952                 invalidate(view, params[0]);
953             } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
954                 requestLayout(view, params[0]);
955             } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
956                 profile(view, clientStream, params[0]);
957             }
958         }
959     }
960 
findView(View root, String parameter)961     private static View findView(View root, String parameter) {
962         // Look by type/hashcode
963         if (parameter.indexOf('@') != -1) {
964             final String[] ids = parameter.split("@");
965             final String className = ids[0];
966             final int hashCode = (int) Long.parseLong(ids[1], 16);
967 
968             View view = root.getRootView();
969             if (view instanceof ViewGroup) {
970                 return findView((ViewGroup) view, className, hashCode);
971             }
972         } else {
973             // Look by id
974             final int id = root.getResources().getIdentifier(parameter, null, null);
975             return root.getRootView().findViewById(id);
976         }
977 
978         return null;
979     }
980 
invalidate(View root, String parameter)981     private static void invalidate(View root, String parameter) {
982         final View view = findView(root, parameter);
983         if (view != null) {
984             view.postInvalidate();
985         }
986     }
987 
requestLayout(View root, String parameter)988     private static void requestLayout(View root, String parameter) {
989         final View view = findView(root, parameter);
990         if (view != null) {
991             root.post(new Runnable() {
992                 public void run() {
993                     view.requestLayout();
994                 }
995             });
996         }
997     }
998 
profile(View root, OutputStream clientStream, String parameter)999     private static void profile(View root, OutputStream clientStream, String parameter)
1000             throws IOException {
1001 
1002         final View view = findView(root, parameter);
1003         BufferedWriter out = null;
1004         try {
1005             out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
1006 
1007             if (view != null) {
1008                 profileViewAndChildren(view, out);
1009             } else {
1010                 out.write("-1 -1 -1");
1011                 out.newLine();
1012             }
1013             out.write("DONE.");
1014             out.newLine();
1015         } catch (Exception e) {
1016             android.util.Log.w("View", "Problem profiling the view:", e);
1017         } finally {
1018             if (out != null) {
1019                 out.close();
1020             }
1021         }
1022     }
1023 
profileViewAndChildren(final View view, BufferedWriter out)1024     private static void profileViewAndChildren(final View view, BufferedWriter out)
1025             throws IOException {
1026         profileViewAndChildren(view, out, true);
1027     }
1028 
profileViewAndChildren(final View view, BufferedWriter out, boolean root)1029     private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root)
1030             throws IOException {
1031 
1032         long durationMeasure =
1033                 (root || (view.mPrivateFlags & View.MEASURED_DIMENSION_SET) != 0) ? profileViewOperation(
1034                         view, new ViewOperation<Void>() {
1035                             public Void[] pre() {
1036                                 forceLayout(view);
1037                                 return null;
1038                             }
1039 
1040                             private void forceLayout(View view) {
1041                                 view.forceLayout();
1042                                 if (view instanceof ViewGroup) {
1043                                     ViewGroup group = (ViewGroup) view;
1044                                     final int count = group.getChildCount();
1045                                     for (int i = 0; i < count; i++) {
1046                                         forceLayout(group.getChildAt(i));
1047                                     }
1048                                 }
1049                             }
1050 
1051                             public void run(Void... data) {
1052                                 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
1053                             }
1054 
1055                             public void post(Void... data) {
1056                             }
1057                         })
1058                         : 0;
1059         long durationLayout =
1060                 (root || (view.mPrivateFlags & View.LAYOUT_REQUIRED) != 0) ? profileViewOperation(
1061                         view, new ViewOperation<Void>() {
1062                             public Void[] pre() {
1063                                 return null;
1064                             }
1065 
1066                             public void run(Void... data) {
1067                                 view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom);
1068                             }
1069 
1070                             public void post(Void... data) {
1071                             }
1072                         }) : 0;
1073         long durationDraw =
1074                 (root || !view.willNotDraw() || (view.mPrivateFlags & View.DRAWN) != 0) ? profileViewOperation(
1075                         view,
1076                         new ViewOperation<Object>() {
1077                             public Object[] pre() {
1078                                 final DisplayMetrics metrics =
1079                                         (view != null && view.getResources() != null) ?
1080                                                 view.getResources().getDisplayMetrics() : null;
1081                                 final Bitmap bitmap = metrics != null ?
1082                                         Bitmap.createBitmap(metrics.widthPixels,
1083                                                 metrics.heightPixels, Bitmap.Config.RGB_565) : null;
1084                                 final Canvas canvas = bitmap != null ? new Canvas(bitmap) : null;
1085                                 return new Object[] {
1086                                         bitmap, canvas
1087                                 };
1088                             }
1089 
1090                             public void run(Object... data) {
1091                                 if (data[1] != null) {
1092                                     view.draw((Canvas) data[1]);
1093                                 }
1094                             }
1095 
1096                             public void post(Object... data) {
1097                                 if (data[1] != null) {
1098                                     ((Canvas) data[1]).setBitmap(null);
1099                                 }
1100                                 if (data[0] != null) {
1101                                     ((Bitmap) data[0]).recycle();
1102                                 }
1103                             }
1104                         }) : 0;
1105         out.write(String.valueOf(durationMeasure));
1106         out.write(' ');
1107         out.write(String.valueOf(durationLayout));
1108         out.write(' ');
1109         out.write(String.valueOf(durationDraw));
1110         out.newLine();
1111         if (view instanceof ViewGroup) {
1112             ViewGroup group = (ViewGroup) view;
1113             final int count = group.getChildCount();
1114             for (int i = 0; i < count; i++) {
1115                 profileViewAndChildren(group.getChildAt(i), out, false);
1116             }
1117         }
1118     }
1119 
1120     interface ViewOperation<T> {
pre()1121         T[] pre();
run(T... data)1122         void run(T... data);
post(T... data)1123         void post(T... data);
1124     }
1125 
profileViewOperation(View view, final ViewOperation<T> operation)1126     private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) {
1127         final CountDownLatch latch = new CountDownLatch(1);
1128         final long[] duration = new long[1];
1129 
1130         view.post(new Runnable() {
1131             public void run() {
1132                 try {
1133                     T[] data = operation.pre();
1134                     long start = Debug.threadCpuTimeNanos();
1135                     //noinspection unchecked
1136                     operation.run(data);
1137                     duration[0] = Debug.threadCpuTimeNanos() - start;
1138                     //noinspection unchecked
1139                     operation.post(data);
1140                 } finally {
1141                     latch.countDown();
1142                 }
1143             }
1144         });
1145 
1146         try {
1147             if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) {
1148                 Log.w("View", "Could not complete the profiling of the view " + view);
1149                 return -1;
1150             }
1151         } catch (InterruptedException e) {
1152             Log.w("View", "Could not complete the profiling of the view " + view);
1153             Thread.currentThread().interrupt();
1154             return -1;
1155         }
1156 
1157         return duration[0];
1158     }
1159 
captureLayers(View root, final DataOutputStream clientStream)1160     private static void captureLayers(View root, final DataOutputStream clientStream)
1161             throws IOException {
1162 
1163         try {
1164             Rect outRect = new Rect();
1165             try {
1166                 root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect);
1167             } catch (RemoteException e) {
1168                 // Ignore
1169             }
1170 
1171             clientStream.writeInt(outRect.width());
1172             clientStream.writeInt(outRect.height());
1173 
1174             captureViewLayer(root, clientStream, true);
1175 
1176             clientStream.write(2);
1177         } finally {
1178             clientStream.close();
1179         }
1180     }
1181 
captureViewLayer(View view, DataOutputStream clientStream, boolean visible)1182     private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
1183             throws IOException {
1184 
1185         final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
1186 
1187         if ((view.mPrivateFlags & View.SKIP_DRAW) != View.SKIP_DRAW) {
1188             final int id = view.getId();
1189             String name = view.getClass().getSimpleName();
1190             if (id != View.NO_ID) {
1191                 name = resolveId(view.getContext(), id).toString();
1192             }
1193 
1194             clientStream.write(1);
1195             clientStream.writeUTF(name);
1196             clientStream.writeByte(localVisible ? 1 : 0);
1197 
1198             int[] position = new int[2];
1199             // XXX: Should happen on the UI thread
1200             view.getLocationInWindow(position);
1201 
1202             clientStream.writeInt(position[0]);
1203             clientStream.writeInt(position[1]);
1204             clientStream.flush();
1205 
1206             Bitmap b = performViewCapture(view, true);
1207             if (b != null) {
1208                 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
1209                         b.getHeight() * 2);
1210                 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
1211                 clientStream.writeInt(arrayOut.size());
1212                 arrayOut.writeTo(clientStream);
1213             }
1214             clientStream.flush();
1215         }
1216 
1217         if (view instanceof ViewGroup) {
1218             ViewGroup group = (ViewGroup) view;
1219             int count = group.getChildCount();
1220 
1221             for (int i = 0; i < count; i++) {
1222                 captureViewLayer(group.getChildAt(i), clientStream, localVisible);
1223             }
1224         }
1225     }
1226 
outputDisplayList(View root, String parameter)1227     private static void outputDisplayList(View root, String parameter) throws IOException {
1228         final View view = findView(root, parameter);
1229         view.getViewRootImpl().outputDisplayList(view);
1230     }
1231 
capture(View root, final OutputStream clientStream, String parameter)1232     private static void capture(View root, final OutputStream clientStream, String parameter)
1233             throws IOException {
1234 
1235         final View captureView = findView(root, parameter);
1236         Bitmap b = performViewCapture(captureView, false);
1237 
1238         if (b == null) {
1239             Log.w("View", "Failed to create capture bitmap!");
1240             // Send an empty one so that it doesn't get stuck waiting for
1241             // something.
1242             b = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
1243         }
1244 
1245         BufferedOutputStream out = null;
1246         try {
1247             out = new BufferedOutputStream(clientStream, 32 * 1024);
1248             b.compress(Bitmap.CompressFormat.PNG, 100, out);
1249             out.flush();
1250         } finally {
1251             if (out != null) {
1252                 out.close();
1253             }
1254             b.recycle();
1255         }
1256     }
1257 
performViewCapture(final View captureView, final boolean skpiChildren)1258     private static Bitmap performViewCapture(final View captureView, final boolean skpiChildren) {
1259         if (captureView != null) {
1260             final CountDownLatch latch = new CountDownLatch(1);
1261             final Bitmap[] cache = new Bitmap[1];
1262 
1263             captureView.post(new Runnable() {
1264                 public void run() {
1265                     try {
1266                         cache[0] = captureView.createSnapshot(
1267                                 Bitmap.Config.ARGB_8888, 0, skpiChildren);
1268                     } catch (OutOfMemoryError e) {
1269                         Log.w("View", "Out of memory for bitmap");
1270                     } finally {
1271                         latch.countDown();
1272                     }
1273                 }
1274             });
1275 
1276             try {
1277                 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
1278                 return cache[0];
1279             } catch (InterruptedException e) {
1280                 Log.w("View", "Could not complete the capture of the view " + captureView);
1281                 Thread.currentThread().interrupt();
1282             }
1283         }
1284 
1285         return null;
1286     }
1287 
dump(View root, OutputStream clientStream)1288     private static void dump(View root, OutputStream clientStream) throws IOException {
1289         BufferedWriter out = null;
1290         try {
1291             out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
1292             View view = root.getRootView();
1293             if (view instanceof ViewGroup) {
1294                 ViewGroup group = (ViewGroup) view;
1295                 dumpViewHierarchyWithProperties(group.getContext(), group, out, 0);
1296             }
1297             out.write("DONE.");
1298             out.newLine();
1299         } catch (Exception e) {
1300             android.util.Log.w("View", "Problem dumping the view:", e);
1301         } finally {
1302             if (out != null) {
1303                 out.close();
1304             }
1305         }
1306     }
1307 
findView(ViewGroup group, String className, int hashCode)1308     private static View findView(ViewGroup group, String className, int hashCode) {
1309         if (isRequestedView(group, className, hashCode)) {
1310             return group;
1311         }
1312 
1313         final int count = group.getChildCount();
1314         for (int i = 0; i < count; i++) {
1315             final View view = group.getChildAt(i);
1316             if (view instanceof ViewGroup) {
1317                 final View found = findView((ViewGroup) view, className, hashCode);
1318                 if (found != null) {
1319                     return found;
1320                 }
1321             } else if (isRequestedView(view, className, hashCode)) {
1322                 return view;
1323             }
1324         }
1325 
1326         return null;
1327     }
1328 
isRequestedView(View view, String className, int hashCode)1329     private static boolean isRequestedView(View view, String className, int hashCode) {
1330         return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
1331     }
1332 
dumpViewHierarchyWithProperties(Context context, ViewGroup group, BufferedWriter out, int level)1333     private static void dumpViewHierarchyWithProperties(Context context, ViewGroup group,
1334             BufferedWriter out, int level) {
1335         if (!dumpViewWithProperties(context, group, out, level)) {
1336             return;
1337         }
1338 
1339         final int count = group.getChildCount();
1340         for (int i = 0; i < count; i++) {
1341             final View view = group.getChildAt(i);
1342             if (view instanceof ViewGroup) {
1343                 dumpViewHierarchyWithProperties(context, (ViewGroup) view, out, level + 1);
1344             } else {
1345                 dumpViewWithProperties(context, view, out, level + 1);
1346             }
1347         }
1348     }
1349 
dumpViewWithProperties(Context context, View view, BufferedWriter out, int level)1350     private static boolean dumpViewWithProperties(Context context, View view,
1351             BufferedWriter out, int level) {
1352 
1353         try {
1354             for (int i = 0; i < level; i++) {
1355                 out.write(' ');
1356             }
1357             out.write(view.getClass().getName());
1358             out.write('@');
1359             out.write(Integer.toHexString(view.hashCode()));
1360             out.write(' ');
1361             dumpViewProperties(context, view, out);
1362             out.newLine();
1363         } catch (IOException e) {
1364             Log.w("View", "Error while dumping hierarchy tree");
1365             return false;
1366         }
1367         return true;
1368     }
1369 
getExportedPropertyFields(Class<?> klass)1370     private static Field[] getExportedPropertyFields(Class<?> klass) {
1371         if (sFieldsForClasses == null) {
1372             sFieldsForClasses = new HashMap<Class<?>, Field[]>();
1373         }
1374         if (sAnnotations == null) {
1375             sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
1376         }
1377 
1378         final HashMap<Class<?>, Field[]> map = sFieldsForClasses;
1379 
1380         Field[] fields = map.get(klass);
1381         if (fields != null) {
1382             return fields;
1383         }
1384 
1385         final ArrayList<Field> foundFields = new ArrayList<Field>();
1386         fields = klass.getDeclaredFields();
1387 
1388         int count = fields.length;
1389         for (int i = 0; i < count; i++) {
1390             final Field field = fields[i];
1391             if (field.isAnnotationPresent(ExportedProperty.class)) {
1392                 field.setAccessible(true);
1393                 foundFields.add(field);
1394                 sAnnotations.put(field, field.getAnnotation(ExportedProperty.class));
1395             }
1396         }
1397 
1398         fields = foundFields.toArray(new Field[foundFields.size()]);
1399         map.put(klass, fields);
1400 
1401         return fields;
1402     }
1403 
getExportedPropertyMethods(Class<?> klass)1404     private static Method[] getExportedPropertyMethods(Class<?> klass) {
1405         if (sMethodsForClasses == null) {
1406             sMethodsForClasses = new HashMap<Class<?>, Method[]>(100);
1407         }
1408         if (sAnnotations == null) {
1409             sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
1410         }
1411 
1412         final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
1413 
1414         Method[] methods = map.get(klass);
1415         if (methods != null) {
1416             return methods;
1417         }
1418 
1419         final ArrayList<Method> foundMethods = new ArrayList<Method>();
1420         methods = klass.getDeclaredMethods();
1421 
1422         int count = methods.length;
1423         for (int i = 0; i < count; i++) {
1424             final Method method = methods[i];
1425             if (method.getParameterTypes().length == 0 &&
1426                     method.isAnnotationPresent(ExportedProperty.class) &&
1427                     method.getReturnType() != Void.class) {
1428                 method.setAccessible(true);
1429                 foundMethods.add(method);
1430                 sAnnotations.put(method, method.getAnnotation(ExportedProperty.class));
1431             }
1432         }
1433 
1434         methods = foundMethods.toArray(new Method[foundMethods.size()]);
1435         map.put(klass, methods);
1436 
1437         return methods;
1438     }
1439 
dumpViewProperties(Context context, Object view, BufferedWriter out)1440     private static void dumpViewProperties(Context context, Object view,
1441             BufferedWriter out) throws IOException {
1442 
1443         dumpViewProperties(context, view, out, "");
1444     }
1445 
dumpViewProperties(Context context, Object view, BufferedWriter out, String prefix)1446     private static void dumpViewProperties(Context context, Object view,
1447             BufferedWriter out, String prefix) throws IOException {
1448 
1449         Class<?> klass = view.getClass();
1450 
1451         do {
1452             exportFields(context, view, out, klass, prefix);
1453             exportMethods(context, view, out, klass, prefix);
1454             klass = klass.getSuperclass();
1455         } while (klass != Object.class);
1456     }
1457 
exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)1458     private static void exportMethods(Context context, Object view, BufferedWriter out,
1459             Class<?> klass, String prefix) throws IOException {
1460 
1461         final Method[] methods = getExportedPropertyMethods(klass);
1462 
1463         int count = methods.length;
1464         for (int i = 0; i < count; i++) {
1465             final Method method = methods[i];
1466             //noinspection EmptyCatchBlock
1467             try {
1468                 // TODO: This should happen on the UI thread
1469                 Object methodValue = method.invoke(view, (Object[]) null);
1470                 final Class<?> returnType = method.getReturnType();
1471                 final ExportedProperty property = sAnnotations.get(method);
1472                 String categoryPrefix =
1473                         property.category().length() != 0 ? property.category() + ":" : "";
1474 
1475                 if (returnType == int.class) {
1476 
1477                     if (property.resolveId() && context != null) {
1478                         final int id = (Integer) methodValue;
1479                         methodValue = resolveId(context, id);
1480                     } else {
1481                         final FlagToString[] flagsMapping = property.flagMapping();
1482                         if (flagsMapping.length > 0) {
1483                             final int intValue = (Integer) methodValue;
1484                             final String valuePrefix =
1485                                     categoryPrefix + prefix + method.getName() + '_';
1486                             exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1487                         }
1488 
1489                         final IntToString[] mapping = property.mapping();
1490                         if (mapping.length > 0) {
1491                             final int intValue = (Integer) methodValue;
1492                             boolean mapped = false;
1493                             int mappingCount = mapping.length;
1494                             for (int j = 0; j < mappingCount; j++) {
1495                                 final IntToString mapper = mapping[j];
1496                                 if (mapper.from() == intValue) {
1497                                     methodValue = mapper.to();
1498                                     mapped = true;
1499                                     break;
1500                                 }
1501                             }
1502 
1503                             if (!mapped) {
1504                                 methodValue = intValue;
1505                             }
1506                         }
1507                     }
1508                 } else if (returnType == int[].class) {
1509                     final int[] array = (int[]) methodValue;
1510                     final String valuePrefix = categoryPrefix + prefix + method.getName() + '_';
1511                     final String suffix = "()";
1512 
1513                     exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1514 
1515                     // Probably want to return here, same as for fields.
1516                     return;
1517                 } else if (!returnType.isPrimitive()) {
1518                     if (property.deepExport()) {
1519                         dumpViewProperties(context, methodValue, out, prefix + property.prefix());
1520                         continue;
1521                     }
1522                 }
1523 
1524                 writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue);
1525             } catch (IllegalAccessException e) {
1526             } catch (InvocationTargetException e) {
1527             }
1528         }
1529     }
1530 
exportFields(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)1531     private static void exportFields(Context context, Object view, BufferedWriter out,
1532             Class<?> klass, String prefix) throws IOException {
1533 
1534         final Field[] fields = getExportedPropertyFields(klass);
1535 
1536         int count = fields.length;
1537         for (int i = 0; i < count; i++) {
1538             final Field field = fields[i];
1539 
1540             //noinspection EmptyCatchBlock
1541             try {
1542                 Object fieldValue = null;
1543                 final Class<?> type = field.getType();
1544                 final ExportedProperty property = sAnnotations.get(field);
1545                 String categoryPrefix =
1546                         property.category().length() != 0 ? property.category() + ":" : "";
1547 
1548                 if (type == int.class) {
1549 
1550                     if (property.resolveId() && context != null) {
1551                         final int id = field.getInt(view);
1552                         fieldValue = resolveId(context, id);
1553                     } else {
1554                         final FlagToString[] flagsMapping = property.flagMapping();
1555                         if (flagsMapping.length > 0) {
1556                             final int intValue = field.getInt(view);
1557                             final String valuePrefix =
1558                                     categoryPrefix + prefix + field.getName() + '_';
1559                             exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1560                         }
1561 
1562                         final IntToString[] mapping = property.mapping();
1563                         if (mapping.length > 0) {
1564                             final int intValue = field.getInt(view);
1565                             int mappingCount = mapping.length;
1566                             for (int j = 0; j < mappingCount; j++) {
1567                                 final IntToString mapped = mapping[j];
1568                                 if (mapped.from() == intValue) {
1569                                     fieldValue = mapped.to();
1570                                     break;
1571                                 }
1572                             }
1573 
1574                             if (fieldValue == null) {
1575                                 fieldValue = intValue;
1576                             }
1577                         }
1578                     }
1579                 } else if (type == int[].class) {
1580                     final int[] array = (int[]) field.get(view);
1581                     final String valuePrefix = categoryPrefix + prefix + field.getName() + '_';
1582                     final String suffix = "";
1583 
1584                     exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
1585 
1586                     // We exit here!
1587                     return;
1588                 } else if (!type.isPrimitive()) {
1589                     if (property.deepExport()) {
1590                         dumpViewProperties(context, field.get(view), out, prefix
1591                                 + property.prefix());
1592                         continue;
1593                     }
1594                 }
1595 
1596                 if (fieldValue == null) {
1597                     fieldValue = field.get(view);
1598                 }
1599 
1600                 writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue);
1601             } catch (IllegalAccessException e) {
1602             }
1603         }
1604     }
1605 
writeEntry(BufferedWriter out, String prefix, String name, String suffix, Object value)1606     private static void writeEntry(BufferedWriter out, String prefix, String name,
1607             String suffix, Object value) throws IOException {
1608 
1609         out.write(prefix);
1610         out.write(name);
1611         out.write(suffix);
1612         out.write("=");
1613         writeValue(out, value);
1614         out.write(' ');
1615     }
1616 
exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, int intValue, String prefix)1617     private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
1618             int intValue, String prefix) throws IOException {
1619 
1620         final int count = mapping.length;
1621         for (int j = 0; j < count; j++) {
1622             final FlagToString flagMapping = mapping[j];
1623             final boolean ifTrue = flagMapping.outputIf();
1624             final int maskResult = intValue & flagMapping.mask();
1625             final boolean test = maskResult == flagMapping.equals();
1626             if ((test && ifTrue) || (!test && !ifTrue)) {
1627                 final String name = flagMapping.name();
1628                 final String value = "0x" + Integer.toHexString(maskResult);
1629                 writeEntry(out, prefix, name, "", value);
1630             }
1631         }
1632     }
1633 
exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix)1634     private static void exportUnrolledArray(Context context, BufferedWriter out,
1635             ExportedProperty property, int[] array, String prefix, String suffix)
1636             throws IOException {
1637 
1638         final IntToString[] indexMapping = property.indexMapping();
1639         final boolean hasIndexMapping = indexMapping.length > 0;
1640 
1641         final IntToString[] mapping = property.mapping();
1642         final boolean hasMapping = mapping.length > 0;
1643 
1644         final boolean resolveId = property.resolveId() && context != null;
1645         final int valuesCount = array.length;
1646 
1647         for (int j = 0; j < valuesCount; j++) {
1648             String name;
1649             String value = null;
1650 
1651             final int intValue = array[j];
1652 
1653             name = String.valueOf(j);
1654             if (hasIndexMapping) {
1655                 int mappingCount = indexMapping.length;
1656                 for (int k = 0; k < mappingCount; k++) {
1657                     final IntToString mapped = indexMapping[k];
1658                     if (mapped.from() == j) {
1659                         name = mapped.to();
1660                         break;
1661                     }
1662                 }
1663             }
1664 
1665             if (hasMapping) {
1666                 int mappingCount = mapping.length;
1667                 for (int k = 0; k < mappingCount; k++) {
1668                     final IntToString mapped = mapping[k];
1669                     if (mapped.from() == intValue) {
1670                         value = mapped.to();
1671                         break;
1672                     }
1673                 }
1674             }
1675 
1676             if (resolveId) {
1677                 if (value == null) value = (String) resolveId(context, intValue);
1678             } else {
1679                 value = String.valueOf(intValue);
1680             }
1681 
1682             writeEntry(out, prefix, name, suffix, value);
1683         }
1684     }
1685 
resolveId(Context context, int id)1686     static Object resolveId(Context context, int id) {
1687         Object fieldValue;
1688         final Resources resources = context.getResources();
1689         if (id >= 0) {
1690             try {
1691                 fieldValue = resources.getResourceTypeName(id) + '/' +
1692                         resources.getResourceEntryName(id);
1693             } catch (Resources.NotFoundException e) {
1694                 fieldValue = "id/0x" + Integer.toHexString(id);
1695             }
1696         } else {
1697             fieldValue = "NO_ID";
1698         }
1699         return fieldValue;
1700     }
1701 
writeValue(BufferedWriter out, Object value)1702     private static void writeValue(BufferedWriter out, Object value) throws IOException {
1703         if (value != null) {
1704             String output = value.toString().replace("\n", "\\n");
1705             out.write(String.valueOf(output.length()));
1706             out.write(",");
1707             out.write(output);
1708         } else {
1709             out.write("4,null");
1710         }
1711     }
1712 
dumpViewHierarchy(ViewGroup group, BufferedWriter out, int level)1713     private static void dumpViewHierarchy(ViewGroup group, BufferedWriter out, int level) {
1714         if (!dumpView(group, out, level)) {
1715             return;
1716         }
1717 
1718         final int count = group.getChildCount();
1719         for (int i = 0; i < count; i++) {
1720             final View view = group.getChildAt(i);
1721             if (view instanceof ViewGroup) {
1722                 dumpViewHierarchy((ViewGroup) view, out, level + 1);
1723             } else {
1724                 dumpView(view, out, level + 1);
1725             }
1726         }
1727     }
1728 
dumpView(Object view, BufferedWriter out, int level)1729     private static boolean dumpView(Object view, BufferedWriter out, int level) {
1730         try {
1731             for (int i = 0; i < level; i++) {
1732                 out.write(' ');
1733             }
1734             out.write(view.getClass().getName());
1735             out.write('@');
1736             out.write(Integer.toHexString(view.hashCode()));
1737             out.newLine();
1738         } catch (IOException e) {
1739             Log.w("View", "Error while dumping hierarchy tree");
1740             return false;
1741         }
1742         return true;
1743     }
1744 
capturedViewGetPropertyFields(Class<?> klass)1745     private static Field[] capturedViewGetPropertyFields(Class<?> klass) {
1746         if (mCapturedViewFieldsForClasses == null) {
1747             mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>();
1748         }
1749         final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses;
1750 
1751         Field[] fields = map.get(klass);
1752         if (fields != null) {
1753             return fields;
1754         }
1755 
1756         final ArrayList<Field> foundFields = new ArrayList<Field>();
1757         fields = klass.getFields();
1758 
1759         int count = fields.length;
1760         for (int i = 0; i < count; i++) {
1761             final Field field = fields[i];
1762             if (field.isAnnotationPresent(CapturedViewProperty.class)) {
1763                 field.setAccessible(true);
1764                 foundFields.add(field);
1765             }
1766         }
1767 
1768         fields = foundFields.toArray(new Field[foundFields.size()]);
1769         map.put(klass, fields);
1770 
1771         return fields;
1772     }
1773 
capturedViewGetPropertyMethods(Class<?> klass)1774     private static Method[] capturedViewGetPropertyMethods(Class<?> klass) {
1775         if (mCapturedViewMethodsForClasses == null) {
1776             mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>();
1777         }
1778         final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses;
1779 
1780         Method[] methods = map.get(klass);
1781         if (methods != null) {
1782             return methods;
1783         }
1784 
1785         final ArrayList<Method> foundMethods = new ArrayList<Method>();
1786         methods = klass.getMethods();
1787 
1788         int count = methods.length;
1789         for (int i = 0; i < count; i++) {
1790             final Method method = methods[i];
1791             if (method.getParameterTypes().length == 0 &&
1792                     method.isAnnotationPresent(CapturedViewProperty.class) &&
1793                     method.getReturnType() != Void.class) {
1794                 method.setAccessible(true);
1795                 foundMethods.add(method);
1796             }
1797         }
1798 
1799         methods = foundMethods.toArray(new Method[foundMethods.size()]);
1800         map.put(klass, methods);
1801 
1802         return methods;
1803     }
1804 
capturedViewExportMethods(Object obj, Class<?> klass, String prefix)1805     private static String capturedViewExportMethods(Object obj, Class<?> klass,
1806             String prefix) {
1807 
1808         if (obj == null) {
1809             return "null";
1810         }
1811 
1812         StringBuilder sb = new StringBuilder();
1813         final Method[] methods = capturedViewGetPropertyMethods(klass);
1814 
1815         int count = methods.length;
1816         for (int i = 0; i < count; i++) {
1817             final Method method = methods[i];
1818             try {
1819                 Object methodValue = method.invoke(obj, (Object[]) null);
1820                 final Class<?> returnType = method.getReturnType();
1821 
1822                 CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class);
1823                 if (property.retrieveReturn()) {
1824                     //we are interested in the second level data only
1825                     sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#"));
1826                 } else {
1827                     sb.append(prefix);
1828                     sb.append(method.getName());
1829                     sb.append("()=");
1830 
1831                     if (methodValue != null) {
1832                         final String value = methodValue.toString().replace("\n", "\\n");
1833                         sb.append(value);
1834                     } else {
1835                         sb.append("null");
1836                     }
1837                     sb.append("; ");
1838                 }
1839               } catch (IllegalAccessException e) {
1840                   //Exception IllegalAccess, it is OK here
1841                   //we simply ignore this method
1842               } catch (InvocationTargetException e) {
1843                   //Exception InvocationTarget, it is OK here
1844                   //we simply ignore this method
1845               }
1846         }
1847         return sb.toString();
1848     }
1849 
capturedViewExportFields(Object obj, Class<?> klass, String prefix)1850     private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) {
1851 
1852         if (obj == null) {
1853             return "null";
1854         }
1855 
1856         StringBuilder sb = new StringBuilder();
1857         final Field[] fields = capturedViewGetPropertyFields(klass);
1858 
1859         int count = fields.length;
1860         for (int i = 0; i < count; i++) {
1861             final Field field = fields[i];
1862             try {
1863                 Object fieldValue = field.get(obj);
1864 
1865                 sb.append(prefix);
1866                 sb.append(field.getName());
1867                 sb.append("=");
1868 
1869                 if (fieldValue != null) {
1870                     final String value = fieldValue.toString().replace("\n", "\\n");
1871                     sb.append(value);
1872                 } else {
1873                     sb.append("null");
1874                 }
1875                 sb.append(' ');
1876             } catch (IllegalAccessException e) {
1877                 //Exception IllegalAccess, it is OK here
1878                 //we simply ignore this field
1879             }
1880         }
1881         return sb.toString();
1882     }
1883 
1884     /**
1885      * Dump view info for id based instrument test generation
1886      * (and possibly further data analysis). The results are dumped
1887      * to the log.
1888      * @param tag for log
1889      * @param view for dump
1890      */
dumpCapturedView(String tag, Object view)1891     public static void dumpCapturedView(String tag, Object view) {
1892         Class<?> klass = view.getClass();
1893         StringBuilder sb = new StringBuilder(klass.getName() + ": ");
1894         sb.append(capturedViewExportFields(view, klass, ""));
1895         sb.append(capturedViewExportMethods(view, klass, ""));
1896         Log.d(tag, sb.toString());
1897     }
1898 }
1899