Merge changes from topic "magnifier_sync_movement"

* changes:
  [Magnifier-24] Add completion callback TestApi
  [Magnifier-21] Rate-limit drawings to renderer
  [Magnifier-20] Raw Surface instead of PopupWindow
diff --git a/api/test-current.txt b/api/test-current.txt
index 049211d..121ca6e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1097,6 +1097,11 @@
     method public android.graphics.Bitmap getContent();
     method public static android.graphics.PointF getMagnifierDefaultSize();
     method public android.graphics.Rect getWindowPositionOnScreen();
+    method public void setOnOperationCompleteCallback(android.widget.Magnifier.Callback);
+  }
+
+  public static abstract interface Magnifier.Callback {
+    method public abstract void onOperationComplete();
   }
 
   public class NumberPicker extends android.widget.LinearLayout {
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index 59472d9..6da51d1 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -34,6 +34,7 @@
 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 com.android.internal.util.VirtualRefBasePtr;
@@ -943,6 +944,109 @@
         }
     }
 
+    /**
+     * Basic synchronous renderer. Currently only used to render the Magnifier, so use with care.
+     * TODO: deduplicate against ThreadedRenderer.
+     *
+     * @hide
+     */
+    public static class SimpleRenderer {
+        private final RenderNode mRootNode;
+        private long mNativeProxy;
+        private final float mLightY, mLightZ;
+        private Surface mSurface;
+        private final FrameInfo mFrameInfo = new FrameInfo();
+
+        public SimpleRenderer(final Context context, final String name, final Surface 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);
+            final float lightRadius = a.getDimension(R.styleable.Lighting_lightRadius, 0);
+            final int ambientShadowAlpha =
+                    (int) (255 * a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0) + 0.5f);
+            final int spotShadowAlpha =
+                    (int) (255 * a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0) + 0.5f);
+            a.recycle();
+
+            final long rootNodePtr = nCreateRootRenderNode();
+            mRootNode = RenderNode.adopt(rootNodePtr);
+            mRootNode.setClipToBounds(false);
+            mNativeProxy = nCreateProxy(true /* translucent */, rootNodePtr);
+            nSetName(mNativeProxy, name);
+
+            ProcessInitializer.sInstance.init(context, mNativeProxy);
+            nLoadSystemProperties(mNativeProxy);
+
+            nSetup(mNativeProxy, lightRadius, ambientShadowAlpha, spotShadowAlpha);
+
+            mSurface = surface;
+            nUpdateSurface(mNativeProxy, surface);
+        }
+
+        /**
+         * Set the light center.
+         */
+        public void setLightCenter(final Display display,
+                final int windowLeft, final int windowTop) {
+            // Adjust light position for window offsets.
+            final Point displaySize = new Point();
+            display.getRealSize(displaySize);
+            final float lightX = displaySize.x / 2f - windowLeft;
+            final float lightY = mLightY - windowTop;
+
+            nSetLightCenter(mNativeProxy, lightX, lightY, mLightZ);
+        }
+
+        public RenderNode getRootNode() {
+            return mRootNode;
+        }
+
+        /**
+         * Draw the surface.
+         */
+        public void draw(final FrameDrawingCallback callback) {
+            final long vsync = AnimationUtils.currentAnimationTimeMillis() * 1000000L;
+            mFrameInfo.setVsync(vsync, vsync);
+            mFrameInfo.addFlags(1 << 2 /* VSYNC */);
+            // TODO: remove this fence
+            nFence(mNativeProxy);
+            if (callback != null) {
+                callback.onFrameDraw(mSurface.getNextFrameNumber());
+            }
+            nSyncAndDrawFrame(mNativeProxy, mFrameInfo.mFrameInfo, mFrameInfo.mFrameInfo.length);
+        }
+
+        /**
+         * Destroy the renderer.
+         */
+        public void destroy() {
+            mSurface = null;
+            nDestroy(mNativeProxy, mRootNode.mNativeRenderNode);
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            try {
+                nDeleteProxy(mNativeProxy);
+                mNativeProxy = 0;
+            } finally {
+                super.finalize();
+            }
+        }
+    }
+
+    /**
+     * Interface used to receive callbacks when a frame is being drawn.
+     */
+    public interface FrameDrawingCallback {
+        /**
+         * Invoked during a frame drawing.
+         *
+         * @param frame The id of the frame being drawn.
+         */
+        void onFrameDraw(long frame);
+    }
+
     private static class ProcessInitializer {
         static ProcessInitializer sInstance = new ProcessInitializer();
 
diff --git a/core/java/android/widget/Magnifier.java b/core/java/android/widget/Magnifier.java
index 8836561..6427965 100644
--- a/core/java/android/widget/Magnifier.java
+++ b/core/java/android/widget/Magnifier.java
@@ -18,21 +18,32 @@
 
 import android.annotation.FloatRange;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.annotation.UiThread;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Handler;
-import android.view.Gravity;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.view.Display;
+import android.view.DisplayListCanvas;
 import android.view.LayoutInflater;
 import android.view.PixelCopy;
+import android.view.RenderNode;
 import android.view.Surface;
+import android.view.SurfaceControl;
 import android.view.SurfaceHolder;
+import android.view.SurfaceSession;
 import android.view.SurfaceView;
+import android.view.ThreadedRenderer;
 import android.view.View;
 import android.view.ViewParent;
 import android.view.ViewRootImpl;
@@ -46,32 +57,37 @@
 public final class Magnifier {
     // Use this to specify that a previous configuration value does not exist.
     private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
+    // The callbacks of the pixel copy requests will be invoked on
+    // the Handler of this Thread when the copy is finished.
+    private static final HandlerThread sPixelCopyHandlerThread =
+            new HandlerThread("magnifier pixel copy result handler");
+
     // The view to which this magnifier is attached.
     private final View mView;
     // The coordinates of the view in the surface.
     private final int[] mViewCoordinatesInSurface;
     // The window containing the magnifier.
-    private final PopupWindow mWindow;
+    private InternalPopupWindow mWindow;
     // The center coordinates of the window containing the magnifier.
     private final Point mWindowCoords = new Point();
     // The width of the window containing the magnifier.
     private final int mWindowWidth;
     // The height of the window containing the magnifier.
     private final int mWindowHeight;
-    // The bitmap used to display the contents of the magnifier.
-    private final Bitmap mBitmap;
+    // The width of the bitmaps where the magnifier content is copied.
+    private final int mBitmapWidth;
+    // The height of the bitmaps where the magnifier content is copied.
+    private final int mBitmapHeight;
+    // The elevation of the window containing the magnifier.
+    private final float mWindowElevation;
     // The center coordinates of the content that is to be magnified.
     private final Point mCenterZoomCoords = new Point();
-    // The callback of the pixel copy request will be invoked on this Handler when
-    // the copy is finished.
-    private final Handler mPixelCopyHandler = Handler.getMain();
-    // Current magnification scale.
-    private final float mZoomScale;
     // Variables holding previous states, used for detecting redundant calls and invalidation.
     private final Point mPrevStartCoordsInSurface = new Point(
             NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
     private final PointF mPrevPosInView = new PointF(
             NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
+    // Rectangle defining the view surface area we pixel copy content from.
     private final Rect mPixelCopyRequestRect = new Rect();
 
     /**
@@ -82,8 +98,6 @@
     public Magnifier(@NonNull View view) {
         mView = Preconditions.checkNotNull(view);
         final Context context = mView.getContext();
-        final float elevation = context.getResources().getDimension(
-                com.android.internal.R.dimen.magnifier_elevation);
         final View content = LayoutInflater.from(context).inflate(
                 com.android.internal.R.layout.magnifier, null);
         content.findViewById(com.android.internal.R.id.magnifier_inner).setClipToOutline(true);
@@ -91,23 +105,18 @@
                 com.android.internal.R.dimen.magnifier_width);
         mWindowHeight = context.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.magnifier_height);
-        mZoomScale = context.getResources().getFloat(
+        mWindowElevation = context.getResources().getDimension(
+                com.android.internal.R.dimen.magnifier_elevation);
+        final float zoomScale = context.getResources().getFloat(
                 com.android.internal.R.dimen.magnifier_zoom_scale);
+        mBitmapWidth = Math.round(mWindowWidth / zoomScale);
+        mBitmapHeight = Math.round(mWindowHeight / zoomScale);
         // The view's surface coordinates will not be updated until the magnifier is first shown.
         mViewCoordinatesInSurface = new int[2];
+    }
 
-        mWindow = new PopupWindow(context);
-        mWindow.setContentView(content);
-        mWindow.setWidth(mWindowWidth);
-        mWindow.setHeight(mWindowHeight);
-        mWindow.setElevation(elevation);
-        mWindow.setTouchable(false);
-        mWindow.setBackgroundDrawable(null);
-
-        final int bitmapWidth = Math.round(mWindowWidth / mZoomScale);
-        final int bitmapHeight = Math.round(mWindowHeight / mZoomScale);
-        mBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
-        getImageView().setImageBitmap(mBitmap);
+    static {
+        sPixelCopyHandlerThread.start();
     }
 
     /**
@@ -155,30 +164,45 @@
         }
 
         final int startX = Math.max(zeroScrollXInSurface, Math.min(
-                mCenterZoomCoords.x - mBitmap.getWidth() / 2,
-                zeroScrollXInSurface + actualWidth - mBitmap.getWidth()));
-        final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
+                mCenterZoomCoords.x - mBitmapWidth / 2,
+                zeroScrollXInSurface + actualWidth - mBitmapWidth));
+        final int startY = mCenterZoomCoords.y - mBitmapHeight / 2;
 
         if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) {
-            performPixelCopy(startX, startY);
-
+            if (mWindow == null) {
+                mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
+                        getValidViewSurface(), mWindowWidth, mWindowHeight, mWindowElevation,
+                        Handler.getMain() /* draw the magnifier on the UI thread */, mCallback);
+            }
+            performPixelCopy(startX, startY, true /* update window position */);
             mPrevPosInView.x = xPosInView;
             mPrevPosInView.y = yPosInView;
-
-            if (mWindow.isShowing()) {
-                mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(),
-                        mWindow.getHeight());
-            } else {
-                mWindow.showAtLocation(mView, Gravity.NO_GRAVITY, mWindowCoords.x, mWindowCoords.y);
-            }
         }
     }
 
