/*
 * Copyright (C) 2013 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.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.BLASTBufferQueue;
import android.graphics.FrameInfo;
import android.graphics.HardwareRenderer;
import android.graphics.Picture;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.os.Trace;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Surface.OutOfResourcesException;
import android.view.View.AttachInfo;
import android.view.animation.AnimationUtils;

import com.android.internal.R;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;

/**
 * Threaded renderer that proxies the rendering to a render thread. Most calls
 * are currently synchronous.
 *
 * The UI thread can block on the RenderThread, but RenderThread must never
 * block on the UI thread.
 *
 * ThreadedRenderer creates an instance of RenderProxy. RenderProxy in turn creates
 * and manages a CanvasContext on the RenderThread. The CanvasContext is fully managed
 * by the lifecycle of the RenderProxy.
 *
 * Note that although currently the EGL context & surfaces are created & managed
 * by the render thread, the goal is to move that into a shared structure that can
 * be managed by both threads. EGLSurface creation & deletion should ideally be
 * done on the UI thread and not the RenderThread to avoid stalling the
 * RenderThread with surface buffer allocation.
 *
 * @hide
 */
public final class ThreadedRenderer extends HardwareRenderer {
    /**
     * System property used to enable or disable threaded rendering profiling.
     * The default value of this property is assumed to be false.
     *
     * When profiling is enabled, the adb shell dumpsys gfxinfo command will
     * output extra information about the time taken to execute by the last
     * frames.
     *
     * Possible values:
     * "true", to enable profiling
     * "visual_bars", to enable profiling and visualize the results on screen
     * "false", to disable profiling
     *
     * @see #PROFILE_PROPERTY_VISUALIZE_BARS
     *
     * @hide
     */
    public static final String PROFILE_PROPERTY = "debug.hwui.profile";

    /**
     * Value for {@link #PROFILE_PROPERTY}. When the property is set to this
     * value, profiling data will be visualized on screen as a bar chart.
     *
     * @hide
     */
    public static final String PROFILE_PROPERTY_VISUALIZE_BARS = "visual_bars";

    /**
     * System property used to specify the number of frames to be used
     * when doing threaded rendering profiling.
     * The default value of this property is #PROFILE_MAX_FRAMES.
     *
     * When profiling is enabled, the adb shell dumpsys gfxinfo command will
     * output extra information about the time taken to execute by the last
     * frames.
     *
     * Possible values:
     * "60", to set the limit of frames to 60
     */
    static final String PROFILE_MAXFRAMES_PROPERTY = "debug.hwui.profile.maxframes";

    /**
     * System property used to debug EGL configuration choice.
     *
     * Possible values:
     * "choice", print the chosen configuration only
     * "all", print all possible configurations
     */
    static final String PRINT_CONFIG_PROPERTY = "debug.hwui.print_config";

    /**
     * Turn on to draw dirty regions every other frame.
     *
     * Possible values:
     * "true", to enable dirty regions debugging
     * "false", to disable dirty regions debugging
     *
     * @hide
     */
    public static final String DEBUG_DIRTY_REGIONS_PROPERTY = "debug.hwui.show_dirty_regions";

    /**
     * Turn on to flash hardware layers when they update.
     *
     * Possible values:
     * "true", to enable hardware layers updates debugging
     * "false", to disable hardware layers updates debugging
     *
     * @hide
     */
    public static final String DEBUG_SHOW_LAYERS_UPDATES_PROPERTY =
            "debug.hwui.show_layers_updates";

    /**
     * Controls overdraw debugging.
     *
     * Possible values:
     * "false", to disable overdraw debugging
     * "show", to show overdraw areas on screen
     * "count", to display an overdraw counter
     *
     * @hide
     */
    public static final String DEBUG_OVERDRAW_PROPERTY = "debug.hwui.overdraw";

    /**
     * Value for {@link #DEBUG_OVERDRAW_PROPERTY}. When the property is set to this
     * value, overdraw will be shown on screen by coloring pixels.
     *
     * @hide
     */
    public static final String OVERDRAW_PROPERTY_SHOW = "show";

