Magnifier.java revision f298068a7fd9e8249eca86d74ed4bcf5a6410582
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.annotation.FloatRange;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.TestApi;
23import android.annotation.UiThread;
24import android.content.Context;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.Bitmap;
28import android.graphics.Color;
29import android.graphics.Outline;
30import android.graphics.Paint;
31import android.graphics.PixelFormat;
32import android.graphics.Point;
33import android.graphics.PointF;
34import android.graphics.Rect;
35import android.os.Handler;
36import android.os.HandlerThread;
37import android.os.Message;
38import android.view.ContextThemeWrapper;
39import android.view.Display;
40import android.view.DisplayListCanvas;
41import android.view.PixelCopy;
42import android.view.RenderNode;
43import android.view.Surface;
44import android.view.SurfaceControl;
45import android.view.SurfaceHolder;
46import android.view.SurfaceSession;
47import android.view.SurfaceView;
48import android.view.ThreadedRenderer;
49import android.view.View;
50import android.view.ViewRootImpl;
51
52import com.android.internal.R;
53import com.android.internal.util.Preconditions;
54
55/**
56 * Android magnifier widget. Can be used by any view which is attached to a window.
57 */
58@UiThread
59public final class Magnifier {
60    // Use this to specify that a previous configuration value does not exist.
61    private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
62    // The callbacks of the pixel copy requests will be invoked on
63    // the Handler of this Thread when the copy is finished.
64    private static final HandlerThread sPixelCopyHandlerThread =
65            new HandlerThread("magnifier pixel copy result handler");
66
67    // The view to which this magnifier is attached.
68    private final View mView;
69    // The coordinates of the view in the surface.
70    private final int[] mViewCoordinatesInSurface;
71    // The window containing the magnifier.
72    private InternalPopupWindow mWindow;
73    // The width of the window containing the magnifier.
74    private final int mWindowWidth;
75    // The height of the window containing the magnifier.
76    private final int mWindowHeight;
77    // The zoom applied to the view region copied to the magnifier window.
78    private final float mZoom;
79    // The width of the bitmaps where the magnifier content is copied.
80    private final int mBitmapWidth;
81    // The height of the bitmaps where the magnifier content is copied.
82    private final int mBitmapHeight;
83    // The elevation of the window containing the magnifier.
84    private final float mWindowElevation;
85    // The corner radius of the window containing the magnifier.
86    private final float mWindowCornerRadius;
87    // The parent surface for the magnifier surface.
88    private SurfaceInfo mParentSurface;
89    // The surface where the content will be copied from.
90    private SurfaceInfo mContentCopySurface;
91    // The center coordinates of the window containing the magnifier.
92    private final Point mWindowCoords = new Point();
93    // The center coordinates of the content to be magnified,
94    // which can potentially contain a region outside the magnified view.
95    private final Point mCenterZoomCoords = new Point();
96    // The center coordinates of the content to be magnified,
97    // clamped inside the visible region of the magnified view.
98    private final Point mClampedCenterZoomCoords = new Point();
99    // Variables holding previous states, used for detecting redundant calls and invalidation.
100    private final Point mPrevStartCoordsInSurface = new Point(
101            NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
102    private final PointF mPrevPosInView = new PointF(
103            NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
104    // Rectangle defining the view surface area we pixel copy content from.
105    private final Rect mPixelCopyRequestRect = new Rect();
106    // Lock to synchronize between the UI thread and the thread that handles pixel copy results.
107    // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread.
108    private final Object mLock = new Object();
109
110    /**
111     * Initializes a magnifier.
112     *
113     * @param view the view for which this magnifier is attached
114     */
115    public Magnifier(@NonNull View view) {
116        mView = Preconditions.checkNotNull(view);
117        final Context context = mView.getContext();
118        mWindowWidth = context.getResources().getDimensionPixelSize(R.dimen.magnifier_width);
119        mWindowHeight = context.getResources().getDimensionPixelSize(R.dimen.magnifier_height);
120        mWindowElevation = context.getResources().getDimension(R.dimen.magnifier_elevation);
121        mWindowCornerRadius = getDeviceDefaultDialogCornerRadius();
122        mZoom = context.getResources().getFloat(R.dimen.magnifier_zoom_scale);
123        mBitmapWidth = Math.round(mWindowWidth / mZoom);
124        mBitmapHeight = Math.round(mWindowHeight / mZoom);
125        // The view's surface coordinates will not be updated until the magnifier is first shown.
126        mViewCoordinatesInSurface = new int[2];
127    }
128
129    static {
130        sPixelCopyHandlerThread.start();
131    }
132
133    /**
134     * Returns the device default theme dialog corner radius attribute.
135     * We retrieve this from the device default theme to avoid
136     * using the values set in the custom application themes.
137     */
138    private float getDeviceDefaultDialogCornerRadius() {
139        final Context deviceDefaultContext =
140                new ContextThemeWrapper(mView.getContext(), R.style.Theme_DeviceDefault);
141        final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
142                new int[]{android.R.attr.dialogCornerRadius});
143        final float dialogCornerRadius = ta.getDimension(0, 0);
144        ta.recycle();
145        return dialogCornerRadius;
146    }
147
148    /**
149     * Shows the magnifier on the screen.
150     *
151     * @param xPosInView horizontal coordinate of the center point of the magnifier source relative
152     *        to the view. The lower end is clamped to 0 and the higher end is clamped to the view
153     *        width.
154     * @param yPosInView vertical coordinate of the center point of the magnifier source
155     *        relative to the view. The lower end is clamped to 0 and the higher end is clamped to
156     *        the view height.
157     */
158    public void show(@FloatRange(from = 0) float xPosInView,
159            @FloatRange(from = 0) float yPosInView) {
160        xPosInView = Math.max(0, Math.min(xPosInView, mView.getWidth()));
161        yPosInView = Math.max(0, Math.min(yPosInView, mView.getHeight()));
162
163        obtainSurfaces();
164        obtainContentCoordinates(xPosInView, yPosInView);
165        obtainWindowCoordinates();
166
167        final int startX = mClampedCenterZoomCoords.x - mBitmapWidth / 2;
168        final int startY = mClampedCenterZoomCoords.y - mBitmapHeight / 2;
169        if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) {
170            if (mWindow == null) {
171                synchronized (mLock) {
172                    mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
173                            mParentSurface.mSurface,
174                            mWindowWidth, mWindowHeight, mWindowElevation, mWindowCornerRadius,
175                            Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
176                            mCallback);
177                }
178            }
179            performPixelCopy(startX, startY, true /* update window position */);
180            mPrevPosInView.x = xPosInView;
181            mPrevPosInView.y = yPosInView;
182        }
183    }
184
185    /**
186     * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
187     */
188    public void dismiss() {
189        if (mWindow != null) {
190            synchronized (mLock) {
191                mWindow.destroy();
192                mWindow = null;
193            }
194            mPrevPosInView.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
195            mPrevPosInView.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
196            mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
197            mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
198        }
199    }
200
201    /**
202     * Forces the magnifier to update its content. It uses the previous coordinates passed to
203     * {@link #show(float, float)}. This only happens if the magnifier is currently showing.
204     */
205    public void update() {
206        if (mWindow != null) {
207            obtainSurfaces();
208            // Update the content shown in the magnifier.
209            performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
210                    false /* update window position */);
211        }
212    }
213
214    /**
215     * @return The width of the magnifier window, in pixels.
216     */
217    public int getWidth() {
218        return mWindowWidth;
219    }
220
221    /**
222     * @return The height of the magnifier window, in pixels.
223     */
224    public int getHeight() {
225        return mWindowHeight;
226    }
227
228    /**
229     * @return The zoom applied to the magnified view region copied to the magnifier window.
230     * If the zoom is x and the magnifier window size is (width, height), the original size
231     * of the content copied in the magnifier will be (width / x, height / x).
232     */
233    public float getZoom() {
234        return mZoom;
235    }
236
237    /**
238     * @hide
239     *
240     * @return The top left coordinates of the magnifier, relative to the parent window.
241     */
242    @Nullable
243    public Point getWindowCoords() {
244        if (mWindow == null) {
245            return null;
246        }
247        final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
248        return new Point(mWindow.mLastDrawContentPositionX - surfaceInsets.left,
249                mWindow.mLastDrawContentPositionY - surfaceInsets.top);
250    }
251
252    /**
253     * Retrieves the surfaces used by the magnifier:
254     * - a parent surface for the magnifier surface. This will usually be the main app window.
255     * - a surface where the magnified content will be copied from. This will be the main app
256     *   window unless the magnified view is a SurfaceView, in which case its backing surface
257     *   will be used.
258     */
259    private void obtainSurfaces() {
260        // Get the main window surface.
261        SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
262        if (mView.getViewRootImpl() != null) {
263            final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
264            final Surface mainWindowSurface = viewRootImpl.mSurface;
265            if (mainWindowSurface != null && mainWindowSurface.isValid()) {
266                final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
267                final int surfaceWidth =
268                        viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
269                final int surfaceHeight =
270                        viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
271                validMainWindowSurface =
272                        new SurfaceInfo(mainWindowSurface, surfaceWidth, surfaceHeight, true);
273            }
274        }
275        // Get the surface backing the magnified view, if it is a SurfaceView.
276        SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
277        if (mView instanceof SurfaceView) {
278            final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
279            final Surface surfaceViewSurface = surfaceHolder.getSurface();
280            if (surfaceViewSurface != null && surfaceViewSurface.isValid()) {
281                final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
282                validSurfaceViewSurface = new SurfaceInfo(surfaceViewSurface,
283                        surfaceFrame.right, surfaceFrame.bottom, false);
284            }
285        }
286
287        // Choose the parent surface for the magnifier and the source surface for the content.
288        mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
289                ? validMainWindowSurface : validSurfaceViewSurface;
290        mContentCopySurface = mView instanceof SurfaceView
291                ? validSurfaceViewSurface : validMainWindowSurface;
292    }
293
294    /**
295     * Computes the coordinates of the center of the content going to be displayed in the
296     * magnifier. These are relative to the surface the content is copied from.
297     */
298    private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
299        final float posX;
300        final float posY;
301        mView.getLocationInSurface(mViewCoordinatesInSurface);
302        if (mView instanceof SurfaceView) {
303            // No offset required if the backing Surface matches the size of the SurfaceView.
304            posX = xPosInView;
305            posY = yPosInView;
306        } else {
307            posX = xPosInView + mViewCoordinatesInSurface[0];
308            posY = yPosInView + mViewCoordinatesInSurface[1];
309        }
310        mCenterZoomCoords.x = Math.round(posX);
311        mCenterZoomCoords.y = Math.round(posY);
312
313        // Clamp the x location to avoid magnifying content which does not belong
314        // to the magnified view. This will not take into account overlapping views.
315        final Rect viewVisibleRegion = new Rect();
316        mView.getGlobalVisibleRect(viewVisibleRegion);
317        if (mView.getViewRootImpl() != null) {
318            // Clamping coordinates relative to the surface, not to the window.
319            final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
320            viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
321        }
322        if (mView instanceof SurfaceView) {
323            // If we copy content from a SurfaceView, clamp coordinates relative to it.
324            viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
325        }
326        mClampedCenterZoomCoords.x = Math.max(viewVisibleRegion.left + mBitmapWidth / 2, Math.min(
327                mCenterZoomCoords.x, viewVisibleRegion.right - mBitmapWidth / 2));
328        mClampedCenterZoomCoords.y = mCenterZoomCoords.y;
329    }
330
331    private void obtainWindowCoordinates() {
332        // Compute the position of the magnifier window. Again, this has to be relative to the
333        // surface of the magnified view, as this surface is the parent of the magnifier surface.
334        final int verticalOffset = mView.getContext().getResources().getDimensionPixelSize(
335                R.dimen.magnifier_offset);
336        mWindowCoords.x = mCenterZoomCoords.x - mWindowWidth / 2;
337        mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalOffset;
338        if (mParentSurface != mContentCopySurface) {
339            mWindowCoords.x += mViewCoordinatesInSurface[0];
340            mWindowCoords.y += mViewCoordinatesInSurface[1];
341        }
342    }
343
344    private void performPixelCopy(final int startXInSurface, final int startYInSurface,
345            final boolean updateWindowPosition) {
346        if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
347            return;
348        }
349        // Clamp copy coordinates inside the surface to avoid displaying distorted content.
350        final int clampedStartXInSurface = Math.max(0,
351                Math.min(startXInSurface, mContentCopySurface.mWidth - mBitmapWidth));
352        final int clampedStartYInSurface = Math.max(0,
353                Math.min(startYInSurface, mContentCopySurface.mHeight - mBitmapHeight));
354
355        // Clamp window coordinates inside the parent surface, to avoid displaying
356        // the magnifier out of screen or overlapping with system insets.
357        final Rect windowBounds;
358        if (mParentSurface.mIsMainWindowSurface) {
359            final Rect systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
360            windowBounds = new Rect(systemInsets.left, systemInsets.top,
361                     mParentSurface.mWidth - systemInsets.right,
362                    mParentSurface.mHeight - systemInsets.bottom);
363        } else {
364            windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
365        }
366        final int windowCoordsX = Math.max(windowBounds.left,
367                Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
368        final int windowCoordsY = Math.max(windowBounds.top,
369                Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
370
371        // Perform the pixel copy.
372        mPixelCopyRequestRect.set(clampedStartXInSurface,
373                clampedStartYInSurface,
374                clampedStartXInSurface + mBitmapWidth,
375                clampedStartYInSurface + mBitmapHeight);
376        final InternalPopupWindow currentWindowInstance = mWindow;
377        final Bitmap bitmap =
378                Bitmap.createBitmap(mBitmapWidth, mBitmapHeight, Bitmap.Config.ARGB_8888);
379        PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
380                result -> {
381                    synchronized (mLock) {
382                        if (mWindow != currentWindowInstance) {
383                            // The magnifier was dismissed (and maybe shown again) in the meantime.
384                            return;
385                        }
386                        if (updateWindowPosition) {
387                            // TODO: pull the position update outside #performPixelCopy
388                            mWindow.setContentPositionForNextDraw(windowCoordsX, windowCoordsY);
389                        }
390                        mWindow.updateContent(bitmap);
391                    }
392                },
393                sPixelCopyHandlerThread.getThreadHandler());
394        mPrevStartCoordsInSurface.x = startXInSurface;
395        mPrevStartCoordsInSurface.y = startYInSurface;
396    }
397
398    /**
399     * Contains a surface and metadata corresponding to it.
400     */
401    private static class SurfaceInfo {
402        public static final SurfaceInfo NULL = new SurfaceInfo(null, 0, 0, false);
403
404        private Surface mSurface;
405        private int mWidth;
406        private int mHeight;
407        private boolean mIsMainWindowSurface;
408
409        SurfaceInfo(final Surface surface, final int width, final int height,
410                final boolean isMainWindowSurface) {
411            mSurface = surface;
412            mWidth = width;
413            mHeight = height;
414            mIsMainWindowSurface = isMainWindowSurface;
415        }
416    }
417
418    /**
419     * Magnifier's own implementation of PopupWindow-similar floating window.
420     * This exists to ensure frame-synchronization between window position updates and window
421     * content updates. By using a PopupWindow, these events would happen in different frames,
422     * producing a shakiness effect for the magnifier content.
423     */
424    private static class InternalPopupWindow {
425        // The alpha set on the magnifier's content, which defines how
426        // prominent the white background is.
427        private static final int CONTENT_BITMAP_ALPHA = 242;
428        // The z of the magnifier surface, defining its z order in the list of
429        // siblings having the same parent surface (usually the main app surface).
430        private static final int SURFACE_Z = 5;
431
432        // Display associated to the view the magnifier is attached to.
433        private final Display mDisplay;
434        // The size of the content of the magnifier.
435        private final int mContentWidth;
436        private final int mContentHeight;
437        // The size of the allocated surface.
438        private final int mSurfaceWidth;
439        private final int mSurfaceHeight;
440        // The insets of the content inside the allocated surface.
441        private final int mOffsetX;
442        private final int mOffsetY;
443        // The surface we allocate for the magnifier content + shadow.
444        private final SurfaceSession mSurfaceSession;
445        private final SurfaceControl mSurfaceControl;
446        private final Surface mSurface;
447        // The renderer used for the allocated surface.
448        private final ThreadedRenderer.SimpleRenderer mRenderer;
449        // The RenderNode used to draw the magnifier content in the surface.
450        private final RenderNode mBitmapRenderNode;
451        // The job that will be post'd to apply the pending magnifier updates to the surface.
452        private final Runnable mMagnifierUpdater;
453        // The handler where the magnifier updater jobs will be post'd.
454        private final Handler mHandler;
455        // The callback to be run after the next draw.
456        private Callback mCallback;
457        // The position of the magnifier content when the last draw was requested.
458        private int mLastDrawContentPositionX;
459        private int mLastDrawContentPositionY;
460
461        // Members below describe the state of the magnifier. Reads/writes to them
462        // have to be synchronized between the UI thread and the thread that handles
463        // the pixel copy results. This is the purpose of mLock.
464        private final Object mLock;
465        // Whether a magnifier frame draw is currently pending in the UI thread queue.
466        private boolean mFrameDrawScheduled;
467        // The content bitmap.
468        private Bitmap mBitmap;
469        // Whether the next draw will be the first one for the current instance.
470        private boolean mFirstDraw = true;
471        // The window position in the parent surface. Might be applied during the next draw,
472        // when mPendingWindowPositionUpdate is true.
473        private int mWindowPositionX;
474        private int mWindowPositionY;
475        private boolean mPendingWindowPositionUpdate;
476
477        // The lock used to synchronize the UI and render threads when a #destroy
478        // is performed on the UI thread and a frame callback on the render thread.
479        // When both mLock and mDestroyLock need to be held at the same time,
480        // mDestroyLock should be acquired before mLock in order to avoid deadlocks.
481        private final Object mDestroyLock = new Object();
482
483        InternalPopupWindow(final Context context, final Display display,
484                final Surface parentSurface,
485                final int width, final int height, final float elevation, final float cornerRadius,
486                final Handler handler, final Object lock, final Callback callback) {
487            mDisplay = display;
488            mLock = lock;
489            mCallback = callback;
490
491            mContentWidth = width;
492            mContentHeight = height;
493            mOffsetX = (int) (0.1f * width);
494            mOffsetY = (int) (0.1f * height);
495            // Setup the surface we will use for drawing the content and shadow.
496            mSurfaceWidth = mContentWidth + 2 * mOffsetX;
497            mSurfaceHeight = mContentHeight + 2 * mOffsetY;
498            mSurfaceSession = new SurfaceSession(parentSurface);
499            mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
500                    .setFormat(PixelFormat.TRANSLUCENT)
501                    .setSize(mSurfaceWidth, mSurfaceHeight)
502                    .setName("magnifier surface")
503                    .setFlags(SurfaceControl.HIDDEN)
504                    .build();
505            mSurface = new Surface();
506            mSurface.copyFrom(mSurfaceControl);
507
508            // Setup the RenderNode tree. The root has only one child, which contains the bitmap.
509            mRenderer = new ThreadedRenderer.SimpleRenderer(
510                    context,
511                    "magnifier renderer",
512                    mSurface
513            );
514            mBitmapRenderNode = createRenderNodeForBitmap(
515                    "magnifier content",
516                    elevation,
517                    cornerRadius
518            );
519
520            final DisplayListCanvas canvas = mRenderer.getRootNode().start(width, height);
521            try {
522                canvas.insertReorderBarrier();
523                canvas.drawRenderNode(mBitmapRenderNode);
524                canvas.insertInorderBarrier();
525            } finally {
526                mRenderer.getRootNode().end(canvas);
527            }
528
529            // Initialize the update job and the handler where this will be post'd.
530            mHandler = handler;
531            mMagnifierUpdater = this::doDraw;
532            mFrameDrawScheduled = false;
533        }
534
535        private RenderNode createRenderNodeForBitmap(final String name,
536                final float elevation, final float cornerRadius) {
537            final RenderNode bitmapRenderNode = RenderNode.create(name, null);
538
539            // Define the position of the bitmap in the parent render node. The surface regions
540            // outside the bitmap are used to draw elevation.
541            bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
542                    mOffsetX + mContentWidth, mOffsetY + mContentHeight);
543            bitmapRenderNode.setElevation(elevation);
544
545            final Outline outline = new Outline();
546            outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
547            outline.setAlpha(1.0f);
548            bitmapRenderNode.setOutline(outline);
549            bitmapRenderNode.setClipToOutline(true);
550
551            // Create a dummy draw, which will be replaced later with real drawing.
552            final DisplayListCanvas canvas = bitmapRenderNode.start(mContentWidth, mContentHeight);
553            try {
554                canvas.drawColor(0xFF00FF00);
555            } finally {
556                bitmapRenderNode.end(canvas);
557            }
558
559            return bitmapRenderNode;
560        }
561
562        /**
563         * Sets the position of the magnifier content relative to the parent surface.
564         * The position update will happen in the same frame with the next draw.
565         * The method has to be called in a context that holds {@link #mLock}.
566         *
567         * @param contentX the x coordinate of the content
568         * @param contentY the y coordinate of the content
569         */
570        public void setContentPositionForNextDraw(final int contentX, final int contentY) {
571            mWindowPositionX = contentX - mOffsetX;
572            mWindowPositionY = contentY - mOffsetY;
573            mPendingWindowPositionUpdate = true;
574            requestUpdate();
575        }
576
577        /**
578         * Sets the content that should be displayed in the magnifier.
579         * The update happens immediately, and possibly triggers a pending window movement set
580         * by {@link #setContentPositionForNextDraw(int, int)}.
581         * The method has to be called in a context that holds {@link #mLock}.
582         *
583         * @param bitmap the content bitmap
584         */
585        public void updateContent(final @NonNull Bitmap bitmap) {
586            if (mBitmap != null) {
587                mBitmap.recycle();
588            }
589            mBitmap = bitmap;
590            requestUpdate();
591        }
592
593        private void requestUpdate() {
594            if (mFrameDrawScheduled) {
595                return;
596            }
597            final Message request = Message.obtain(mHandler, mMagnifierUpdater);
598            request.setAsynchronous(true);
599            request.sendToTarget();
600            mFrameDrawScheduled = true;
601        }
602
603        /**
604         * Destroys this instance.
605         */
606        public void destroy() {
607            synchronized (mDestroyLock) {
608                mSurface.destroy();
609            }
610            synchronized (mLock) {
611                mRenderer.destroy();
612                mSurfaceControl.destroy();
613                mSurfaceSession.kill();
614                mBitmapRenderNode.destroy();
615                mHandler.removeCallbacks(mMagnifierUpdater);
616                if (mBitmap != null) {
617                    mBitmap.recycle();
618                }
619            }
620        }
621
622        private void doDraw() {
623            final ThreadedRenderer.FrameDrawingCallback callback;
624
625            // Draw the current bitmap to the surface, and prepare the callback which updates the
626            // surface position. These have to be in the same synchronized block, in order to
627            // guarantee the consistency between the bitmap content and the surface position.
628            synchronized (mLock) {
629                if (!mSurface.isValid()) {
630                    // Probably #destroy() was called for the current instance, so we skip the draw.
631                    return;
632                }
633
634                final DisplayListCanvas canvas =
635                        mBitmapRenderNode.start(mContentWidth, mContentHeight);
636                try {
637                    canvas.drawColor(Color.WHITE);
638
639                    final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
640                    final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
641                    final Paint paint = new Paint();
642                    paint.setFilterBitmap(true);
643                    paint.setAlpha(CONTENT_BITMAP_ALPHA);
644                    canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
645                } finally {
646                    mBitmapRenderNode.end(canvas);
647                }
648
649                if (mPendingWindowPositionUpdate || mFirstDraw) {
650                    // If the window has to be shown or moved, defer this until the next draw.
651                    final boolean firstDraw = mFirstDraw;
652                    mFirstDraw = false;
653                    final boolean updateWindowPosition = mPendingWindowPositionUpdate;
654                    mPendingWindowPositionUpdate = false;
655                    final int pendingX = mWindowPositionX;
656                    final int pendingY = mWindowPositionY;
657
658                    callback = frame -> {
659                        synchronized (mDestroyLock) {
660                            if (!mSurface.isValid()) {
661                                return;
662                            }
663                            synchronized (mLock) {
664                                mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
665                                // Show or move the window at the content draw frame.
666                                SurfaceControl.openTransaction();
667                                mSurfaceControl.deferTransactionUntil(mSurface, frame);
668                                if (updateWindowPosition) {
669                                    mSurfaceControl.setPosition(pendingX, pendingY);
670                                }
671                                if (firstDraw) {
672                                    mSurfaceControl.setLayer(SURFACE_Z);
673                                    mSurfaceControl.show();
674                                }
675                                SurfaceControl.closeTransaction();
676                            }
677                        }
678                    };
679                } else {
680                    callback = null;
681                }
682
683                mLastDrawContentPositionX = mWindowPositionX + mOffsetX;
684                mLastDrawContentPositionY = mWindowPositionY + mOffsetY;
685                mFrameDrawScheduled = false;
686            }
687
688            mRenderer.draw(callback);
689            if (mCallback != null) {
690                mCallback.onOperationComplete();
691            }
692        }
693    }
694
695    // The rest of the file consists of test APIs.
696
697    /**
698     * See {@link #setOnOperationCompleteCallback(Callback)}.
699     */
700    @TestApi
701    private Callback mCallback;
702
703    /**
704     * Sets a callback which will be invoked at the end of the next
705     * {@link #show(float, float)} or {@link #update()} operation.
706     *
707     * @hide
708     */
709    @TestApi
710    public void setOnOperationCompleteCallback(final Callback callback) {
711        mCallback = callback;
712        if (mWindow != null) {
713            mWindow.mCallback = callback;
714        }
715    }
716
717    /**
718     * @return the content being currently displayed in the magnifier, as bitmap
719     *
720     * @hide
721     */
722    @TestApi
723    public @Nullable Bitmap getContent() {
724        if (mWindow == null) {
725            return null;
726        }
727        synchronized (mWindow.mLock) {
728            return Bitmap.createScaledBitmap(mWindow.mBitmap, mWindowWidth, mWindowHeight, true);
729        }
730    }
731
732    /**
733     * @return the position of the magnifier window relative to the screen
734     *
735     * @hide
736     */
737    @TestApi
738    public Rect getWindowPositionOnScreen() {
739        final int[] viewLocationOnScreen = new int[2];
740        mView.getLocationOnScreen(viewLocationOnScreen);
741        final int[] viewLocationInSurface = new int[2];
742        mView.getLocationInSurface(viewLocationInSurface);
743
744        final int left = mWindowCoords.x + viewLocationOnScreen[0] - viewLocationInSurface[0];
745        final int top = mWindowCoords.y + viewLocationOnScreen[1] - viewLocationInSurface[1];
746        return new Rect(left, top, left + mWindowWidth, top + mWindowHeight);
747    }
748
749    /**
750     * @return the size of the magnifier window in dp
751     *
752     * @hide
753     */
754    @TestApi
755    public static PointF getMagnifierDefaultSize() {
756        final Resources resources = Resources.getSystem();
757        final float density = resources.getDisplayMetrics().density;
758        final PointF size = new PointF();
759        size.x = resources.getDimension(R.dimen.magnifier_width) / density;
760        size.y = resources.getDimension(R.dimen.magnifier_height) / density;
761        return size;
762    }
763
764    /**
765     * @hide
766     */
767    @TestApi
768    public interface Callback {
769        /**
770         * Callback called after the drawing for a magnifier update has happened.
771         */
772        void onOperationComplete();
773    }
774}
775