+    @Nullable
+    private Surface getValidViewSurface() {
+        // TODO: deduplicate this against the first part of #performPixelCopy
+        final Surface surface;
+        if (mView instanceof SurfaceView) {
+            surface = ((SurfaceView) mView).getHolder().getSurface();
+        } else if (mView.getViewRootImpl() != null) {
+            surface = mView.getViewRootImpl().mSurface;
+        } else {
+            surface = null;
+        }
+
+        return (surface != null && surface.isValid()) ? surface : null;
+    }
+
     /**
      * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
      */
     public void dismiss() {
-        mWindow.dismiss();
+        if (mWindow != null) {
+            mWindow.destroy();
+            mWindow = null;
+        }
     }
 
     /**
@@ -186,43 +210,40 @@
      * {@link #show(float, float)}. This only happens if the magnifier is currently showing.
      */
     public void update() {
-        if (mWindow.isShowing()) {
-            // Update the contents shown in the magnifier.
-            performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y);
+        if (mWindow != null) {
+            // Update the content shown in the magnifier.
+            performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
+                    false /* update window position */);
         }
     }
 
     private void configureCoordinates(final float xPosInView, final float yPosInView) {
         // Compute the coordinates of the center of the content going to be displayed in the
         // magnifier. These are relative to the surface the content is copied from.
-        final float contentPosX;
-        final float contentPosY;
+        final float posX;
+        final float posY;
         if (mView instanceof SurfaceView) {
             // No offset required if the backing Surface matches the size of the SurfaceView.
-            contentPosX = xPosInView;
-            contentPosY = yPosInView;
+            posX = xPosInView;
+            posY = yPosInView;
         } else {
             mView.getLocationInSurface(mViewCoordinatesInSurface);
-            contentPosX = xPosInView + mViewCoordinatesInSurface[0];
-            contentPosY = yPosInView + mViewCoordinatesInSurface[1];
+            posX = xPosInView + mViewCoordinatesInSurface[0];
+            posY = yPosInView + mViewCoordinatesInSurface[1];
         }
-        mCenterZoomCoords.x = Math.round(contentPosX);
-        mCenterZoomCoords.y = Math.round(contentPosY);
+        mCenterZoomCoords.x = Math.round(posX);
+        mCenterZoomCoords.y = Math.round(posY);
 
-        // Compute the position of the magnifier window. These have to be relative to the window
-        // of the view the magnifier is attached to, as the magnifier popup is a panel window
-        // attached to that window.
-        final int[] viewCoordinatesInWindow = new int[2];
-        mView.getLocationInWindow(viewCoordinatesInWindow);
+        // Compute the position of the magnifier window. Again, this has to be relative to the
+        // surface of the magnified view, as this surface is the parent of the magnifier surface.
         final int verticalOffset = mView.getContext().getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.magnifier_offset);