    /**
     * Turn on to debug non-rectangular clip operations.
     *
     * Possible values:
     * "hide", to disable this debug mode
     * "highlight", highlight drawing commands tested against a non-rectangular clip
     * "stencil", renders the clip region on screen when set
     *
     * @hide
     */
    public static final String DEBUG_SHOW_NON_RECTANGULAR_CLIP_PROPERTY =
            "debug.hwui.show_non_rect_clip";

    /**
     * Sets the FPS devisor to lower the FPS.
     *
     * Sets a positive integer as a divisor. 1 (the default value) menas the full FPS, and 2
     * means half the full FPS.
     *
     *
     * @hide
     */
    public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor";

    /**
     * Forces smart-dark to be always on.
     * @hide
     */
    public static final String DEBUG_FORCE_DARK = "debug.hwui.force_dark";

    public static int EGL_CONTEXT_PRIORITY_REALTIME_NV = 0x3357;
    public static int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101;
    public static int EGL_CONTEXT_PRIORITY_MEDIUM_IMG = 0x3102;
    public static int EGL_CONTEXT_PRIORITY_LOW_IMG = 0x3103;

    /**
     * Further threaded renderer disabling for the system process.
     *
     * @hide
     */
    public static boolean sRendererEnabled = true;

    /**
     * Controls whether or not the renderer should aggressively trim
     * memory. Note that this must not be set for any process that uses
     * WebView! This should be only used by system_process or similar
     * that do not go into the background.
     */
    public static void enableForegroundTrimming() {
        // TODO: Remove
    }


    /**
     * Initialize HWUI for being in a system process like system_server
     * Should not be called in non-system processes
     */
    public static void initForSystemProcess() {
        // The system process on low-memory devices do not get to use hardware
        // accelerated drawing, since this can add too much overhead to the
        // process.
        if (!ActivityManager.isHighEndGfx()) {
            sRendererEnabled = false;
        }
        setIsSystemOrPersistent();
    }

    /**
     * Creates a threaded renderer using OpenGL.
     *
     * @param translucent True if the surface is translucent, false otherwise
     *
     * @return A threaded renderer backed by OpenGL.
     */
    public static ThreadedRenderer create(Context context, boolean translucent, String name) {
        return new ThreadedRenderer(context, translucent, name);
    }

    private static final String[] VISUALIZERS = {
        PROFILE_PROPERTY_VISUALIZE_BARS,
    };

    // Size of the rendered content.
    private int mWidth, mHeight;

    // Actual size of the drawing surface.
    private int mSurfaceWidth, mSurfaceHeight;

    // Insets between the drawing surface and rendered content. These are
    // applied as translation when updating the root render node.
    private int mInsetTop, mInsetLeft;

    // Light properties specified by the theme.
    private final float mLightY;
    private final float mLightZ;
    private final float mLightRadius;

    private boolean mInitialized = false;
    private boolean mRootNodeNeedsUpdate;

    private boolean mEnabled;
    private boolean mRequested = true;

