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