/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view; import android.util.Log; import android.util.DisplayMetrics; import android.content.res.Resources; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.os.Environment; import android.os.Debug; import java.io.File; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.io.FileOutputStream; import java.io.DataOutputStream; import java.io.OutputStreamWriter; import java.io.BufferedOutputStream; import java.io.OutputStream; import java.util.List; import java.util.LinkedList; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.lang.annotation.Target; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.AccessibleObject; /** * Various debugging/tracing tools related to {@link View} and the view hierarchy. */ public class ViewDebug { /** * Log tag used to log errors related to the consistency of the view hierarchy. * * @hide */ public static final String CONSISTENCY_LOG_TAG = "ViewConsistency"; /** * Flag indicating the consistency check should check layout-related properties. * * @hide */ public static final int CONSISTENCY_LAYOUT = 0x1; /** * Flag indicating the consistency check should check drawing-related properties. * * @hide */ public static final int CONSISTENCY_DRAWING = 0x2; /** * Enables or disables view hierarchy tracing. Any invoker of * {@link #trace(View, android.view.ViewDebug.HierarchyTraceType)} should first * check that this value is set to true as not to affect performance. */ public static final boolean TRACE_HIERARCHY = false; /** * Enables or disables view recycler tracing. Any invoker of * {@link #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])} should first * check that this value is set to true as not to affect performance. */ public static final boolean TRACE_RECYCLER = false; /** * The system property of dynamic switch for capturing view information * when it is set, we dump interested fields and methods for the view on focus */ static final String SYSTEM_PROPERTY_CAPTURE_VIEW = "debug.captureview"; /** * The system property of dynamic switch for capturing event information * when it is set, we log key events, touch/motion and trackball events */ static final String SYSTEM_PROPERTY_CAPTURE_EVENT = "debug.captureevent"; /** * Profiles drawing times in the events log. * * @hide */ @Debug.DebugProperty public static boolean profileDrawing = false; /** * Profiles layout times in the events log. * * @hide */ @Debug.DebugProperty public static boolean profileLayout = false; /** * Profiles real fps (times between draws) and displays the result. * * @hide */ @Debug.DebugProperty public static boolean showFps = false; /** *
Enables or disables views consistency check. Even when this property is enabled, * view consistency checks happen only if {@link android.util.Config#DEBUG} is set * to true. The value of this property can be configured externally in one of the * following files:
*
         * @ViewDebug.ExportedProperty(mapping = {
         *     @ViewDebug.IntToString(from = 0, to = "VISIBLE"),
         *     @ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
         *     @ViewDebug.IntToString(from = 8, to = "GONE")
         * })
         * public int getVisibility() { ...
         * 
         *
         * @return An array of int to String mappings
         *
         * @see android.view.ViewDebug.IntToString
         */
        IntToString[] mapping() default { };
        /**
         * A mapping can be defined to map array indices to specific strings.
         * A mapping can be used to see human readable values for the indices
         * of an array:
         *
         * 
         * @ViewDebug.ExportedProperty(indexMapping = {
         *     @ViewDebug.IntToString(from = 0, to = "INVALID"),
         *     @ViewDebug.IntToString(from = 1, to = "FIRST"),
         *     @ViewDebug.IntToString(from = 2, to = "SECOND")
         * })
         * private int[] mElements;
         * 
         *
         * @return An array of int to String mappings
         *
         * @see android.view.ViewDebug.IntToString
         * @see #mapping()
         */
        IntToString[] indexMapping() default { };
        /**
         * A flags mapping can be defined to map flags encoded in an integer to
         * specific strings. A mapping can be used to see human readable values
         * for the flags of an integer:
         *
         * 
         * @ViewDebug.ExportedProperty(flagMapping = {
         *     @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED, name = "ENABLED"),
         *     @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED, name = "DISABLED"),
         * })
         * private int mFlags;
         * 
         *
         * A specified String is output when the following is true:
         *
         * @return An array of int to String mappings
         */
        FlagToString[] flagMapping() default { };
        /**
         * When deep export is turned on, this property is not dumped. Instead, the
         * properties contained in this property are dumped. Each child property
         * is prefixed with the name of this property.
         *
         * @return true if the properties of this property should be dumped
         *
         * @see #prefix()
         */
        boolean deepExport() default false;
        /**
         * The prefix to use on child properties when deep export is enabled
         *
         * @return a prefix as a String
         *
         * @see #deepExport()
         */
        String prefix() default "";
    }
    /**
     * Defines a mapping from an int value to a String. Such a mapping can be used
     * in a @ExportedProperty to provide more meaningful values to the end user.
     *
     * @see android.view.ViewDebug.ExportedProperty
     */
    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface IntToString {
        /**
         * The original int value to map to a String.
         *
         * @return An arbitrary int value.
         */
        int from();
        /**
         * The String to use in place of the original int value.
         *
         * @return An arbitrary non-null String.
         */
        String to();
    }
    /**
     * Defines a mapping from an flag to a String. Such a mapping can be used
     * in a @ExportedProperty to provide more meaningful values to the end user.
     *
     * @see android.view.ViewDebug.ExportedProperty
     */
    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FlagToString {
        /**
         * The mask to apply to the original value.
         *
         * @return An arbitrary int value.
         */
        int mask();
        /**
         * The value to compare to the result of:
         * original value & {@link #mask()}.
         *
         * @return An arbitrary value.
         */
        int equals();
        /**
         * The String to use in place of the original int value.
         *
         * @return An arbitrary non-null String.
         */
        String name();
        /**
         * Indicates whether to output the flag when the test is true,
         * or false. Defaults to true.
         */
        boolean outputIf() default true;
    }
    /**
     * This annotation can be used to mark fields and methods to be dumped when
     * the view is captured. Methods with this annotation must have no arguments
     * and must return a valid type of data.
     */
    @Target({ ElementType.FIELD, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CapturedViewProperty {
        /**
         * When retrieveReturn is true, we need to retrieve second level methods
         * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
         * we will set retrieveReturn = true on the annotation of
         * myView.getFirstLevelMethod()
         * @return true if we need the second level methods
         */
        boolean retrieveReturn() default false;
    }
    private static HashMap, Method[]> mCapturedViewMethodsForClasses = null;
    private static HashMap, Field[]> mCapturedViewFieldsForClasses = null;
    // Maximum delay in ms after which we stop trying to capture a View's drawing
    private static final int CAPTURE_TIMEOUT = 4000;
    private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
    private static final String REMOTE_COMMAND_DUMP = "DUMP";
    private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
    private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
    private static final String REMOTE_PROFILE = "PROFILE";
    private static HashMap, Field[]> sFieldsForClasses;
    private static HashMap, Method[]> sMethodsForClasses;
    private static HashMap sAnnotations;
    /**
     * Defines the type of hierarhcy trace to output to the hierarchy traces file.
     */
    public enum HierarchyTraceType {
        INVALIDATE,
        INVALIDATE_CHILD,
        INVALIDATE_CHILD_IN_PARENT,
        REQUEST_LAYOUT,
        ON_LAYOUT,
        ON_MEASURE,
        DRAW,
        BUILD_CACHE
    }
    private static BufferedWriter sHierarchyTraces;
    private static ViewRoot sHierarhcyRoot;
    private static String sHierarchyTracePrefix;
    /**
     * Defines the type of recycler trace to output to the recycler traces file.
     */
    public enum RecyclerTraceType {
        NEW_VIEW,
        BIND_VIEW,
        RECYCLE_FROM_ACTIVE_HEAP,
        RECYCLE_FROM_SCRAP_HEAP,
        MOVE_TO_ACTIVE_HEAP,
        MOVE_TO_SCRAP_HEAP,
        MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
    }
    private static class RecyclerTrace {
        public int view;
        public RecyclerTraceType type;
        public int position;
        public int indexOnScreen;
    }
    private static View sRecyclerOwnerView;
    private static List sRecyclerViews;
    private static List sRecyclerTraces;
    private static String sRecyclerTracePrefix;
    /**
     * Returns the number of instanciated Views.
     *
     * @return The number of Views instanciated in the current process.
     *
     * @hide
     */
    public static long getViewInstanceCount() {
        return View.sInstanceCount;
    }
    /**
     * Returns the number of instanciated ViewRoots.
     *
     * @return The number of ViewRoots instanciated in the current process.
     *
     * @hide
     */
    public static long getViewRootInstanceCount() {
        return ViewRoot.getInstanceCount();
    }
    /**
     * Outputs a trace to the currently opened recycler traces. The trace records the type of
     * recycler action performed on the supplied view as well as a number of parameters.
     *
     * @param view the view to trace
     * @param type the type of the trace
     * @param parameters parameters depending on the type of the trace
     */
    public static void trace(View view, RecyclerTraceType type, int... parameters) {
        if (sRecyclerOwnerView == null || sRecyclerViews == null) {
            return;
        }
        if (!sRecyclerViews.contains(view)) {
            sRecyclerViews.add(view);
        }
        final int index = sRecyclerViews.indexOf(view);
        RecyclerTrace trace = new RecyclerTrace();
        trace.view = index;
        trace.type = type;
        trace.position = parameters[0];
        trace.indexOnScreen = parameters[1];
        sRecyclerTraces.add(trace);
    }
    /**
     * Starts tracing the view recycler of the specified view. The trace is identified by a prefix,
     * used to build the traces files names: /EXTERNAL/view-recycler/PREFIX.traces and
     * /EXTERNAL/view-recycler/PREFIX.recycler.
     *
     * Only one view recycler can be traced at the same time. After calling this method, any
     * other invocation will result in a IllegalStateException unless
     * {@link #stopRecyclerTracing()} is invoked before.
     *
     * Traces files are created only after {@link #stopRecyclerTracing()} is invoked.
     *
     * This method will return immediately if TRACE_RECYCLER is false.
     *
     * @param prefix the traces files name prefix
     * @param view the view whose recycler must be traced
     *
     * @see #stopRecyclerTracing()
     * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
     */
    public static void startRecyclerTracing(String prefix, View view) {
        //noinspection PointlessBooleanExpression,ConstantConditions
        if (!TRACE_RECYCLER) {
            return;
        }
        if (sRecyclerOwnerView != null) {
            throw new IllegalStateException("You must call stopRecyclerTracing() before running" +
                " a new trace!");
        }
        sRecyclerTracePrefix = prefix;
        sRecyclerOwnerView = view;
        sRecyclerViews = new ArrayList();
        sRecyclerTraces = new LinkedList();
    }
    /**
     * Stops the current view recycer tracing.
     *
     * Calling this method creates the file /EXTERNAL/view-recycler/PREFIX.traces
     * containing all the traces (or method calls) relative to the specified view's recycler.
     *
     * Calling this method creates the file /EXTERNAL/view-recycler/PREFIX.recycler
     * containing all of the views used by the recycler of the view supplied to
     * {@link #startRecyclerTracing(String, View)}.
     *
     * This method will return immediately if TRACE_RECYCLER is false.
     *
     * @see #startRecyclerTracing(String, View)
     * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
     */
    public static void stopRecyclerTracing() {
        //noinspection PointlessBooleanExpression,ConstantConditions
        if (!TRACE_RECYCLER) {
            return;
        }
        if (sRecyclerOwnerView == null || sRecyclerViews == null) {
            throw new IllegalStateException("You must call startRecyclerTracing() before" +
                " stopRecyclerTracing()!");
        }
        File recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/");
        //noinspection ResultOfMethodCallIgnored
        recyclerDump.mkdirs();
        recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".recycler");
        try {
            final BufferedWriter out = new BufferedWriter(new FileWriter(recyclerDump), 8 * 1024);
            for (View view : sRecyclerViews) {
                final String name = view.getClass().getName();
                out.write(name);
                out.newLine();
            }
            out.close();
        } catch (IOException e) {
            Log.e("View", "Could not dump recycler content");
            return;
        }
        recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/");
        recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces");
        try {
            final FileOutputStream file = new FileOutputStream(recyclerDump);
            final DataOutputStream out = new DataOutputStream(file);
            for (RecyclerTrace trace : sRecyclerTraces) {
                out.writeInt(trace.view);
                out.writeInt(trace.type.ordinal());
                out.writeInt(trace.position);
                out.writeInt(trace.indexOnScreen);
                out.flush();
            }
            out.close();
        } catch (IOException e) {
            Log.e("View", "Could not dump recycler traces");
            return;
        }
        sRecyclerViews.clear();
        sRecyclerViews = null;
        sRecyclerTraces.clear();
        sRecyclerTraces = null;
        sRecyclerOwnerView = null;
    }
    /**
     * Outputs a trace to the currently opened traces file. The trace contains the class name
     * and instance's hashcode of the specified view as well as the supplied trace type.
     *
     * @param view the view to trace
     * @param type the type of the trace
     */
    public static void trace(View view, HierarchyTraceType type) {
        if (sHierarchyTraces == null) {
            return;
        }
        try {
            sHierarchyTraces.write(type.name());
            sHierarchyTraces.write(' ');
            sHierarchyTraces.write(view.getClass().getName());
            sHierarchyTraces.write('@');
            sHierarchyTraces.write(Integer.toHexString(view.hashCode()));
            sHierarchyTraces.newLine();
        } catch (IOException e) {
            Log.w("View", "Error while dumping trace of type " + type + " for view " + view);
        }
    }
    /**
     * Starts tracing the view hierarchy of the specified view. The trace is identified by a prefix,
     * used to build the traces files names: /EXTERNAL/view-hierarchy/PREFIX.traces and
     * /EXTERNAL/view-hierarchy/PREFIX.tree.
     *
     * Only one view hierarchy can be traced at the same time. After calling this method, any
     * other invocation will result in a IllegalStateException unless
     * {@link #stopHierarchyTracing()} is invoked before.
     *
     * Calling this method creates the file /EXTERNAL/view-hierarchy/PREFIX.traces
     * containing all the traces (or method calls) relative to the specified view's hierarchy.
     *
     * This method will return immediately if TRACE_HIERARCHY is false.
     *
     * @param prefix the traces files name prefix
     * @param view the view whose hierarchy must be traced
     *
     * @see #stopHierarchyTracing()
     * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
     */
    public static void startHierarchyTracing(String prefix, View view) {
        //noinspection PointlessBooleanExpression,ConstantConditions
        if (!TRACE_HIERARCHY) {
            return;
        }
        if (sHierarhcyRoot != null) {
            throw new IllegalStateException("You must call stopHierarchyTracing() before running" +
                " a new trace!");
        }
        File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/");
        //noinspection ResultOfMethodCallIgnored
        hierarchyDump.mkdirs();
        hierarchyDump = new File(hierarchyDump, prefix + ".traces");
        sHierarchyTracePrefix = prefix;
        try {
            sHierarchyTraces = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
        } catch (IOException e) {
            Log.e("View", "Could not dump view hierarchy");
            return;
        }
        sHierarhcyRoot = (ViewRoot) view.getRootView().getParent();
    }
    /**
     * Stops the current view hierarchy tracing. This method closes the file
     * /EXTERNAL/view-hierarchy/PREFIX.traces.
     *
     * Calling this method creates the file /EXTERNAL/view-hierarchy/PREFIX.tree
     * containing the view hierarchy of the view supplied to
     * {@link #startHierarchyTracing(String, View)}.
     *
     * This method will return immediately if TRACE_HIERARCHY is false.
     *
     * @see #startHierarchyTracing(String, View)
     * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
     */
    public static void stopHierarchyTracing() {
        //noinspection PointlessBooleanExpression,ConstantConditions
        if (!TRACE_HIERARCHY) {
            return;
        }
        if (sHierarhcyRoot == null || sHierarchyTraces == null) {
            throw new IllegalStateException("You must call startHierarchyTracing() before" +
                " stopHierarchyTracing()!");
        }
        try {
            sHierarchyTraces.close();
        } catch (IOException e) {
            Log.e("View", "Could not write view traces");
        }
        sHierarchyTraces = null;
        File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/");
        //noinspection ResultOfMethodCallIgnored
        hierarchyDump.mkdirs();
        hierarchyDump = new File(hierarchyDump, sHierarchyTracePrefix + ".tree");
        BufferedWriter out;
        try {
            out = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
        } catch (IOException e) {
            Log.e("View", "Could not dump view hierarchy");
            return;
        }
        View view = sHierarhcyRoot.getView();
        if (view instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) view;
            dumpViewHierarchy(group, out, 0);
            try {
                out.close();
            } catch (IOException e) {
                Log.e("View", "Could not dump view hierarchy");
            }
        }
        sHierarhcyRoot = null;
    }
    static void dispatchCommand(View view, String command, String parameters,
            OutputStream clientStream) throws IOException {
        // Paranoid but safe...
        view = view.getRootView();
        if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
            dump(view, clientStream);
        } else {
            final String[] params = parameters.split(" ");
            if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
                capture(view, clientStream, params[0]);
            } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
                invalidate(view, params[0]);
            } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
                requestLayout(view, params[0]);
            } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
                profile(view, clientStream, params[0]);
            }
        }
    }
    private static View findView(View root, String parameter) {
        // Look by type/hashcode
        if (parameter.indexOf('@') != -1) {
            final String[] ids = parameter.split("@");
            final String className = ids[0];
            final int hashCode = Integer.parseInt(ids[1], 16);
            View view = root.getRootView();
            if (view instanceof ViewGroup) {
                return findView((ViewGroup) view, className, hashCode);
            }
        } else {
            // Look by id
            final int id = root.getResources().getIdentifier(parameter, null, null);
            return root.getRootView().findViewById(id);
        }
        return null;
    }
    private static void invalidate(View root, String parameter) {
        final View view = findView(root, parameter);
        if (view != null) {
            view.postInvalidate();
        }
    }
    private static void requestLayout(View root, String parameter) {
        final View view = findView(root, parameter);
        if (view != null) {
            root.post(new Runnable() {
                public void run() {
                    view.requestLayout();
                }
            });
        }
    }
    private static void profile(View root, OutputStream clientStream, String parameter)
            throws IOException {
        final View view = findView(root, parameter);
        BufferedWriter out = null;
        try {
            out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
            if (view != null) {
                final long durationMeasure = profileViewOperation(view, new ViewOperation() {
                    public Void[] pre() {
                        forceLayout(view);
                        return null;
                    }
                    private void forceLayout(View view) {
                        view.forceLayout();
                        if (view instanceof ViewGroup) {
                            ViewGroup group = (ViewGroup) view;
                            final int count = group.getChildCount();
                            for (int i = 0; i < count; i++) {
                                forceLayout(group.getChildAt(i));
                            }
                        }
                    }
                    public void run(Void... data) {
                        view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
                    }
                    public void post(Void... data) {
                    }
                });
                final long durationLayout = profileViewOperation(view, new ViewOperation() {
                    public Void[] pre() {
                        return null;
                    }
                    public void run(Void... data) {
                        view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom);
                    }
                    public void post(Void... data) {
                    }
                });
                final long durationDraw = profileViewOperation(view, new ViewOperation