    /**
     * This child class exists to break ownership cycles. ViewRootImpl owns a ThreadedRenderer
     * which owns a WebViewOverlayProvider. WebViewOverlayProvider will in turn be set as
     * the listener for HardwareRenderer callbacks. By keeping this a child class, there are
     * no cycles in the chain. The ThreadedRenderer will remain GC-able if any callbacks are
     * still outstanding, which will in turn release any JNI references to WebViewOverlayProvider.
     */
    private static final class WebViewOverlayProvider implements
            PrepareSurfaceControlForWebviewCallback, ASurfaceTransactionCallback {
        private static final boolean sOverlaysAreEnabled =
                HardwareRenderer.isWebViewOverlaysEnabled();
        private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
        private boolean mHasWebViewOverlays = false;
        private BLASTBufferQueue mBLASTBufferQueue;
        private SurfaceControl mSurfaceControl;

        public boolean setSurfaceControlOpaque(boolean opaque) {
            synchronized (this) {
                if (mHasWebViewOverlays) return false;
                mTransaction.setOpaque(mSurfaceControl, opaque).apply();
            }
            return opaque;
        }

        public boolean shouldEnableOverlaySupport() {
            return sOverlaysAreEnabled && mSurfaceControl != null && mBLASTBufferQueue != null;
        }

        public void setSurfaceControl(SurfaceControl surfaceControl) {
            synchronized (this) {
                mSurfaceControl = surfaceControl;
                if (mSurfaceControl != null && mHasWebViewOverlays) {
                    mTransaction.setOpaque(surfaceControl, false).apply();
                }
            }
        }

        public void setBLASTBufferQueue(BLASTBufferQueue bufferQueue) {
            synchronized (this) {
                mBLASTBufferQueue = bufferQueue;
            }
        }

        @Override
        public void prepare() {
            synchronized (this) {
                mHasWebViewOverlays = true;
                if (mSurfaceControl != null) {
                    mTransaction.setOpaque(mSurfaceControl, false).apply();
                }
            }
        }

        @Override
        public boolean onMergeTransaction(long nativeTransactionObj,
                long aSurfaceControlNativeObj, long frameNr) {
            synchronized (this) {
                if (mBLASTBufferQueue == null) {
                    return false;
                } else {
                    mBLASTBufferQueue.mergeWithNextTransaction(nativeTransactionObj, frameNr);
                    return true;
                }
            }
        }
    }

    private final WebViewOverlayProvider mWebViewOverlayProvider = new WebViewOverlayProvider();
    private boolean mWebViewOverlaysEnabled = false;

    @Nullable
    private ArrayList<FrameDrawingCallback> mNextRtFrameCallbacks;

