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