Automatic sources dropoff on 2020-06-10 18:32:38.095721

The change is generated with prebuilt drop tool.

Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java
new file mode 100644
index 0000000..8ea824d
--- /dev/null
+++ b/android/widget/Magnifier.java
@@ -0,0 +1,1720 @@
+/*
+ * Copyright (C) 2017 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.widget;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Px;
+import android.annotation.TestApi;
+import android.annotation.UiThread;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.PixelCopy;
+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.ViewRootImpl;
+
+import com.android.internal.R;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Android magnifier widget. Can be used by any view which is attached to a window.
+ */
+@UiThread
+public final class Magnifier {
+    private static final String TAG = "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 width of the ramp region in DP on the left & right sides of the fish-eye effect.
+    private static final float FISHEYE_RAMP_WIDTH = 12f;
+
+    // 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 InternalPopupWindow mWindow;
+    // The width of the window containing the magnifier.
+    private final int mWindowWidth;
+    // The height of the window containing the magnifier.
+    private int mWindowHeight;
+    // The zoom applied to the view region copied to the magnifier view.
+    private float mZoom;
+    // The width of the content that will be copied to the magnifier.
+    private int mSourceWidth;
+    // The height of the content that will be copied to the magnifier.
+    private int mSourceHeight;
+    // Whether the zoom of the magnifier or the view position have changed since last content copy.
+    private boolean mDirtyState;
+    // The elevation of the window containing the magnifier.
+    private final float mWindowElevation;
+    // The corner radius of the window containing the magnifier.
+    private final float mWindowCornerRadius;
+    // The overlay to be drawn on the top of the magnifier content.
+    private final Drawable mOverlay;
+    // The horizontal offset between the source and window coords when #show(float, float) is used.
+    private final int mDefaultHorizontalSourceToMagnifierOffset;
+    // The vertical offset between the source and window coords when #show(float, float) is used.
+    private final int mDefaultVerticalSourceToMagnifierOffset;
+    // Whether the area where the magnifier can be positioned will be clipped to the main window
+    // and within system insets.
+    private final boolean mClippingEnabled;
+    // The behavior of the left bound of the rectangle where the content can be copied from.
+    private @SourceBound int mLeftContentBound;
+    // The behavior of the top bound of the rectangle where the content can be copied from.
+    private @SourceBound int mTopContentBound;
+    // The behavior of the right bound of the rectangle where the content can be copied from.
+    private @SourceBound int mRightContentBound;
+    // The behavior of the bottom bound of the rectangle where the content can be copied from.
+    private @SourceBound int mBottomContentBound;
+    // The parent surface for the magnifier surface.
+    private SurfaceInfo mParentSurface;
+    // The surface where the content will be copied from.
+    private SurfaceInfo mContentCopySurface;
+    // The center coordinates of the window containing the magnifier.
+    private final Point mWindowCoords = new Point();
+    // The center coordinates of the content to be magnified,
+    // clamped inside the visible region of the magnified view.
+    private final Point mClampedCenterZoomCoords = new Point();
+    // 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 mPrevShowSourceCoords = new PointF(
+            NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
+    private final PointF mPrevShowWindowCoords = 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();
+    // Lock to synchronize between the UI thread and the thread that handles pixel copy results.
+    // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread.
+    private final Object mLock = new Object();
+    // The lock used to synchronize the UI and render threads when a #dismiss is performed.
+    private final Object mDestroyLock = new Object();
+
+    // Members for new styled magnifier (Eloquent style).
+
+    // Whether the magnifier is in new style.
+    private boolean mIsFishEyeStyle;
+    // The width of the cut region on the left edge of the pixel copy source rect.
+    private int mLeftCutWidth = 0;
+    // The width of the cut region on the right edge of the pixel copy source rect.
+    private int mRightCutWidth = 0;
+    // The horizontal bounds of the content source in pixels, relative to the view.
+    private int mLeftBound = Integer.MIN_VALUE;
+    private int mRightBound = Integer.MAX_VALUE;
+    // The width of the ramp region in pixels on the left & right sides of the fish-eye effect.
+    private final int mRamp;
+
+    /**
+     * Initializes a magnifier.
+     *
+     * @param view the view for which this magnifier is attached
+     *
+     * @deprecated Please use {@link Builder} instead
+     */
+    @Deprecated
+    public Magnifier(@NonNull View view) {
+        this(createBuilderWithOldMagnifierDefaults(view));
+    }
+
+    static Builder createBuilderWithOldMagnifierDefaults(final View view) {
+        final Builder params = new Builder(view);
+        final Context context = view.getContext();
+        final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier,
+                R.attr.magnifierStyle, 0);
+        params.mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0);
+        params.mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0);
+        params.mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0);
+        params.mCornerRadius = getDeviceDefaultDialogCornerRadius(context);
+        params.mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0);
+        params.mHorizontalDefaultSourceToMagnifierOffset =
+                a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0);
+        params.mVerticalDefaultSourceToMagnifierOffset =
+                a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0);
+        params.mOverlay = new ColorDrawable(a.getColor(
+                R.styleable.Magnifier_magnifierColorOverlay, Color.TRANSPARENT));
+        a.recycle();
+        params.mClippingEnabled = true;
+        params.mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
+        params.mTopContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
+        params.mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
+        params.mBottomContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
+        return params;
+    }
+
+    /**
+     * Returns the device default theme dialog corner radius attribute.
+     * We retrieve this from the device default theme to avoid
+     * using the values set in the custom application themes.
+     */
+    private static float getDeviceDefaultDialogCornerRadius(final Context context) {
+        final Context deviceDefaultContext =
+                new ContextThemeWrapper(context, R.style.Theme_DeviceDefault);
+        final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
+                new int[]{android.R.attr.dialogCornerRadius});
+        final float dialogCornerRadius = ta.getDimension(0, 0);
+        ta.recycle();
+        return dialogCornerRadius;
+    }
+
+    private Magnifier(@NonNull Builder params) {
+        // Copy params from builder.
+        mView = params.mView;
+        mWindowWidth = params.mWidth;
+        mWindowHeight = params.mHeight;
+        mZoom = params.mZoom;
+        mIsFishEyeStyle = params.mIsFishEyeStyle;
+        if (params.mSourceWidth > 0 && params.mSourceHeight > 0) {
+            mSourceWidth = params.mSourceWidth;
+            mSourceHeight = params.mSourceHeight;
+        } else {
+            mSourceWidth = Math.round(mWindowWidth / mZoom);
+            mSourceHeight = Math.round(mWindowHeight / mZoom);
+        }
+        mWindowElevation = params.mElevation;
+        mWindowCornerRadius = params.mCornerRadius;
+        mOverlay = params.mOverlay;
+        mDefaultHorizontalSourceToMagnifierOffset =
+                params.mHorizontalDefaultSourceToMagnifierOffset;
+        mDefaultVerticalSourceToMagnifierOffset =
+                params.mVerticalDefaultSourceToMagnifierOffset;
+        mClippingEnabled = params.mClippingEnabled;
+        mLeftContentBound = params.mLeftContentBound;
+        mTopContentBound = params.mTopContentBound;
+        mRightContentBound = params.mRightContentBound;
+        mBottomContentBound = params.mBottomContentBound;
+        // The view's surface coordinates will not be updated until the magnifier is first shown.
+        mViewCoordinatesInSurface = new int[2];
+        mRamp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, FISHEYE_RAMP_WIDTH,
+                mView.getContext().getResources().getDisplayMetrics());
+    }
+
+    static {
+        sPixelCopyHandlerThread.start();
+    }
+
+    /**
+     * Sets the horizontal bounds of the source when showing the magnifier.
+     * This is used for new style magnifier. e.g. limit the source bounds by the text line bounds.
+     *
+     * @param left the left of the bounds, relative to the view.
+     * @param right the right of the bounds, relative to the view.
+     */
+    void setSourceHorizontalBounds(int left, int right) {
+        mLeftBound = left;
+        mRightBound = right;
+    }
+
+    /**
+     * Shows the magnifier on the screen. The method takes the coordinates of the center
+     * of the content source going to be magnified and copied to the magnifier. The coordinates
+     * are relative to the top left corner of the magnified view. The magnifier will be
+     * positioned such that its center will be at the default offset from the center of the source.
+     * The default offset can be specified using the method
+     * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should
+     * be different across calls to this method, you should consider to use method
+     * {@link #show(float, float, float, float)} instead.
+     *
+     * @param sourceCenterX horizontal coordinate of the source center, relative to the view
+     * @param sourceCenterY vertical coordinate of the source center, relative to the view
+     *
+     * @see Builder#setDefaultSourceToMagnifierOffset(int, int)
+     * @see Builder#getDefaultHorizontalSourceToMagnifierOffset()
+     * @see Builder#getDefaultVerticalSourceToMagnifierOffset()
+     * @see #show(float, float, float, float)
+     */
+    public void show(@FloatRange(from = 0) float sourceCenterX,
+            @FloatRange(from = 0) float sourceCenterY) {
+        show(sourceCenterX, sourceCenterY,
+                sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset,
+                sourceCenterY + mDefaultVerticalSourceToMagnifierOffset);
+    }
+
+    /**
+     * Shows the magnifier on the screen at a position that is independent from its content
+     * position. The first two arguments represent the coordinates of the center of the
+     * content source going to be magnified and copied to the magnifier. The last two arguments
+     * represent the coordinates of the center of the magnifier itself. All four coordinates
+     * are relative to the top left corner of the magnified view. If you consider using this
+     * method such that the offset between the source center and the magnifier center coordinates
+     * remains constant, you should consider using method {@link #show(float, float)} instead.
+     *
+     * @param sourceCenterX horizontal coordinate of the source center relative to the view
+     * @param sourceCenterY vertical coordinate of the source center, relative to the view
+     * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view
+     * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view
+     */
+    public void show(@FloatRange(from = 0) float sourceCenterX,
+            @FloatRange(from = 0) float sourceCenterY,
+            float magnifierCenterX, float magnifierCenterY) {
+
+        obtainSurfaces();
+        obtainContentCoordinates(sourceCenterX, sourceCenterY);
+
+        int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2;
+        final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2;
+
+        if (mIsFishEyeStyle) {
+            // The magnifier center is the same as source center in new style.
+            magnifierCenterX = mClampedCenterZoomCoords.x - mViewCoordinatesInSurface[0];
+            magnifierCenterY = mClampedCenterZoomCoords.y - mViewCoordinatesInSurface[1];
+
+            // mLeftBound & mRightBound (typically the text line left/right) is for magnified
+            // content. However the PixelCopy requires the pre-magnified bounds.
+            // The below logic calculates the leftBound & rightBound for the pre-magnified bounds.
+            final float rampPre =
+                    (mSourceWidth - (mSourceWidth - 2 * mRamp) / mZoom) / 2;
+
+            // Calculates the pre-zoomed left edge.
+            // The leftEdge moves from the left of view towards to sourceCenterX, considering the
+            // fisheye-like zooming.
+            final float x0 = sourceCenterX - mSourceWidth / 2;
+            final float rampX0 = x0 + mRamp;
+            float leftEdge = 0;
+            if (leftEdge > rampX0) {
+                // leftEdge is in the zoom range, the distance from leftEdge to sourceCenterX
+                // should reduce per mZoom.
+                leftEdge = sourceCenterX - (sourceCenterX - leftEdge) / mZoom;
+            } else if (leftEdge > x0) {
+                // leftEdge is in the ramp range, the distance from leftEdge to rampX0 should
+                // increase per ramp zoom (ramp / rampPre).
+                leftEdge = x0 + rampPre - (rampX0 - leftEdge) * rampPre / mRamp;
+            }
+            int leftBound = Math.min(Math.max((int) leftEdge, mLeftBound), mRightBound);
+
+            // Calculates the pre-zoomed right edge.
+            // The rightEdge moves from the right of view towards to sourceCenterX, considering the
+            // fisheye-like zooming.
+            final float x1 = sourceCenterX + mSourceWidth / 2;
+            final float rampX1 = x1 - mRamp;
+            float rightEdge = mView.getWidth();
+            if (rightEdge < rampX1) {
+                // rightEdge is in the zoom range, the distance from rightEdge to sourceCenterX
+                // should reduce per mZoom.
+                rightEdge = sourceCenterX + (rightEdge - sourceCenterX) / mZoom;
+            } else if (rightEdge < x1) {
+                // rightEdge is in the ramp range, the distance from rightEdge to rampX1 should
+                // increase per ramp zoom (ramp / rampPre).
+                rightEdge = x1 - rampPre + (rightEdge - rampX1) * rampPre / mRamp;
+            }
+            int rightBound = Math.max(leftBound, Math.min((int) rightEdge, mRightBound));
+
+            // Gets the startX for new style, which should be bounded by the horizontal bounds.
+            // Also calculates the left/right cut width for pixel copy.
+            leftBound = Math.max(leftBound + mViewCoordinatesInSurface[0], 0);
+            rightBound = Math.min(
+                rightBound + mViewCoordinatesInSurface[0], mContentCopySurface.mWidth);
+            mLeftCutWidth = Math.max(0, leftBound - startX);
+            mRightCutWidth = Math.max(0, startX + mSourceWidth - rightBound);
+            startX = Math.max(startX, leftBound);
+        }
+        obtainWindowCoordinates(magnifierCenterX, magnifierCenterY);
+
+        if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y
+                || mDirtyState) {
+            if (mWindow == null) {
+                synchronized (mLock) {
+                    mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
+                            mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight, mZoom,
+                            mRamp, mWindowElevation, mWindowCornerRadius,
+                            mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT),
+                            Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
+                            mCallback, mIsFishEyeStyle);
+                }
+            }
+            performPixelCopy(startX, startY, true /* update window position */);
+        } else if (magnifierCenterX != mPrevShowWindowCoords.x
+                || magnifierCenterY != mPrevShowWindowCoords.y) {
+            final Point windowCoords = getCurrentClampedWindowCoordinates();
+            final InternalPopupWindow currentWindowInstance = mWindow;
+            sPixelCopyHandlerThread.getThreadHandler().post(() -> {
+                synchronized (mLock) {
+                    if (mWindow != currentWindowInstance) {
+                        // The magnifier was dismissed (and maybe shown again) in the meantime.
+                        return;
+                    }
+                    mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
+                }
+            });
+        }
+        mPrevShowSourceCoords.x = sourceCenterX;
+        mPrevShowSourceCoords.y = sourceCenterY;
+        mPrevShowWindowCoords.x = magnifierCenterX;
+        mPrevShowWindowCoords.y = magnifierCenterY;
+    }
+
+    /**
+     * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
+     */
+    public void dismiss() {
+        if (mWindow != null) {
+            synchronized (mLock) {
+                mWindow.destroy();
+                mWindow = null;
+            }
+            mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+            mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+            mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+            mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+            mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+            mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+        }
+    }
+
+    /**
+     * Asks the magnifier to update its content. It uses the previous coordinates passed to
+     * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The
+     * method only has effect if the magnifier is currently showing.
+     */
+    public void update() {
+        if (mWindow != null) {
+            obtainSurfaces();
+            if (!mDirtyState) {
+                // Update the content shown in the magnifier.
+                performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
+                        false /* update window position */);
+            } else {
+                // If for example the zoom has changed, we cannot use the same top left
+                // coordinates as before, so just #show again to have them recomputed.
+                show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y,
+                        mPrevShowWindowCoords.x, mPrevShowWindowCoords.y);
+            }
+        }
+    }
+
+    /**
+     * @return the width of the magnifier window, in pixels
+     * @see Magnifier.Builder#setSize(int, int)
+     */
+    @Px
+    public int getWidth() {
+        return mWindowWidth;
+    }
+
+    /**
+     * @return the height of the magnifier window, in pixels
+     * @see Magnifier.Builder#setSize(int, int)
+     */
+    @Px
+    public int getHeight() {
+        return mWindowHeight;
+    }
+
+    /**
+     * @return the initial width of the content magnified and copied to the magnifier, in pixels
+     * @see Magnifier.Builder#setSize(int, int)
+     * @see Magnifier.Builder#setInitialZoom(float)
+     */
+    @Px
+    public int getSourceWidth() {
+        return mSourceWidth;
+    }
+
+    /**
+     * @return the initial height of the content magnified and copied to the magnifier, in pixels
+     * @see Magnifier.Builder#setSize(int, int)
+     * @see Magnifier.Builder#setInitialZoom(float)
+     */
+    @Px
+    public int getSourceHeight() {
+        return mSourceHeight;
+    }
+
+    /**
+     * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup.
+     * The change will become effective at the next #show or #update call.
+     * @param zoom the zoom to be set
+     */
+    public void setZoom(@FloatRange(from = 0f) float zoom) {
+        Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
+        mZoom = zoom;
+        mSourceWidth = mIsFishEyeStyle ? mWindowWidth : Math.round(mWindowWidth / mZoom);
+        mSourceHeight = Math.round(mWindowHeight / mZoom);
+        mDirtyState = true;
+    }
+
+    /**
+     * Updates the factors of source which may impact the magnifier's size.
+     * This can be called while the magnifier is showing and moving.
+     * @param sourceHeight the new source height.
+     * @param zoom the new zoom factor.
+     */
+    void updateSourceFactors(final int sourceHeight, final float zoom) {
+        mZoom = zoom;
+        mSourceHeight = sourceHeight;
+        mWindowHeight = (int) (sourceHeight * zoom);
+        if (mWindow != null) {
+            mWindow.updateContentFactors(mWindowHeight, zoom);
+        }
+    }
+
+    /**
+     * Returns the zoom to be applied to the magnified view region copied to the magnifier.
+     * If the zoom is x and the magnifier window size is (width, height), the original size
+     * of the content being magnified will be (width / x, height / x).
+     * @return the zoom applied to the content
+     * @see Magnifier.Builder#setInitialZoom(float)
+     */
+    public float getZoom() {
+        return mZoom;
+    }
+
+    /**
+     * @return the elevation set for the magnifier window, in pixels
+     * @see Magnifier.Builder#setElevation(float)
+     */
+    @Px
+    public float getElevation() {
+        return mWindowElevation;
+    }
+
+    /**
+     * @return the corner radius of the magnifier window, in pixels
+     * @see Magnifier.Builder#setCornerRadius(float)
+     */
+    @Px
+    public float getCornerRadius() {
+        return mWindowCornerRadius;
+    }
+
+    /**
+     * Returns the horizontal offset, in pixels, to be applied to the source center position
+     * to obtain the magnifier center position when {@link #show(float, float)} is called.
+     * The value is ignored when {@link #show(float, float, float, float)} is used instead.
+     *
+     * @return the default horizontal offset between the source center and the magnifier
+     * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
+     * @see Magnifier#show(float, float)
+     */
+    @Px
+    public int getDefaultHorizontalSourceToMagnifierOffset() {
+        return mDefaultHorizontalSourceToMagnifierOffset;
+    }
+
+    /**
+     * Returns the vertical offset, in pixels, to be applied to the source center position
+     * to obtain the magnifier center position when {@link #show(float, float)} is called.
+     * The value is ignored when {@link #show(float, float, float, float)} is used instead.
+     *
+     * @return the default vertical offset between the source center and the magnifier
+     * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
+     * @see Magnifier#show(float, float)
+     */
+    @Px
+    public int getDefaultVerticalSourceToMagnifierOffset() {
+        return mDefaultVerticalSourceToMagnifierOffset;
+    }
+
+    /**
+     * Returns the overlay to be drawn on the top of the magnifier, or
+     * {@code null} if no overlay should be drawn.
+     * @return the overlay
+     * @see Magnifier.Builder#setOverlay(Drawable)
+     */
+    @Nullable
+    public Drawable getOverlay() {
+        return mOverlay;
+    }
+
+    /**
+     * Returns whether the magnifier position will be adjusted such that the magnifier will be
+     * fully within the bounds of the main application window, by also avoiding any overlap
+     * with system insets (such as the one corresponding to the status bar) i.e. whether the
+     * area where the magnifier can be positioned will be clipped to the main application window
+     * and the system insets.
+     * @return whether the magnifier position will be adjusted
+     * @see Magnifier.Builder#setClippingEnabled(boolean)
+     */
+    public boolean isClippingEnabled() {
+        return mClippingEnabled;
+    }
+
+    /**
+     * Returns the top left coordinates of the magnifier, relative to the main application
+     * window. They will be determined by the coordinates of the last {@link #show(float, float)}
+     * or {@link #show(float, float, float, float)} call, adjusted to take into account any
+     * potential clamping behavior. The method can be used immediately after a #show
+     * call to find out where the magnifier will be positioned. However, the position of the
+     * magnifier will not be updated visually in the same frame, due to the async nature of
+     * the content copying and of the magnifier rendering.
+     * The method will return {@code null} if #show has not yet been called, or if the last
+     * operation performed was a #dismiss.
+     *
+     * @return the top left coordinates of the magnifier
+     */
+    @Nullable
+    public Point getPosition() {
+        if (mWindow == null) {
+            return null;
+        }
+        final Point position = getCurrentClampedWindowCoordinates();
+        position.offset(-mParentSurface.mInsets.left, -mParentSurface.mInsets.top);
+        return new Point(position);
+    }
+
+    /**
+     * Returns the top left coordinates of the magnifier source (i.e. the view region going to
+     * be magnified and copied to the magnifier), relative to the window or surface the content
+     * is copied from. The content will be copied:
+     * - if the magnified view is a {@link SurfaceView}, from the surface backing it
+     * - otherwise, from the surface backing the main application window, and the coordinates
+     *   returned will be relative to the main application window
+     * The method will return {@code null} if #show has not yet been called, or if the last
+     * operation performed was a #dismiss.
+     *
+     * @return the top left coordinates of the magnifier source
+     */
+    @Nullable
+    public Point getSourcePosition() {
+        if (mWindow == null) {
+            return null;
+        }
+        final Point position = new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top);
+        position.offset(-mContentCopySurface.mInsets.left, -mContentCopySurface.mInsets.top);
+        return new Point(position);
+    }
+
+    /**
+     * Retrieves the surfaces used by the magnifier:
+     * - a parent surface for the magnifier surface. This will usually be the main app window.
+     * - a surface where the magnified content will be copied from. This will be the main app
+     *   window unless the magnified view is a SurfaceView, in which case its backing surface
+     *   will be used.
+     */
+    private void obtainSurfaces() {
+        // Get the main window surface.
+        SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
+        if (mView.getViewRootImpl() != null) {
+            final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
+            final Surface mainWindowSurface = viewRootImpl.mSurface;
+            if (mainWindowSurface != null && mainWindowSurface.isValid()) {
+                final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
+                final int surfaceWidth =
+                        viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
+                final int surfaceHeight =
+                        viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
+                validMainWindowSurface =
+                        new SurfaceInfo(viewRootImpl.getSurfaceControl(), mainWindowSurface,
+                                surfaceWidth, surfaceHeight, surfaceInsets, true);
+            }
+        }
+        // Get the surface backing the magnified view, if it is a SurfaceView.
+        SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
+        if (mView instanceof SurfaceView) {
+            final SurfaceControl sc = ((SurfaceView) mView).getSurfaceControl();
+            final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
+            final Surface surfaceViewSurface = surfaceHolder.getSurface();
+
+            if (sc != null && sc.isValid()) {
+                final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
+                validSurfaceViewSurface = new SurfaceInfo(sc, surfaceViewSurface,
+                        surfaceFrame.right, surfaceFrame.bottom, new Rect(), false);
+            }
+        }
+
+        // Choose the parent surface for the magnifier and the source surface for the content.
+        mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
+                ? validMainWindowSurface : validSurfaceViewSurface;
+        mContentCopySurface = mView instanceof SurfaceView
+                ? validSurfaceViewSurface : validMainWindowSurface;
+    }
+
+    /**
+     * Computes 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.
+     */
+    private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
+        final int prevViewXInSurface = mViewCoordinatesInSurface[0];
+        final int prevViewYInSurface = mViewCoordinatesInSurface[1];
+        mView.getLocationInSurface(mViewCoordinatesInSurface);
+        if (mViewCoordinatesInSurface[0] != prevViewXInSurface
+                || mViewCoordinatesInSurface[1] != prevViewYInSurface) {
+            mDirtyState = true;
+        }
+
+        final int zoomCenterX;
+        final int zoomCenterY;
+        if (mView instanceof SurfaceView) {
+            // No offset required if the backing Surface matches the size of the SurfaceView.
+            zoomCenterX = Math.round(xPosInView);
+            zoomCenterY = Math.round(yPosInView);
+        } else {
+            zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]);
+            zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]);
+        }
+
+        final Rect[] bounds = new Rect[2]; // [MAX_IN_SURFACE, MAX_VISIBLE]
+        // Obtain the surface bounds rectangle.
+        final Rect surfaceBounds = new Rect(0, 0,
+                mContentCopySurface.mWidth, mContentCopySurface.mHeight);
+        bounds[0] = surfaceBounds;
+        // Obtain the visible view region rectangle.
+        final Rect viewVisibleRegion = new Rect();
+        mView.getGlobalVisibleRect(viewVisibleRegion);
+        if (mView.getViewRootImpl() != null) {
+            // Clamping coordinates relative to the surface, not to the window.
+            final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
+            viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
+        }
+        if (mView instanceof SurfaceView) {
+            // If we copy content from a SurfaceView, clamp coordinates relative to it.
+            viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
+        }
+        bounds[1] = viewVisibleRegion;
+
+        // Aggregate the above to obtain the bounds where the content copy will be restricted.
+        int resolvedLeft = Integer.MIN_VALUE;
+        for (int i = mLeftContentBound; i >= 0; --i) {
+            resolvedLeft = Math.max(resolvedLeft, bounds[i].left);
+        }
+        int resolvedTop = Integer.MIN_VALUE;
+        for (int i = mTopContentBound; i >= 0; --i) {
+            resolvedTop = Math.max(resolvedTop, bounds[i].top);
+        }
+        int resolvedRight = Integer.MAX_VALUE;
+        for (int i = mRightContentBound; i >= 0; --i) {
+            resolvedRight = Math.min(resolvedRight, bounds[i].right);
+        }
+        int resolvedBottom = Integer.MAX_VALUE;
+        for (int i = mBottomContentBound; i >= 0; --i) {
+            resolvedBottom = Math.min(resolvedBottom, bounds[i].bottom);
+        }
+        // Adjust <left-right> and <top-bottom> pairs of bounds to make sense.
+        resolvedLeft = Math.min(resolvedLeft, mContentCopySurface.mWidth - mSourceWidth);
+        resolvedTop = Math.min(resolvedTop, mContentCopySurface.mHeight - mSourceHeight);
+        if (resolvedLeft < 0 || resolvedTop < 0) {
+            Log.e(TAG, "Magnifier's content is copied from a surface smaller than"
+                    + "the content requested size. The magnifier will be dismissed.");
+        }
+        resolvedRight = Math.max(resolvedRight, resolvedLeft + mSourceWidth);
+        resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight);
+
+        // Finally compute the coordinates of the source center.
+        mClampedCenterZoomCoords.x = mIsFishEyeStyle
+                ? Math.max(resolvedLeft, Math.min(zoomCenterX, resolvedRight))
+                : Math.max(resolvedLeft + mSourceWidth / 2, Math.min(
+                        zoomCenterX, resolvedRight - mSourceWidth / 2));
+        mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min(
+                zoomCenterY, resolvedBottom - mSourceHeight / 2));
+    }
+
+    /**
+     * Computes the coordinates of the top left corner of the magnifier window.
+     * These are relative to the surface the magnifier window is attached to.
+     */
+    private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) {
+        final int windowCenterX;
+        final int windowCenterY;
+        if (mView instanceof SurfaceView) {
+            // No offset required if the backing Surface matches the size of the SurfaceView.
+            windowCenterX = Math.round(xWindowPos);
+            windowCenterY = Math.round(yWindowPos);
+        } else {
+            windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]);
+            windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]);
+        }
+
+        mWindowCoords.x = windowCenterX - mWindowWidth / 2;
+        mWindowCoords.y = windowCenterY - mWindowHeight / 2;
+        if (mParentSurface != mContentCopySurface) {
+            mWindowCoords.x += mViewCoordinatesInSurface[0];
+            mWindowCoords.y += mViewCoordinatesInSurface[1];
+        }
+    }
+
+    private void performPixelCopy(final int startXInSurface, final int startYInSurface,
+            final boolean updateWindowPosition) {
+        if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
+            onPixelCopyFailed();
+            return;
+        }
+
+        // Clamp window coordinates inside the parent surface, to avoid displaying
+        // the magnifier out of screen or overlapping with system insets.
+        final Point windowCoords = getCurrentClampedWindowCoordinates();
+
+        // Perform the pixel copy.
+        mPixelCopyRequestRect.set(startXInSurface,
+                startYInSurface,
+                startXInSurface + mSourceWidth - mLeftCutWidth - mRightCutWidth,
+                startYInSurface + mSourceHeight);
+        mPrevStartCoordsInSurface.x = startXInSurface;
+        mPrevStartCoordsInSurface.y = startYInSurface;
+        mDirtyState = false;
+
+        final InternalPopupWindow currentWindowInstance = mWindow;
+        if (mPixelCopyRequestRect.width() == 0) {
+            // If the copy rect is empty, updates an empty bitmap to the window.
+            mWindow.updateContent(
+                    Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ALPHA_8));
+            return;
+        }
+        final Bitmap bitmap =
+                Bitmap.createBitmap(mSourceWidth - mLeftCutWidth - mRightCutWidth,
+                        mSourceHeight, Bitmap.Config.ARGB_8888);
+        PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
+                result -> {
+                    if (result != PixelCopy.SUCCESS) {
+                        onPixelCopyFailed();
+                        return;
+                    }
+                    synchronized (mLock) {
+                        if (mWindow != currentWindowInstance) {
+                            // The magnifier was dismissed (and maybe shown again) in the meantime.
+                            return;
+                        }
+                        if (updateWindowPosition) {
+                            // TODO: pull the position update outside #performPixelCopy
+                            mWindow.setContentPositionForNextDraw(windowCoords.x,
+                                    windowCoords.y);
+                        }
+                        if (bitmap.getWidth() < mSourceWidth) {
+                            // When bitmap width has been cut, re-fills it with full width bitmap.
+                            // This only happens in new styled magnifier.
+                            final Bitmap newBitmap = Bitmap.createBitmap(
+                                    mSourceWidth, bitmap.getHeight(), bitmap.getConfig());
+                            final Canvas can = new Canvas(newBitmap);
+                            final Rect dstRect = new Rect(mLeftCutWidth, 0,
+                                    mSourceWidth - mRightCutWidth, bitmap.getHeight());
+                            can.drawBitmap(bitmap, null, dstRect, null);
+                            mWindow.updateContent(newBitmap);
+                        } else {
+                            mWindow.updateContent(bitmap);
+                        }
+                    }
+                },
+                sPixelCopyHandlerThread.getThreadHandler());
+    }
+
+    private void onPixelCopyFailed() {
+        Log.e(TAG, "Magnifier failed to copy content from the view Surface. It will be dismissed.");
+        // Post to make sure #dismiss is done on the main thread.
+        Handler.getMain().postAtFrontOfQueue(() -> {
+            dismiss();
+            if (mCallback != null) {
+                mCallback.onOperationComplete();
+            }
+        });
+    }
+
+    /**
+     * Clamp window coordinates inside the surface the magnifier is attached to, to avoid
+     * displaying the magnifier out of screen or overlapping with system insets.
+     * @return the current window coordinates, after they are clamped inside the parent surface
+     */
+    private Point getCurrentClampedWindowCoordinates() {
+        if (!mClippingEnabled) {
+            // No position adjustment should be done, so return the raw coordinates.
+            return new Point(mWindowCoords);
+        }
+
+        final Rect windowBounds;
+        if (mParentSurface.mIsMainWindowSurface) {
+            final Insets systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
+            windowBounds = new Rect(
+                    systemInsets.left + mParentSurface.mInsets.left,
+                    systemInsets.top + mParentSurface.mInsets.top,
+                    mParentSurface.mWidth - systemInsets.right - mParentSurface.mInsets.right,
+                    mParentSurface.mHeight - systemInsets.bottom
+                            - mParentSurface.mInsets.bottom
+            );
+        } else {
+            windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
+        }
+        final int windowCoordsX = Math.max(windowBounds.left,
+                Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
+        final int windowCoordsY = Math.max(windowBounds.top,
+                Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
+        return new Point(windowCoordsX, windowCoordsY);
+    }
+
+    /**
+     * Contains a surface and metadata corresponding to it.
+     */
+    private static class SurfaceInfo {
+        public static final SurfaceInfo NULL = new SurfaceInfo(null, null, 0, 0, null, false);
+
+        private Surface mSurface;
+        private SurfaceControl mSurfaceControl;
+        private int mWidth;
+        private int mHeight;
+        private Rect mInsets;
+        private boolean mIsMainWindowSurface;
+
+        SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface,
+                final int width, final int height, final Rect insets,
+                final boolean isMainWindowSurface) {
+            mSurfaceControl = surfaceControl;
+            mSurface = surface;
+            mWidth = width;
+            mHeight = height;
+            mInsets = insets;
+            mIsMainWindowSurface = isMainWindowSurface;
+        }
+    }
+
+    /**
+     * 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 {
+        // The z of the magnifier surface, defining its z order in the list of
+        // siblings having the same parent surface (usually the main app surface).
+        private static final int SURFACE_Z = 5;
+
+        // 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 int mContentHeight;
+        // The insets of the content inside the allocated surface.
+        private final int mOffsetX;
+        private final int mOffsetY;
+        // The overlay to be drawn on the top of the content.
+        private final Drawable mOverlay;
+        // The surface we allocate for the magnifier content + shadow.
+        private final SurfaceSession mSurfaceSession;
+        private final SurfaceControl mSurfaceControl;
+        private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+        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 RenderNode used to draw the overlay over the magnifier content.
+        private final RenderNode mOverlayRenderNode;
+        // 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.
+        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;
+        // Whether a magnifier frame draw is currently pending in the UI thread queue.
+        private boolean mFrameDrawScheduled;
+        // The content bitmap, as returned by pixel copy.
+        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;
+
+        // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing.
+        private Bitmap mCurrentContent;
+
+        private float mZoom;
+        // The width of the ramp region in pixels on the left & right sides of the fish-eye effect.
+        private final int mRamp;
+        // Whether is in the new magnifier style.
+        private boolean mIsFishEyeStyle;
+        // The mesh matrix for the fish-eye effect.
+        private float[] mMeshLeft;
+        private float[] mMeshRight;
+        private int mMeshWidth;
+        private int mMeshHeight;
+
+        InternalPopupWindow(final Context context, final Display display,
+                final SurfaceControl parentSurfaceControl, final int width, final int height,
+                final float zoom, final int ramp, final float elevation, final float cornerRadius,
+                final Drawable overlay, final Handler handler, final Object lock,
+                final Callback callback, final boolean isFishEyeStyle) {
+            mDisplay = display;
+            mOverlay = overlay;
+            mLock = lock;
+            mCallback = callback;
+
+            mContentWidth = width;
+            mContentHeight = height;
+            mZoom = zoom;
+            mRamp = ramp;
+            mOffsetX = (int) (1.05f * elevation);
+            mOffsetY = (int) (1.05f * elevation);
+            // Setup the surface we will use for drawing the content and shadow.
+            final int surfaceWidth = mContentWidth + 2 * mOffsetX;
+            final int surfaceHeight = mContentHeight + 2 * mOffsetY;
+            mSurfaceSession = new SurfaceSession();
+            mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
+                    .setFormat(PixelFormat.TRANSLUCENT)
+                    .setBufferSize(surfaceWidth, surfaceHeight)
+                    .setName("magnifier surface")
+                    .setFlags(SurfaceControl.HIDDEN)
+                    .setParent(parentSurfaceControl)
+                    .build();
+            mSurface = new Surface();
+            mSurface.copyFrom(mSurfaceControl);
+
+            // Setup the RenderNode tree. The root has two children, one containing the bitmap
+            // and one containing the overlay. We use a separate render node for the overlay
+            // to avoid drawing this as the same rate we do for content.
+            mRenderer = new ThreadedRenderer.SimpleRenderer(
+                    context,
+                    "magnifier renderer",
+                    mSurface
+            );
+            mBitmapRenderNode = createRenderNodeForBitmap(
+                    "magnifier content",
+                    elevation,
+                    cornerRadius
+            );
+            mOverlayRenderNode = createRenderNodeForOverlay(
+                    "magnifier overlay",
+                    cornerRadius
+            );
+            setupOverlay();
+
+            final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height);
+            try {
+                canvas.enableZ();
+                canvas.drawRenderNode(mBitmapRenderNode);
+                canvas.disableZ();
+                canvas.drawRenderNode(mOverlayRenderNode);
+                canvas.disableZ();
+            } finally {
+                mRenderer.getRootNode().endRecording();
+            }
+            if (mCallback != null) {
+                mCurrentContent =
+                        Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888);
+                updateCurrentContentForTesting();
+            }
+
+            // Initialize the update job and the handler where this will be post'd.
+            mHandler = handler;
+            mMagnifierUpdater = this::doDraw;
+            mFrameDrawScheduled = false;
+            mIsFishEyeStyle = isFishEyeStyle;
+
+            if (mIsFishEyeStyle) {
+                createMeshMatrixForFishEyeEffect();
+            }
+        }
+
+        /**
+         * Updates the factors of content which may resize the window.
+         * @param contentHeight the new height of content.
+         * @param zoom the new zoom factor.
+         */
+        private void updateContentFactors(final int contentHeight, final float zoom) {
+            if (mContentHeight == contentHeight && mZoom == zoom) {
+              return;
+            }
+            if (mContentHeight < contentHeight) {
+                // Grows the surface height as necessary.
+                new SurfaceControl.Transaction().setBufferSize(
+                        mSurfaceControl, mContentWidth, contentHeight).apply();
+                mSurface.copyFrom(mSurfaceControl);
+                mRenderer.setSurface(mSurface);
+
+                final Outline outline = new Outline();
+                outline.setRoundRect(0, 0, mContentWidth, contentHeight, 0);
+                outline.setAlpha(1.0f);
+
+                mBitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
+                        mOffsetX + mContentWidth, mOffsetY + contentHeight);
+                mBitmapRenderNode.setOutline(outline);
+
+                mOverlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
+                        mOffsetX + mContentWidth, mOffsetY + contentHeight);
+                mOverlayRenderNode.setOutline(outline);
+
+                final RecordingCanvas canvas =
+                        mRenderer.getRootNode().beginRecording(mContentWidth, contentHeight);
+                try {
+                    canvas.enableZ();
+                    canvas.drawRenderNode(mBitmapRenderNode);
+                    canvas.disableZ();
+                    canvas.drawRenderNode(mOverlayRenderNode);
+                    canvas.disableZ();
+                } finally {
+                    mRenderer.getRootNode().endRecording();
+                }
+            }
+            mContentHeight = contentHeight;
+            mZoom = zoom;
+            fillMeshMatrix();
+        }
+
+        private void createMeshMatrixForFishEyeEffect() {
+            mMeshWidth = 1;
+            mMeshHeight = 6;
+            mMeshLeft = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)];
+            mMeshRight = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)];
+            fillMeshMatrix();
+        }
+
+        private void fillMeshMatrix() {
+            mMeshWidth = 1;
+            mMeshHeight = 6;
+            final float w = mContentWidth;
+            final float h = mContentHeight;
+            final float h0 = h / mZoom;
+            final float dh = h - h0;
+            for (int i = 0; i < 2 * (mMeshWidth + 1) * (mMeshHeight + 1); i += 2) {
+                // Calculates X value.
+                final int colIndex = i % (2 * (mMeshWidth + 1)) / 2;
+                mMeshLeft[i] = (float) colIndex * mRamp / mMeshWidth;
+                mMeshRight[i] = w - mRamp + colIndex * mRamp / mMeshWidth;
+
+                // Calculates Y value.
+                final int rowIndex = i / 2 / (mMeshWidth + 1);
+                final float hl = h0 + dh * colIndex / mMeshWidth;
+                final float yl = (h - hl) / 2;
+                mMeshLeft[i + 1] = yl + hl * rowIndex / mMeshHeight;
+                final float hr = h - dh * colIndex / mMeshWidth;
+                final float yr = (h - hr) / 2;
+                mMeshRight[i + 1] = yr + hr * rowIndex / mMeshHeight;
+            }
+        }
+
+        private RenderNode createRenderNodeForBitmap(final String name,
+                final float elevation, final float cornerRadius) {
+            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, cornerRadius);
+            outline.setAlpha(1.0f);
+            bitmapRenderNode.setOutline(outline);
+            bitmapRenderNode.setClipToOutline(true);
+
+            // Create a dummy draw, which will be replaced later with real drawing.
+            final RecordingCanvas canvas = bitmapRenderNode.beginRecording(
+                    mContentWidth, mContentHeight);
+            try {
+                canvas.drawColor(0xFF00FF00);
+            } finally {
+                bitmapRenderNode.endRecording();
+            }
+
+            return bitmapRenderNode;
+        }
+
+        private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) {
+            final RenderNode overlayRenderNode = RenderNode.create(name, null);
+
+            // Define the position of the overlay in the parent render node.
+            // This coincides with the position of the content.
+            overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
+                    mOffsetX + mContentWidth, mOffsetY + mContentHeight);
+
+            final Outline outline = new Outline();
+            outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
+            outline.setAlpha(1.0f);
+            overlayRenderNode.setOutline(outline);
+            overlayRenderNode.setClipToOutline(true);
+
+            return overlayRenderNode;
+        }
+
+        private void setupOverlay() {
+            drawOverlay();
+
+            mOverlay.setCallback(new Drawable.Callback() {
+                @Override
+                public void invalidateDrawable(Drawable who) {
+                    // When the overlay drawable is invalidated, redraw it to the render node.
+                    drawOverlay();
+                    if (mCallback != null) {
+                        updateCurrentContentForTesting();
+                    }
+                }
+
+                @Override
+                public void scheduleDrawable(Drawable who, Runnable what, long when) {
+                    Handler.getMain().postAtTime(what, who, when);
+                }
+
+                @Override
+                public void unscheduleDrawable(Drawable who, Runnable what) {
+                    Handler.getMain().removeCallbacks(what, who);
+                }
+            });
+        }
+
+        private void drawOverlay() {
+            // Draw the drawable to the render node. This happens once during
+            // initialization and whenever the overlay drawable is invalidated.
+            final RecordingCanvas canvas =
+                    mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight);
+            try {
+                mOverlay.setBounds(0, 0, mContentWidth, mContentHeight);
+                mOverlay.draw(canvas);
+            } finally {
+                mOverlayRenderNode.endRecording();
+            }
+        }
+
+        /**
+         * 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. The method has to be called in a context holding {@link #mLock}.
+         */
+        public void destroy() {
+            // Destroy the renderer. This will not proceed until pending frame callbacks complete.
+            mRenderer.destroy();
+            mSurface.destroy();
+            new SurfaceControl.Transaction().remove(mSurfaceControl).apply();
+            mSurfaceSession.kill();
+            mHandler.removeCallbacks(mMagnifierUpdater);
+            if (mBitmap != null) {
+                mBitmap.recycle();
+            }
+            mOverlay.setCallback(null);
+        }
+
+        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 RecordingCanvas canvas =
+                        mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight);
+                try {
+                    final int w = mBitmap.getWidth();
+                    final int h = mBitmap.getHeight();
+                    final Paint paint = new Paint();
+                    paint.setFilterBitmap(true);
+                    if (mIsFishEyeStyle) {
+                        final int margin =
+                            (int)((mContentWidth - (mContentWidth - 2 * mRamp) / mZoom) / 2);
+
+                        // Draws the middle part.
+                        final Rect srcRect = new Rect(margin, 0, w - margin, h);
+                        final Rect dstRect = new Rect(
+                            mRamp, 0, mContentWidth - mRamp, mContentHeight);
+                        canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
+
+                        // Draws the left/right parts with mesh matrixes.
+                        canvas.drawBitmapMesh(
+                                Bitmap.createBitmap(mBitmap, 0, 0, margin, h),
+                                mMeshWidth, mMeshHeight, mMeshLeft, 0, null, 0, paint);
+                        canvas.drawBitmapMesh(
+                                Bitmap.createBitmap(mBitmap, w - margin, 0, margin, h),
+                                mMeshWidth, mMeshHeight, mMeshRight, 0, null, 0, paint);
+                    } else {
+                        final Rect srcRect = new Rect(0, 0, w, h);
+                        final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
+                        canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
+                    }
+                } finally {
+                    mBitmapRenderNode.endRecording();
+                }
+                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 -> {
+                        if (!mSurface.isValid()) {
+                            return;
+                        }
+                        // Show or move the window at the content draw frame.
+                        mTransaction.deferTransactionUntil(mSurfaceControl, mSurfaceControl,
+                                frame);
+                        if (updateWindowPosition) {
+                            mTransaction.setPosition(mSurfaceControl, pendingX, pendingY);
+                        }
+                        if (firstDraw) {
+                            mTransaction.setLayer(mSurfaceControl, SURFACE_Z)
+                                .show(mSurfaceControl);
+
+                        }
+                        mTransaction.apply();
+                    };
+                    if (!mIsFishEyeStyle) {
+                        // The new style magnifier doesn't need the light/shadow.
+                        mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
+                    }
+                } else {
+                    callback = null;
+                }
+
+                mFrameDrawScheduled = false;
+            }
+
+            mRenderer.draw(callback);
+            if (mCallback != null) {
+                // The current content bitmap is only used in testing, so, for performance,
+                // we only want to update it when running tests. For this, we check that
+                // mCallback is not null, as it can only be set from a @TestApi.
+                updateCurrentContentForTesting();
+                mCallback.onOperationComplete();
+            }
+        }
+
+        /**
+         * Updates mCurrentContent, which reproduces what is currently supposed to be
+         * drawn in the magnifier. mCurrentContent is only used for testing, so this method
+         * should only be called otherwise.
+         */
+        private void updateCurrentContentForTesting() {
+            final Canvas canvas = new Canvas(mCurrentContent);
+            final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight);
+            if (mBitmap != null && !mBitmap.isRecycled()) {
+                final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+                canvas.drawBitmap(mBitmap, originalBounds, bounds, null);
+            }
+            mOverlay.setBounds(bounds);
+            mOverlay.draw(canvas);
+        }
+    }
+
+    /**
+     * Builder class for {@link Magnifier} objects.
+     */
+    public static final class Builder {
+        private @NonNull View mView;
+        private @Px @IntRange(from = 0) int mWidth;
+        private @Px @IntRange(from = 0) int mHeight;
+        private float mZoom;
+        private @FloatRange(from = 0f) float mElevation;
+        private @FloatRange(from = 0f) float mCornerRadius;
+        private @Nullable Drawable mOverlay;
+        private int mHorizontalDefaultSourceToMagnifierOffset;
+        private int mVerticalDefaultSourceToMagnifierOffset;
+        private boolean mClippingEnabled;
+        private @SourceBound int mLeftContentBound;
+        private @SourceBound int mTopContentBound;
+        private @SourceBound int mRightContentBound;
+        private @SourceBound int  mBottomContentBound;
+        private boolean mIsFishEyeStyle;
+        private int mSourceWidth;
+        private int mSourceHeight;
+
+        /**
+         * Construct a new builder for {@link Magnifier} objects.
+         * @param view the view this magnifier is attached to
+         */
+        public Builder(@NonNull View view) {
+            mView = Objects.requireNonNull(view);
+            applyDefaults();
+        }
+
+        private void applyDefaults() {
+            final Resources resources = mView.getContext().getResources();
+            mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width);
+            mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height);
+            mElevation = resources.getDimension(R.dimen.default_magnifier_elevation);
+            mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius);
+            mZoom = resources.getFloat(R.dimen.default_magnifier_zoom);
+            mHorizontalDefaultSourceToMagnifierOffset =
+                    resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset);
+            mVerticalDefaultSourceToMagnifierOffset =
+                    resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset);
+            mOverlay = new ColorDrawable(resources.getColor(
+                    R.color.default_magnifier_color_overlay, null));
+            mClippingEnabled = true;
+            mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
+            mTopContentBound = SOURCE_BOUND_MAX_VISIBLE;
+            mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
+            mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE;
+            mIsFishEyeStyle = false;
+        }
+
+        /**
+         * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
+         * Note that the size of the content being magnified and copied to the magnifier
+         * will be computed as (window width / zoom, window height / zoom).
+         * @param width the window width to be set
+         * @param height the window height to be set
+         */
+        @NonNull
+        public Builder setSize(@Px @IntRange(from = 0) int width,
+                @Px @IntRange(from = 0) int height) {
+            Preconditions.checkArgumentPositive(width, "Width should be positive");
+            Preconditions.checkArgumentPositive(height, "Height should be positive");
+            mWidth = width;
+            mHeight = height;
+            return this;
+        }
+
+        /**
+         * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
+         * A content of size (content_width, content_height) will be magnified to
+         * (content_width * zoom, content_height * zoom), which will coincide with the size
+         * of the magnifier. A zoom of 1 will translate to no magnification (the content will
+         * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
+         * Note that the zoom can also be changed after the instance is built, using the
+         * {@link Magnifier#setZoom(float)} method.
+         * @param zoom the zoom to be set
+         */
+        @NonNull
+        public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) {
+            Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
+            mZoom = zoom;
+            return this;
+        }
+
+        /**
+         * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
+         * @param elevation the elevation to be set
+         */
+        @NonNull
+        public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
+            Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
+            mElevation = elevation;
+            return this;
+        }
+
+        /**
+         * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp.
+         * @param cornerRadius the corner radius to be set
+         */
+        @NonNull
+        public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
+            Preconditions.checkArgumentNonNegative(cornerRadius,
+                    "Corner radius should be non-negative");
+            mCornerRadius = cornerRadius;
+            return this;
+        }
+
+        /**
+         * Sets an overlay that will be drawn on the top of the magnifier.
+         * In general, the overlay should not be opaque, in order to let the magnified
+         * content be partially visible in the magnifier. The default overlay is {@code null}
+         * (no overlay). As an example, TextView applies a white {@link ColorDrawable}
+         * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark
+         * application regions. To disable the overlay, the parameter should be set
+         * to {@code null}. If not null, the overlay will be automatically redrawn
+         * when the drawable is invalidated. To achieve this, the magnifier will set a new
+         * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable,
+         * so keep in mind that any existing one set by the application will be lost.
+         * @param overlay the overlay to be drawn on top
+         */
+        @NonNull
+        public Builder setOverlay(@Nullable Drawable overlay) {
+            mOverlay = overlay;
+            return this;
+        }
+
+        /**
+         * Sets an offset that should be added to the content source center to obtain
+         * the position of the magnifier window, when the {@link #show(float, float)}
+         * method is called. The offset is ignored when {@link #show(float, float, float, float)}
+         * is used. The offset can be negative. It defaults to (0dp, 0dp).
+         * @param horizontalOffset the horizontal component of the offset
+         * @param verticalOffset the vertical component of the offset
+         */
+        @NonNull
+        public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
+                @Px int verticalOffset) {
+            mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
+            mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
+            return this;
+        }
+
+        /**
+         * Defines the behavior of the magnifier when it is requested to position outside the
+         * surface of the main application window. The default value is {@code true}, which means
+         * that the position will be adjusted such that the magnifier will be fully within the
+         * bounds of the main application window, while also avoiding any overlap with system insets
+         * (such as the one corresponding to the status bar). If this flag is set to {@code false},
+         * the area where the magnifier can be positioned will no longer be clipped, so the
+         * magnifier will be able to extend outside the main application window boundaries (and also
+         * overlap the system insets). This can be useful if you require a custom behavior, but it
+         * should be handled with care, when passing coordinates to {@link #show(float, float)};
+         * note that:
+         * <ul>
+         *   <li>in a multiwindow context, if the magnifier crosses the boundary between the two
+         *   windows, it will not be able to show over the window of the other application</li>
+         *   <li>if the magnifier overlaps the status bar, there is no guarantee about which one
+         *   will be displayed on top. This should be handled with care.</li>
+         * </ul>
+         * @param clip whether the magnifier position will be adjusted
+         */
+        @NonNull
+        public Builder setClippingEnabled(boolean clip) {
+            mClippingEnabled = clip;
+            return this;
+        }
+
+        /**
+         * Defines the bounds of the rectangle where the magnifier will be able to copy its content
+         * from. The content will always be copied from the {@link Surface} of the main application
+         * window unless the magnified view is a {@link SurfaceView}, in which case its backing
+         * surface will be used. Each bound can have a different behavior, with the options being:
+         * <ul>
+         *   <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible
+         *   while remaining in the visible region of the magnified view, as given by
+         *   {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into
+         *   account the case when the view is contained in a scrollable container, and the
+         *   magnifier will refuse to copy content outside of the visible view region</li>
+         *   <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much
+         *   as possible while remaining inside the surface the content is copied from.</li>
+         * </ul>
+         * Note that if either of the first three options is used, the bound will be compared to
+         * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used),
+         * and the more restrictive one will be chosen. In other words, no attempt to copy content
+         * from outside the surface will be permitted. If two opposite bounds are not well-behaved
+         * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top
+         * bounds will have priority and the others will be extended accordingly. If the pairs
+         * obtained this way still remain out of bounds, the smallest possible offset will be added
+         * to the pairs to bring them inside the surface bounds. If this is impossible
+         * (i.e. the surface is too small for the size of the content we try to copy on either
+         * dimension), an error will be logged and the magnifier content will look distorted.
+         * The default values assumed by the builder for the source bounds are
+         * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE},
+         * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}.
+         * @param left the left bound for content copy
+         * @param top the top bound for content copy
+         * @param right the right bound for content copy
+         * @param bottom the bottom bound for content copy
+         */
+        @NonNull
+        public Builder setSourceBounds(@SourceBound int left, @SourceBound int top,
+                @SourceBound int right, @SourceBound int bottom) {
+            mLeftContentBound = left;
+            mTopContentBound = top;
+            mRightContentBound = right;
+            mBottomContentBound = bottom;
+            return this;
+        }
+
+        /**
+         * Sets the source width/height.
+         */
+        @NonNull
+        Builder setSourceSize(int width, int height) {
+            mSourceWidth = width;
+            mSourceHeight = height;
+            return this;
+        }
+
+        /**
+         * Sets the magnifier as the new fish-eye style.
+         */
+        @NonNull
+        Builder setFishEyeStyle() {
+            mIsFishEyeStyle = true;
+            return this;
+        }
+
+        /**
+         * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
+         */
+        public @NonNull Magnifier build() {
+            return new Magnifier(this);
+        }
+    }
+
+    /**
+     * A source bound that will extend as much as possible, while remaining within the surface
+     * the content is copied from.
+     */
+    public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0;
+
+    /**
+     * A source bound that will extend as much as possible, while remaining within the
+     * visible region of the magnified view, as determined by
+     * {@link View#getGlobalVisibleRect(Rect)}.
+     */
+    public static final int SOURCE_BOUND_MAX_VISIBLE = 1;
+
+
+    /**
+     * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed
+     * to be copied from. For more details, see method
+     * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)}
+     *
+     * @hide
+     */
+    @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SourceBound {}
+
+    // The rest of the file consists of test APIs and methods relevant for tests.
+
+    /**
+     * 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;
+        }
+    }
+
+    /**
+     * @return the drawing being currently displayed in the magnifier, as bitmap
+     *
+     * @hide
+     */
+    @TestApi
+    public @Nullable Bitmap getContent() {
+        if (mWindow == null) {
+            return null;
+        }
+        synchronized (mWindow.mLock) {
+            return mWindow.mCurrentContent;
+        }
+    }
+
+    /**
+     * Returns a bitmap containing the content that was magnified and drew to the
+     * magnifier, at its original size, without the overlay applied.
+     * @return the content that is magnified, as bitmap
+     *
+     * @hide
+     */
+    @TestApi
+    public @Nullable Bitmap getOriginalContent() {
+        if (mWindow == null) {
+            return null;
+        }
+        synchronized (mWindow.mLock) {
+            return Bitmap.createBitmap(mWindow.mBitmap);
+        }
+    }
+
+    /**
+     * @return the size of the magnifier window in dp
+     *
+     * @hide
+     */
+    @TestApi
+    public static PointF getMagnifierDefaultSize() {
+        final Resources resources = Resources.getSystem();
+        final float density = resources.getDisplayMetrics().density;
+        final PointF size = new PointF();
+        size.x = resources.getDimension(R.dimen.default_magnifier_width) / density;
+        size.y = resources.getDimension(R.dimen.default_magnifier_height) / density;
+        return size;
+    }
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public interface Callback {
+        /**
+         * Callback called after the drawing for a magnifier update has happened.
+         */
+        void onOperationComplete();
+    }
+}