-        final float magnifierPosX = xPosInView + viewCoordinatesInWindow[0];
-        final float magnifierPosY = yPosInView + viewCoordinatesInWindow[1] - verticalOffset;
-        mWindowCoords.x = Math.round(magnifierPosX - mWindowWidth / 2);
-        mWindowCoords.y = Math.round(magnifierPosY - mWindowHeight / 2);
+        mWindowCoords.x = mCenterZoomCoords.x - mWindowWidth / 2;
+        mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalOffset;
     }
 
-    private void performPixelCopy(final int startXInSurface, final int startYInSurface) {
+    private void performPixelCopy(final int startXInSurface, final int startYInSurface,
+            final boolean updateWindowPosition) {
         // Get the view surface where the content will be copied from.
         final Surface surface;
         final int surfaceWidth;
@@ -256,20 +277,293 @@
         // Perform the pixel copy.
         mPixelCopyRequestRect.set(clampedStartXInSurface,
                 clampedStartYInSurface,
-                clampedStartXInSurface + mBitmap.getWidth(),
-                clampedStartYInSurface + mBitmap.getHeight());
-        PixelCopy.request(surface, mPixelCopyRequestRect, mBitmap,
+                clampedStartXInSurface + mBitmapWidth,
+                clampedStartYInSurface + mBitmapHeight);
+        final int windowCoordsX = mWindowCoords.x;
+        final int windowCoordsY = mWindowCoords.y;
+
+        final Bitmap bitmap =
+                Bitmap.createBitmap(mBitmapWidth, mBitmapHeight, Bitmap.Config.ARGB_8888);
+        PixelCopy.request(surface, mPixelCopyRequestRect, bitmap,
                 result -> {
-                    getImageView().invalidate();
-                    mPrevStartCoordsInSurface.x = startXInSurface;
-                    mPrevStartCoordsInSurface.y = startYInSurface;
+                    synchronized (mWindow.mLock) {
+                        if (mWindow != null) {
+                            if (updateWindowPosition) {
+                                // TODO: pull the position update outside #performPixelCopy
+                                mWindow.setContentPositionForNextDraw(windowCoordsX, windowCoordsY);
+                            }
+                            mWindow.updateContent(bitmap);
+                        }
+                    }
                 },
-                mPixelCopyHandler);
+                sPixelCopyHandlerThread.getThreadHandler());
+        mPrevStartCoordsInSurface.x = startXInSurface;
+        mPrevStartCoordsInSurface.y = startYInSurface;
     }
 