    ThreadedRenderer(Context context, boolean translucent, String name) {
        super();
        setName(name);
        setOpaque(!translucent);

        final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
        mLightY = a.getDimension(R.styleable.Lighting_lightY, 0);
        mLightZ = a.getDimension(R.styleable.Lighting_lightZ, 0);
        mLightRadius = a.getDimension(R.styleable.Lighting_lightRadius, 0);
        float ambientShadowAlpha = a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0);
        float spotShadowAlpha = a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0);
        a.recycle();
        setLightSourceAlpha(ambientShadowAlpha, spotShadowAlpha);
    }

    @Override
    public void destroy() {
        mInitialized = false;
        updateEnabledState(null);
        super.destroy();
    }

    /**
     * Indicates whether threaded rendering is currently enabled.
     *
     * @return True if threaded rendering  is in use, false otherwise.
     */
    boolean isEnabled() {
        return mEnabled;
    }

    /**
     * Indicates whether threaded rendering  is currently enabled.
     *
     * @param enabled True if the threaded renderer is in use, false otherwise.
     */
    void setEnabled(boolean enabled) {
        mEnabled = enabled;
    }

    /**
     * Indicates whether threaded rendering is currently request but not
     * necessarily enabled yet.
     *
     * @return True if requested, false otherwise.
     */
    boolean isRequested() {
        return mRequested;
    }

    /**
     * Indicates whether threaded rendering is currently requested but not
     * necessarily enabled yet.
     */
    void setRequested(boolean requested) {
        mRequested = requested;
    }

    private void updateEnabledState(Surface surface) {
        if (surface == null || !surface.isValid()) {
            setEnabled(false);
        } else {
            setEnabled(mInitialized);
        }
    }

    /**
     * Initializes the threaded renderer for the specified surface.
     *
     * @param surface The surface to render
     *
     * @return True if the initialization was successful, false otherwise.
     */
    boolean initialize(Surface surface) throws OutOfResourcesException {
        boolean status = !mInitialized;
        mInitialized = true;
        updateEnabledState(surface);
        setSurface(surface);
        return status;
    }

    /**
     * Initializes the threaded renderer for the specified surface and setup the
     * renderer for drawing, if needed. This is invoked when the ViewAncestor has
     * potentially lost the threaded renderer. The threaded renderer should be
     * reinitialized and setup when the render {@link #isRequested()} and
     * {@link #isEnabled()}.
     *
     * @param width The width of the drawing surface.
     * @param height The height of the drawing surface.
     * @param attachInfo Information about the window.
     * @param surface The surface to render
     * @param surfaceInsets The drawing surface insets to apply
     *
     * @return true if the surface was initialized, false otherwise. Returning
     *         false might mean that the surface was already initialized.
     */
    boolean initializeIfNeeded(int width, int height, View.AttachInfo attachInfo,
            Surface surface, Rect surfaceInsets) throws OutOfResourcesException {
        if (isRequested()) {
            // We lost the gl context, so recreate it.
            if (!isEnabled()) {
                if (initialize(surface)) {
                    setup(width, height, attachInfo, surfaceInsets);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Updates the threaded renderer for the specified surface.
     *
     * @param surface The surface to render
     */
    void updateSurface(Surface surface) throws OutOfResourcesException {
        updateEnabledState(surface);
        setSurface(surface);
    }

    @Override
    public void setSurface(Surface surface) {
        // TODO: Do we ever pass a non-null but isValid() = false surface?
        // This is here to be super conservative for ViewRootImpl
        if (surface != null && surface.isValid()) {
            super.setSurface(surface);
        } else {
            super.setSurface(null);
        }
    }

    /**
     * Registers a callback to be executed when the next frame is being drawn on RenderThread. This
     * callback will be executed on a RenderThread worker thread, and only used for the next frame
     * and thus it will only fire once.
     *
     * @param callback The callback to register.
     */
    void registerRtFrameCallback(@NonNull FrameDrawingCallback callback) {
        if (mNextRtFrameCallbacks == null) {
            mNextRtFrameCallbacks = new ArrayList<>();
        }
        mNextRtFrameCallbacks.add(callback);
    }

    /**
     * Remove a frame drawing callback that was added via
     * {@link #registerRtFrameCallback(FrameDrawingCallback)}
     *
     * @param callback The callback to unregister.
     */
    void unregisterRtFrameCallback(@NonNull FrameDrawingCallback callback) {
        if (mNextRtFrameCallbacks == null) {
            return;
        }
        mNextRtFrameCallbacks.remove(callback);
    }

    /**
     * Destroys all hardware rendering resources associated with the specified
     * view hierarchy.
     *
     * @param view The root of the view hierarchy
     */
    void destroyHardwareResources(View view) {
        destroyResources(view);
        clearContent();
    }

    private static void destroyResources(View view) {
        view.destroyHardwareResources();
    }

    /**
     * Sets up the renderer for drawing.
     *
     * @param width The width of the drawing surface.
     * @param height The height of the drawing surface.
     * @param attachInfo Information about the window.
     * @param surfaceInsets The drawing surface insets to apply
     */
    void setup(int width, int height, AttachInfo attachInfo, Rect surfaceInsets) {
        mWidth = width;
        mHeight = height;

        if (surfaceInsets != null && (surfaceInsets.left != 0 || surfaceInsets.right != 0
                || surfaceInsets.top != 0 || surfaceInsets.bottom != 0)) {
            mInsetLeft = surfaceInsets.left;
            mInsetTop = surfaceInsets.top;
            mSurfaceWidth = width + mInsetLeft + surfaceInsets.right;
            mSurfaceHeight = height + mInsetTop + surfaceInsets.bottom;

            // If the surface has insets, it can't be opaque.
            setOpaque(false);
        } else {
            mInsetLeft = 0;
            mInsetTop = 0;
            mSurfaceWidth = width;
            mSurfaceHeight = height;
        }

        mRootNode.setLeftTopRightBottom(-mInsetLeft, -mInsetTop, mSurfaceWidth, mSurfaceHeight);

        setLightCenter(attachInfo);
    }

    /**
     * Whether or not the renderer owns the SurfaceControl's opacity. If true, use
     * {@link #setSurfaceControlOpaque(boolean)} to update the opacity
     */
    public boolean rendererOwnsSurfaceControlOpacity() {
        return mWebViewOverlayProvider.mSurfaceControl != null;
    }

    /**
     * Sets the SurfaceControl's opacity that this HardwareRenderer is rendering onto. The renderer
     * may opt to override the opacity, and will return the value that is ultimately set
     *
     * @return true if the surface is opaque, false otherwise
     *
     * @hide
     */
    public boolean setSurfaceControlOpaque(boolean opaque) {
        return mWebViewOverlayProvider.setSurfaceControlOpaque(opaque);
    }

    private void updateWebViewOverlayCallbacks() {
        boolean shouldEnable = mWebViewOverlayProvider.shouldEnableOverlaySupport();
        if (shouldEnable != mWebViewOverlaysEnabled) {
            mWebViewOverlaysEnabled = shouldEnable;
            if (shouldEnable) {
                setASurfaceTransactionCallback(mWebViewOverlayProvider);
                setPrepareSurfaceControlForWebviewCallback(mWebViewOverlayProvider);
            } else {
                setASurfaceTransactionCallback(null);
                setPrepareSurfaceControlForWebviewCallback(null);
            }
        }
    }

    @Override
    public void setSurfaceControl(@Nullable SurfaceControl surfaceControl,
            @Nullable BLASTBufferQueue blastBufferQueue) {
        super.setSurfaceControl(surfaceControl, blastBufferQueue);
        mWebViewOverlayProvider.setSurfaceControl(surfaceControl);
        mWebViewOverlayProvider.setBLASTBufferQueue(blastBufferQueue);
        updateWebViewOverlayCallbacks();
    }

    @Override
    public void notifyCallbackPending() {
        if (isEnabled()) {
            super.notifyCallbackPending();
        }
    }

    @Override
    public void notifyExpensiveFrame() {
        if (isEnabled()) {
            super.notifyExpensiveFrame();
        }
    }

    /**
     * Updates the light position based on the position of the window.
     *
     * @param attachInfo Information about the window.
     */
    void setLightCenter(AttachInfo attachInfo) {
        // Adjust light position for window offsets.
        DisplayMetrics displayMetrics = new DisplayMetrics();
        attachInfo.mDisplay.getRealMetrics(displayMetrics);
        final float lightX = displayMetrics.widthPixels / 2f - attachInfo.mWindowLeft;
        final float lightY = mLightY - attachInfo.mWindowTop;
        // To prevent shadow distortion on larger screens, scale the z position of the light source
        // relative to the smallest screen dimension.
        final float zRatio = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels)
                / (450f * displayMetrics.density);
        final float zWeightedAdjustment = (zRatio + 2) / 3f;
        final float lightZ = mLightZ * zWeightedAdjustment;

        setLightSourceGeometry(lightX, lightY, lightZ, mLightRadius);
    }

    /**
     * Gets the current width of the surface. This is the width that the surface
     * was last set to in a call to {@link #setup(int, int, View.AttachInfo, Rect)}.
     *
     * @return the current width of the surface
     */
    int getWidth() {
        return mWidth;
    }

    /**
     * Gets the current height of the surface. This is the height that the surface
     * was last set to in a call to {@link #setup(int, int, View.AttachInfo, Rect)}.
     *
     * @return the current width of the surface
     */
    int getHeight() {
        return mHeight;
    }

    private static int dumpArgsToFlags(String[] args) {
        // If there's no arguments, eg 'dumpsys gfxinfo', then dump everything.
        // If there's a targetted package, eg 'dumpsys gfxinfo com.android.systemui', then only
        // dump the summary information
        if (args == null || args.length == 0) {
            return FLAG_DUMP_ALL;
        }
        int flags = 0;
        for (int i = 0; i < args.length; i++) {
            switch (args[i]) {
                case "framestats":
                    flags |= FLAG_DUMP_FRAMESTATS;
                    break;
                case "reset":
                    flags |= FLAG_DUMP_RESET;
                    break;
                case "-a": // magic option passed when dumping a bugreport.
                    flags = FLAG_DUMP_ALL;
                    break;
            }
        }
        return flags;
    }

    /** @hide */
    public static void handleDumpGfxInfo(FileDescriptor fd, String[] args) {
        dumpGlobalProfileInfo(fd, dumpArgsToFlags(args));
        WindowManagerGlobal.getInstance().dumpGfxInfo(fd, args);
    }

    /**
     * Outputs extra debugging information in the specified file descriptor.
     */
    void dumpGfxInfo(PrintWriter pw, FileDescriptor fd, String[] args) {
        pw.flush();
        dumpProfileInfo(fd, dumpArgsToFlags(args));
    }

    Picture captureRenderingCommands() {
        return null;
    }

    @Override
    public boolean loadSystemProperties() {
        boolean changed = super.loadSystemProperties();
        if (changed) {
            invalidateRoot();
        }
        return changed;
    }

    private void updateViewTreeDisplayList(View view) {
        view.mPrivateFlags |= View.PFLAG_DRAWN;
        view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
                == View.PFLAG_INVALIDATED;
        view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
        view.updateDisplayListIfDirty();
        view.mRecreateDisplayList = false;
    }

    private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
        updateViewTreeDisplayList(view);

        // Consume and set the frame callback after we dispatch draw to the view above, but before
        // onPostDraw below which may reset the callback for the next frame.  This ensures that
        // updates to the frame callback during scroll handling will also apply in this frame.
        if (mNextRtFrameCallbacks != null) {
            final ArrayList<FrameDrawingCallback> frameCallbacks = mNextRtFrameCallbacks;
            mNextRtFrameCallbacks = null;
            setFrameCallback(new FrameDrawingCallback() {
                @Override
                public void onFrameDraw(long frame) {
                }

                @Override
                public FrameCommitCallback onFrameDraw(int syncResult, long frame) {
                    ArrayList<FrameCommitCallback> frameCommitCallbacks = new ArrayList<>();
                    for (int i = 0; i < frameCallbacks.size(); ++i) {
                        FrameCommitCallback frameCommitCallback = frameCallbacks.get(i)
                                .onFrameDraw(syncResult, frame);
                        if (frameCommitCallback != null) {
                            frameCommitCallbacks.add(frameCommitCallback);
                        }
                    }

                    if (frameCommitCallbacks.isEmpty()) {
                        return null;
                    }

                    return didProduceBuffer -> {
                        for (int i = 0; i < frameCommitCallbacks.size(); ++i) {
                            frameCommitCallbacks.get(i).onFrameCommit(didProduceBuffer);
                        }
                    };
                }
            });
        }

        if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
            RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);
            try {
                final int saveCount = canvas.save();
                canvas.translate(mInsetLeft, mInsetTop);
                callbacks.onPreDraw(canvas);

                canvas.enableZ();
                canvas.drawRenderNode(view.updateDisplayListIfDirty());
                canvas.disableZ();

                callbacks.onPostDraw(canvas);
                canvas.restoreToCount(saveCount);
                mRootNodeNeedsUpdate = false;
            } finally {
                mRootNode.endRecording();
            }
        }
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

    /**
     * Interface used to receive callbacks whenever a view is drawn by
     * a threaded renderer instance.
     */
    interface DrawCallbacks {
        /**
         * Invoked before a view is drawn by a threaded renderer.
         * This method can be used to apply transformations to the
         * canvas but no drawing command should be issued.
         *
         * @param canvas The Canvas used to render the view.
         */
        void onPreDraw(RecordingCanvas canvas);

        /**
         * Invoked after a view is drawn by a threaded renderer.
         * It is safe to invoke drawing commands from this method.
         *
         * @param canvas The Canvas used to render the view.
         */
        void onPostDraw(RecordingCanvas canvas);
    }

    /**
     *  Indicates that the content drawn by DrawCallbacks needs to
     *  be updated, which will be done by the next call to draw()
     */
    void invalidateRoot() {
        mRootNodeNeedsUpdate = true;
    }

    /**
     * Draws the specified view.
     *
     * @param view The view to draw.
     * @param attachInfo AttachInfo tied to the specified view.
     */
    void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
        attachInfo.mViewRootImpl.mViewFrameInfo.markDrawStart();

        updateRootDisplayList(view, callbacks);

        // register animating rendernodes which started animating prior to renderer
        // creation, which is typical for animators started prior to first draw
        if (attachInfo.mPendingAnimatingRenderNodes != null) {
            final int count = attachInfo.mPendingAnimatingRenderNodes.size();
            for (int i = 0; i < count; i++) {
                registerAnimatingRenderNode(
                        attachInfo.mPendingAnimatingRenderNodes.get(i));
            }
            attachInfo.mPendingAnimatingRenderNodes.clear();
            // We don't need this anymore as subsequent calls to
            // ViewRootImpl#attachRenderNodeAnimator will go directly to us.
            attachInfo.mPendingAnimatingRenderNodes = null;
        }

        final FrameInfo frameInfo = attachInfo.mViewRootImpl.getUpdatedFrameInfo();

        int syncResult = syncAndDrawFrame(frameInfo);
        if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
            Log.w("OpenGLRenderer", "Surface lost, forcing relayout");
            // We lost our surface. For a relayout next frame which should give us a new
            // surface from WindowManager, which hopefully will work.
            attachInfo.mViewRootImpl.mForceNextWindowRelayout = true;
            attachInfo.mViewRootImpl.requestLayout();
        }
        if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
            attachInfo.mViewRootImpl.invalidate();
        }
    }

    /** The root of everything */
    public @NonNull RenderNode getRootNode() {
        return mRootNode;
    }

    /**
     * Basic synchronous renderer. Currently only used to render the Magnifier, so use with care.
     * TODO: deduplicate against ThreadedRenderer.
     *
     * @hide
     */
    public static class SimpleRenderer extends HardwareRenderer {
        private final float mLightY, mLightZ, mLightRadius;

        public SimpleRenderer(final Context context, final String name, final Surface surface) {
            super();
            setName(name);
            setOpaque(false);
            setSurface(surface);
            final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
            mLightY = a.getDimension(R.styleable.Lighting_lightY, 0);
            mLightZ = a.getDimension(R.styleable.Lighting_lightZ, 0);
            mLightRadius = a.getDimension(R.styleable.Lighting_lightRadius, 0);
            final float ambientShadowAlpha = a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0);
            final float spotShadowAlpha = a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0);
            a.recycle();
            setLightSourceAlpha(ambientShadowAlpha, spotShadowAlpha);
        }

        /**
         * Set the light center.
         */
        public void setLightCenter(final Display display,
                final int windowLeft, final int windowTop) {
            // Adjust light position for window offsets.
            DisplayMetrics displayMetrics = new DisplayMetrics();
            display.getRealMetrics(displayMetrics);
            final float lightX = displayMetrics.widthPixels / 2f - windowLeft;
            final float lightY = mLightY - windowTop;
            // To prevent shadow distortion on larger screens, scale the z position of the light
            // source relative to the smallest screen dimension.
            final float zRatio = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels)
                    / (450f * displayMetrics.density);
            final float zWeightedAdjustment = (zRatio + 2) / 3f;
            final float lightZ = mLightZ * zWeightedAdjustment;

            setLightSourceGeometry(lightX, lightY, lightZ, mLightRadius);
        }

        public RenderNode getRootNode() {
            return mRootNode;
        }

        /**
         * Draw the surface.
         */
        public void draw(final FrameDrawingCallback callback) {
            final long vsync = AnimationUtils.currentAnimationTimeMillis() * 1000000L;
            if (callback != null) {
                setFrameCallback(callback);
            }
            createRenderRequest()
                    .setVsyncTime(vsync)
                    .syncAndDraw();
        }
    }
}