-    private ImageView getImageView() {
-        return mWindow.getContentView().findViewById(
-                com.android.internal.R.id.magnifier_image);
+    /**
+     * Magnifier's own implementation of PopupWindow-similar floating window.
+     * This exists to ensure frame-synchronization between window position updates and window
+     * content updates. By using a PopupWindow, these events would happen in different frames,
+     * producing a shakiness effect for the magnifier content.
+     */
+    private static class InternalPopupWindow {
+        // Display associated to the view the magnifier is attached to.
+        private final Display mDisplay;
+        // The size of the content of the magnifier.
+        private final int mContentWidth;
+        private final int mContentHeight;
+        // The size of the allocated surface.
+        private final int mSurfaceWidth;
+        private final int mSurfaceHeight;
+        // The insets of the content inside the allocated surface.
+        private final int mOffsetX;
+        private final int mOffsetY;
+        // The surface we allocate for the magnifier content + shadow.
+        private final SurfaceSession mSurfaceSession;
+        private final SurfaceControl mSurfaceControl;
+        private final Surface mSurface;
+        // The renderer used for the allocated surface.
+        private final ThreadedRenderer.SimpleRenderer mRenderer;
+        // The RenderNode used to draw the magnifier content in the surface.
+        private final RenderNode mBitmapRenderNode;
+        // The job that will be post'd to apply the pending magnifier updates to the surface.
+        private final Runnable mMagnifierUpdater;
+        // The handler where the magnifier updater jobs will be post'd.
+        private final Handler mHandler;
+        // The callback to be run after the next draw. Only used for testing.
+        private Callback mCallback;
+
+        // Members below describe the state of the magnifier. Reads/writes to them
+        // have to be synchronized between the UI thread and the thread that handles
+        // the pixel copy results. This is the purpose of mLock.
+        private final Object mLock = new Object();
+        // Whether a magnifier frame draw is currently pending in the UI thread queue.
+        private boolean mFrameDrawScheduled;
+        // The content bitmap.
+        private Bitmap mBitmap;
+        // Whether the next draw will be the first one for the current instance.
+        private boolean mFirstDraw = true;
+        // The window position in the parent surface. Might be applied during the next draw,
+        // when mPendingWindowPositionUpdate is true.
+        private int mWindowPositionX;
+        private int mWindowPositionY;
+        private boolean mPendingWindowPositionUpdate;
+
+        InternalPopupWindow(final Context context, final Display display,
+                final Surface parentSurface,
+                final int width, final int height, final float elevation,
+                final Handler handler, final Callback callback) {
+            mDisplay = display;
+            mCallback = callback;
+
+            mContentWidth = width;
+            mContentHeight = height;
+            mOffsetX = (int) (0.1f * width);
+            mOffsetY = (int) (0.1f * height);
+            // Setup the surface we will use for drawing the content and shadow.
+            mSurfaceWidth = mContentWidth + 2 * mOffsetX;
+            mSurfaceHeight = mContentHeight + 2 * mOffsetY;
+            mSurfaceSession = new SurfaceSession(parentSurface);
+            mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
+                    .setFormat(PixelFormat.TRANSLUCENT)
+                    .setSize(mSurfaceWidth, mSurfaceHeight)
+                    .setName("magnifier surface")
+                    .setFlags(SurfaceControl.HIDDEN)
+                    .build();
+            mSurface = new Surface();
+            mSurface.copyFrom(mSurfaceControl);
+
+            // Setup the RenderNode tree. The root has only one child, which contains the bitmap.
+            mRenderer = new ThreadedRenderer.SimpleRenderer(
+                    context,
+                    "magnifier renderer",
+                    mSurface
+            );
+            mBitmapRenderNode = createRenderNodeForBitmap(
+                    "magnifier content",
+                    elevation
+            );
+
+            final DisplayListCanvas canvas = mRenderer.getRootNode().start(width, height);
+            try {
+                canvas.insertReorderBarrier();
+                canvas.drawRenderNode(mBitmapRenderNode);
+                canvas.insertInorderBarrier();
+            } finally {
+                mRenderer.getRootNode().end(canvas);
+            }
+
+            // Initialize the update job and the handler where this will be post'd.
+            mHandler = handler;
+            mMagnifierUpdater = this::doDraw;
+            mFrameDrawScheduled = false;
+        }
+
+        private RenderNode createRenderNodeForBitmap(final String name, final float elevation) {
+            final RenderNode bitmapRenderNode = RenderNode.create(name, null);
+
+            // Define the position of the bitmap in the parent render node. The surface regions
+            // outside the bitmap are used to draw elevation.
+            bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
+                    mOffsetX + mContentWidth, mOffsetY + mContentHeight);
+            bitmapRenderNode.setElevation(elevation);
+
+            final Outline outline = new Outline();
+            outline.setRoundRect(0, 0, mContentWidth, mContentHeight, 3);
+            outline.setAlpha(1.0f);
+            bitmapRenderNode.setOutline(outline);
+            bitmapRenderNode.setClipToOutline(true);
+
+            // Create a dummy draw, which will be replaced later with real drawing.
+            final DisplayListCanvas canvas = bitmapRenderNode.start(mContentWidth, mContentHeight);
+            try {
+                canvas.drawColor(0xFF00FF00);
+            } finally {
+                bitmapRenderNode.end(canvas);
+            }
+
+            return bitmapRenderNode;
+        }
+
+        /**
+         * Sets the position of the magnifier content relative to the parent surface.
+         * The position update will happen in the same frame with the next draw.
+         * The method has to be called in a context that holds {@link #mLock}.
+         *
+         * @param contentX the x coordinate of the content
+         * @param contentY the y coordinate of the content
+         */
+        public void setContentPositionForNextDraw(final int contentX, final int contentY) {
+            mWindowPositionX = contentX - mOffsetX;
+            mWindowPositionY = contentY - mOffsetY;
+            mPendingWindowPositionUpdate = true;
+            requestUpdate();
+        }
+
+        /**
+         * Sets the content that should be displayed in the magnifier.
+         * The update happens immediately, and possibly triggers a pending window movement set
+         * by {@link #setContentPositionForNextDraw(int, int)}.
+         * The method has to be called in a context that holds {@link #mLock}.
+         *
+         * @param bitmap the content bitmap
+         */
+        public void updateContent(final @NonNull Bitmap bitmap) {
+            if (mBitmap != null) {
+                mBitmap.recycle();
+            }
+            mBitmap = bitmap;
+            requestUpdate();
+        }
+
+        private void requestUpdate() {
+            if (mFrameDrawScheduled) {
+                return;
+            }
+            final Message request = Message.obtain(mHandler, mMagnifierUpdater);
+            request.setAsynchronous(true);
+            request.sendToTarget();
+            mFrameDrawScheduled = true;
+        }
+
+        /**
+         * Destroys this instance.
+         */
+        public void destroy() {
+            mRenderer.destroy();
+            mSurface.destroy();
+            mSurfaceControl.destroy();
+            mSurfaceSession.kill();
+            mBitmapRenderNode.destroy();
+            synchronized (mLock) {
+                mHandler.removeCallbacks(mMagnifierUpdater);
+                if (mBitmap != null) {
+                    mBitmap.recycle();
+                }
+            }
+        }
+
+        private void doDraw() {
+            final ThreadedRenderer.FrameDrawingCallback callback;
+
+            // Draw the current bitmap to the surface, and prepare the callback which updates the
+            // surface position. These have to be in the same synchronized block, in order to
+            // guarantee the consistency between the bitmap content and the surface position.
+            synchronized (mLock) {
+                if (!mSurface.isValid()) {
+                    // Probably #destroy() was called for the current instance, so we skip the draw.
+                    return;
+                }
+
+                final DisplayListCanvas canvas =
+                        mBitmapRenderNode.start(mContentWidth, mContentHeight);
+                try {
+                    final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+                    final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
+                    final Paint paint = new Paint();
+                    paint.setFilterBitmap(true);
+                    canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
+                } finally {
+                    mBitmapRenderNode.end(canvas);
+                }
+
+                if (mPendingWindowPositionUpdate || mFirstDraw) {
+                    // If the window has to be shown or moved, defer this until the next draw.
+                    final boolean firstDraw = mFirstDraw;
+                    mFirstDraw = false;
+                    final boolean updateWindowPosition = mPendingWindowPositionUpdate;
+                    mPendingWindowPositionUpdate = false;
+                    final int pendingX = mWindowPositionX;
+                    final int pendingY = mWindowPositionY;
+
+                    callback = frame -> {
+                        mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
+                        // Show or move the window at the content draw frame.
+                        SurfaceControl.openTransaction();
+                        mSurfaceControl.deferTransactionUntil(mSurface, frame);
+                        if (updateWindowPosition) {
+                            mSurfaceControl.setPosition(pendingX, pendingY);
+                        }
+                        if (firstDraw) {
+                            mSurfaceControl.show();
+                        }
+                        SurfaceControl.closeTransaction();
+                    };
+                } else {
+                    callback = null;
+                }
+
+                mFrameDrawScheduled = false;
+            }
+
+            mRenderer.draw(callback);
+            if (mCallback != null) {
+                mCallback.onOperationComplete();
+            }
+        }
+    }
+
+    // The rest of the file consists of test APIs.
+
+    /**
+     * See {@link #setOnOperationCompleteCallback(Callback)}.
+     */
+    @TestApi
+    private Callback mCallback;
+
+    /**
+     * Sets a callback which will be invoked at the end of the next
+     * {@link #show(float, float)} or {@link #update()} operation.
+     *
+     * @hide
+     */
+    @TestApi
+    public void setOnOperationCompleteCallback(final Callback callback) {
+        mCallback = callback;
+        if (mWindow != null) {
+            mWindow.mCallback = callback;
+        }
     }
 
     /**
@@ -278,8 +572,13 @@
      * @hide
      */
     @TestApi
-    public Bitmap getContent() {
-        return mBitmap;
+    public @Nullable Bitmap getContent() {
+        if (mWindow == null) {
+            return null;
+        }
+        synchronized (mWindow.mLock) {
+            return mWindow.mBitmap;
+        }
     }
 
     /**
@@ -296,7 +595,7 @@
 
         final int left = mWindowCoords.x + viewLocationOnScreen[0] - viewLocationInSurface[0];
         final int top = mWindowCoords.y + viewLocationOnScreen[1] - viewLocationInSurface[1];
-        return new Rect(left, top, left + mWindow.getWidth(), top + mWindow.getHeight());
+        return new Rect(left, top, left + mWindowWidth, top + mWindowHeight);
     }
 
     /**
@@ -313,4 +612,15 @@
         size.y = resources.getDimension(com.android.internal.R.dimen.magnifier_height) / density;
         return size;
     }
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public interface Callback {
+        /**
+         * Callback called after the drawing for a magnifier update has happened.
+         */
+        void onOperationComplete();
+    }
 }