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 
17 package android.widget;
18 
19 import android.annotation.FloatRange;
20 import android.annotation.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.Px;
25 import android.annotation.TestApi;
26 import android.annotation.UiThread;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.content.res.TypedArray;
30 import android.graphics.Bitmap;
31 import android.graphics.Canvas;
32 import android.graphics.Color;
33 import android.graphics.Insets;
34 import android.graphics.Outline;
35 import android.graphics.Paint;
36 import android.graphics.PixelFormat;
37 import android.graphics.Point;
38 import android.graphics.PointF;
39 import android.graphics.RecordingCanvas;
40 import android.graphics.Rect;
41 import android.graphics.RenderNode;
42 import android.graphics.drawable.ColorDrawable;
43 import android.graphics.drawable.Drawable;
44 import android.os.Handler;
45 import android.os.HandlerThread;
46 import android.os.Message;
47 import android.util.Log;
48 import android.view.ContextThemeWrapper;
49 import android.view.Display;
50 import android.view.PixelCopy;
51 import android.view.Surface;
52 import android.view.SurfaceControl;
53 import android.view.SurfaceHolder;
54 import android.view.SurfaceSession;
55 import android.view.SurfaceView;
56 import android.view.ThreadedRenderer;
57 import android.view.View;
58 import android.view.ViewRootImpl;
59 
60 import com.android.internal.R;
61 import com.android.internal.util.Preconditions;
62 
63 import java.lang.annotation.Retention;
64 import java.lang.annotation.RetentionPolicy;
65 
66 /**
67  * Android magnifier widget. Can be used by any view which is attached to a window.
68  */
69 @UiThread
70 public final class Magnifier {
71     private static final String TAG = "Magnifier";
72     // Use this to specify that a previous configuration value does not exist.
73     private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
74     // The callbacks of the pixel copy requests will be invoked on
75     // the Handler of this Thread when the copy is finished.
76     private static final HandlerThread sPixelCopyHandlerThread =
77             new HandlerThread("magnifier pixel copy result handler");
78 
79     // The view to which this magnifier is attached.
80     private final View mView;
81     // The coordinates of the view in the surface.
82     private final int[] mViewCoordinatesInSurface;
83     // The window containing the magnifier.
84     private InternalPopupWindow mWindow;
85     // The width of the window containing the magnifier.
86     private final int mWindowWidth;
87     // The height of the window containing the magnifier.
88     private final int mWindowHeight;
89     // The zoom applied to the view region copied to the magnifier view.
90     private float mZoom;
91     // The width of the content that will be copied to the magnifier.
92     private int mSourceWidth;
93     // The height of the content that will be copied to the magnifier.
94     private int mSourceHeight;
95     // Whether the zoom of the magnifier or the view position have changed since last content copy.
96     private boolean mDirtyState;
97     // The elevation of the window containing the magnifier.
98     private final float mWindowElevation;
99     // The corner radius of the window containing the magnifier.
100     private final float mWindowCornerRadius;
101     // The overlay to be drawn on the top of the magnifier content.
102     private final Drawable mOverlay;
103     // The horizontal offset between the source and window coords when #show(float, float) is used.
104     private final int mDefaultHorizontalSourceToMagnifierOffset;
105     // The vertical offset between the source and window coords when #show(float, float) is used.
106     private final int mDefaultVerticalSourceToMagnifierOffset;
107     // Whether the area where the magnifier can be positioned will be clipped to the main window
108     // and within system insets.
109     private final boolean mClippingEnabled;
110     // The behavior of the left bound of the rectangle where the content can be copied from.
111     private @SourceBound int mLeftContentBound;
112     // The behavior of the top bound of the rectangle where the content can be copied from.
113     private @SourceBound int mTopContentBound;
114     // The behavior of the right bound of the rectangle where the content can be copied from.
115     private @SourceBound int mRightContentBound;
116     // The behavior of the bottom bound of the rectangle where the content can be copied from.
117     private @SourceBound int mBottomContentBound;
118     // The parent surface for the magnifier surface.
119     private SurfaceInfo mParentSurface;
120     // The surface where the content will be copied from.
121     private SurfaceInfo mContentCopySurface;
122     // The center coordinates of the window containing the magnifier.
123     private final Point mWindowCoords = new Point();
124     // The center coordinates of the content to be magnified,
125     // clamped inside the visible region of the magnified view.
126     private final Point mClampedCenterZoomCoords = new Point();
127     // Variables holding previous states, used for detecting redundant calls and invalidation.
128     private final Point mPrevStartCoordsInSurface = new Point(
129             NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
130     private final PointF mPrevShowSourceCoords = new PointF(
131             NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
132     private final PointF mPrevShowWindowCoords = new PointF(
133             NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
134     // Rectangle defining the view surface area we pixel copy content from.
135     private final Rect mPixelCopyRequestRect = new Rect();
136     // Lock to synchronize between the UI thread and the thread that handles pixel copy results.
137     // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread.
138     private final Object mLock = new Object();
139 
140     /**
141      * Initializes a magnifier.
142      *
143      * @param view the view for which this magnifier is attached
144      *
145      * @deprecated Please use {@link Builder} instead
146      */
147     @Deprecated
Magnifier(@onNull View view)148     public Magnifier(@NonNull View view) {
149         this(createBuilderWithOldMagnifierDefaults(view));
150     }
151 
createBuilderWithOldMagnifierDefaults(final View view)152     static Builder createBuilderWithOldMagnifierDefaults(final View view) {
153         final Builder params = new Builder(view);
154         final Context context = view.getContext();
155         final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier,
156                 R.attr.magnifierStyle, 0);
157         params.mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0);
158         params.mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0);
159         params.mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0);
160         params.mCornerRadius = getDeviceDefaultDialogCornerRadius(context);
161         params.mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0);
162         params.mHorizontalDefaultSourceToMagnifierOffset =
163                 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0);
164         params.mVerticalDefaultSourceToMagnifierOffset =
165                 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0);
166         params.mOverlay = new ColorDrawable(a.getColor(
167                 R.styleable.Magnifier_magnifierColorOverlay, Color.TRANSPARENT));
168         a.recycle();
169         params.mClippingEnabled = true;
170         params.mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
171         params.mTopContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
172         params.mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
173         params.mBottomContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
174         return params;
175     }
176 
177     /**
178      * Returns the device default theme dialog corner radius attribute.
179      * We retrieve this from the device default theme to avoid
180      * using the values set in the custom application themes.
181      */
getDeviceDefaultDialogCornerRadius(final Context context)182     private static float getDeviceDefaultDialogCornerRadius(final Context context) {
183         final Context deviceDefaultContext =
184                 new ContextThemeWrapper(context, R.style.Theme_DeviceDefault);
185         final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
186                 new int[]{android.R.attr.dialogCornerRadius});
187         final float dialogCornerRadius = ta.getDimension(0, 0);
188         ta.recycle();
189         return dialogCornerRadius;
190     }
191 
Magnifier(@onNull Builder params)192     private Magnifier(@NonNull Builder params) {
193         // Copy params from builder.
194         mView = params.mView;
195         mWindowWidth = params.mWidth;
196         mWindowHeight = params.mHeight;
197         mZoom = params.mZoom;
198         mSourceWidth = Math.round(mWindowWidth / mZoom);
199         mSourceHeight = Math.round(mWindowHeight / mZoom);
200         mWindowElevation = params.mElevation;
201         mWindowCornerRadius = params.mCornerRadius;
202         mOverlay = params.mOverlay;
203         mDefaultHorizontalSourceToMagnifierOffset =
204                 params.mHorizontalDefaultSourceToMagnifierOffset;
205         mDefaultVerticalSourceToMagnifierOffset =
206                 params.mVerticalDefaultSourceToMagnifierOffset;
207         mClippingEnabled = params.mClippingEnabled;
208         mLeftContentBound = params.mLeftContentBound;
209         mTopContentBound = params.mTopContentBound;
210         mRightContentBound = params.mRightContentBound;
211         mBottomContentBound = params.mBottomContentBound;
212         // The view's surface coordinates will not be updated until the magnifier is first shown.
213         mViewCoordinatesInSurface = new int[2];
214     }
215 
216     static {
sPixelCopyHandlerThread.start()217         sPixelCopyHandlerThread.start();
218     }
219 
220     /**
221      * Shows the magnifier on the screen. The method takes the coordinates of the center
222      * of the content source going to be magnified and copied to the magnifier. The coordinates
223      * are relative to the top left corner of the magnified view. The magnifier will be
224      * positioned such that its center will be at the default offset from the center of the source.
225      * The default offset can be specified using the method
226      * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should
227      * be different across calls to this method, you should consider to use method
228      * {@link #show(float, float, float, float)} instead.
229      *
230      * @param sourceCenterX horizontal coordinate of the source center, relative to the view
231      * @param sourceCenterY vertical coordinate of the source center, relative to the view
232      *
233      * @see Builder#setDefaultSourceToMagnifierOffset(int, int)
234      * @see Builder#getDefaultHorizontalSourceToMagnifierOffset()
235      * @see Builder#getDefaultVerticalSourceToMagnifierOffset()
236      * @see #show(float, float, float, float)
237      */
show(@loatRangefrom = 0) float sourceCenterX, @FloatRange(from = 0) float sourceCenterY)238     public void show(@FloatRange(from = 0) float sourceCenterX,
239             @FloatRange(from = 0) float sourceCenterY) {
240         show(sourceCenterX, sourceCenterY,
241                 sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset,
242                 sourceCenterY + mDefaultVerticalSourceToMagnifierOffset);
243     }
244 
245     /**
246      * Shows the magnifier on the screen at a position that is independent from its content
247      * position. The first two arguments represent the coordinates of the center of the
248      * content source going to be magnified and copied to the magnifier. The last two arguments
249      * represent the coordinates of the center of the magnifier itself. All four coordinates
250      * are relative to the top left corner of the magnified view. If you consider using this
251      * method such that the offset between the source center and the magnifier center coordinates
252      * remains constant, you should consider using method {@link #show(float, float)} instead.
253      *
254      * @param sourceCenterX horizontal coordinate of the source center relative to the view
255      * @param sourceCenterY vertical coordinate of the source center, relative to the view
256      * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view
257      * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view
258      */
show(@loatRangefrom = 0) float sourceCenterX, @FloatRange(from = 0) float sourceCenterY, float magnifierCenterX, float magnifierCenterY)259     public void show(@FloatRange(from = 0) float sourceCenterX,
260             @FloatRange(from = 0) float sourceCenterY,
261             float magnifierCenterX, float magnifierCenterY) {
262 
263         obtainSurfaces();
264         obtainContentCoordinates(sourceCenterX, sourceCenterY);
265         obtainWindowCoordinates(magnifierCenterX, magnifierCenterY);
266 
267         final int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2;
268         final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2;
269         if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y
270                 || mDirtyState) {
271             if (mWindow == null) {
272                 synchronized (mLock) {
273                     mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
274                             mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight,
275                             mWindowElevation, mWindowCornerRadius,
276                             mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT),
277                             Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
278                             mCallback);
279                 }
280             }
281             performPixelCopy(startX, startY, true /* update window position */);
282         } else if (magnifierCenterX != mPrevShowWindowCoords.x
283                 || magnifierCenterY != mPrevShowWindowCoords.y) {
284             final Point windowCoords = getCurrentClampedWindowCoordinates();
285             final InternalPopupWindow currentWindowInstance = mWindow;
286             sPixelCopyHandlerThread.getThreadHandler().post(() -> {
287                 synchronized (mLock) {
288                     if (mWindow != currentWindowInstance) {
289                         // The magnifier was dismissed (and maybe shown again) in the meantime.
290                         return;
291                     }
292                     mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
293                 }
294             });
295         }
296         mPrevShowSourceCoords.x = sourceCenterX;
297         mPrevShowSourceCoords.y = sourceCenterY;
298         mPrevShowWindowCoords.x = magnifierCenterX;
299         mPrevShowWindowCoords.y = magnifierCenterY;
300     }
301 
302     /**
303      * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
304      */
dismiss()305     public void dismiss() {
306         if (mWindow != null) {
307             synchronized (mLock) {
308                 mWindow.destroy();
309                 mWindow = null;
310             }
311             mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
312             mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
313             mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
314             mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
315             mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
316             mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
317         }
318     }
319 
320     /**
321      * Asks the magnifier to update its content. It uses the previous coordinates passed to
322      * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The
323      * method only has effect if the magnifier is currently showing.
324      */
update()325     public void update() {
326         if (mWindow != null) {
327             obtainSurfaces();
328             if (!mDirtyState) {
329                 // Update the content shown in the magnifier.
330                 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
331                         false /* update window position */);
332             } else {
333                 // If for example the zoom has changed, we cannot use the same top left
334                 // coordinates as before, so just #show again to have them recomputed.
335                 show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y,
336                         mPrevShowWindowCoords.x, mPrevShowWindowCoords.y);
337             }
338         }
339     }
340 
341     /**
342      * @return the width of the magnifier window, in pixels
343      * @see Magnifier.Builder#setSize(int, int)
344      */
345     @Px
getWidth()346     public int getWidth() {
347         return mWindowWidth;
348     }
349 
350     /**
351      * @return the height of the magnifier window, in pixels
352      * @see Magnifier.Builder#setSize(int, int)
353      */
354     @Px
getHeight()355     public int getHeight() {
356         return mWindowHeight;
357     }
358 
359     /**
360      * @return the initial width of the content magnified and copied to the magnifier, in pixels
361      * @see Magnifier.Builder#setSize(int, int)
362      * @see Magnifier.Builder#setInitialZoom(float)
363      */
364     @Px
getSourceWidth()365     public int getSourceWidth() {
366         return mSourceWidth;
367     }
368 
369     /**
370      * @return the initial height of the content magnified and copied to the magnifier, in pixels
371      * @see Magnifier.Builder#setSize(int, int)
372      * @see Magnifier.Builder#setInitialZoom(float)
373      */
374     @Px
getSourceHeight()375     public int getSourceHeight() {
376         return mSourceHeight;
377     }
378 
379     /**
380      * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup.
381      * The change will become effective at the next #show or #update call.
382      * @param zoom the zoom to be set
383      */
setZoom(@loatRangefrom = 0f) float zoom)384     public void setZoom(@FloatRange(from = 0f) float zoom) {
385         Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
386         mZoom = zoom;
387         mSourceWidth = Math.round(mWindowWidth / mZoom);
388         mSourceHeight = Math.round(mWindowHeight / mZoom);
389         mDirtyState = true;
390     }
391 
392     /**
393      * Returns the zoom to be applied to the magnified view region copied to the magnifier.
394      * If the zoom is x and the magnifier window size is (width, height), the original size
395      * of the content being magnified will be (width / x, height / x).
396      * @return the zoom applied to the content
397      * @see Magnifier.Builder#setInitialZoom(float)
398      */
getZoom()399     public float getZoom() {
400         return mZoom;
401     }
402 
403     /**
404      * @return the elevation set for the magnifier window, in pixels
405      * @see Magnifier.Builder#setElevation(float)
406      */
407     @Px
getElevation()408     public float getElevation() {
409         return mWindowElevation;
410     }
411 
412     /**
413      * @return the corner radius of the magnifier window, in pixels
414      * @see Magnifier.Builder#setCornerRadius(float)
415      */
416     @Px
getCornerRadius()417     public float getCornerRadius() {
418         return mWindowCornerRadius;
419     }
420 
421     /**
422      * Returns the horizontal offset, in pixels, to be applied to the source center position
423      * to obtain the magnifier center position when {@link #show(float, float)} is called.
424      * The value is ignored when {@link #show(float, float, float, float)} is used instead.
425      *
426      * @return the default horizontal offset between the source center and the magnifier
427      * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
428      * @see Magnifier#show(float, float)
429      */
430     @Px
getDefaultHorizontalSourceToMagnifierOffset()431     public int getDefaultHorizontalSourceToMagnifierOffset() {
432         return mDefaultHorizontalSourceToMagnifierOffset;
433     }
434 
435     /**
436      * Returns the vertical offset, in pixels, to be applied to the source center position
437      * to obtain the magnifier center position when {@link #show(float, float)} is called.
438      * The value is ignored when {@link #show(float, float, float, float)} is used instead.
439      *
440      * @return the default vertical offset between the source center and the magnifier
441      * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
442      * @see Magnifier#show(float, float)
443      */
444     @Px
getDefaultVerticalSourceToMagnifierOffset()445     public int getDefaultVerticalSourceToMagnifierOffset() {
446         return mDefaultVerticalSourceToMagnifierOffset;
447     }
448 
449     /**
450      * Returns the overlay to be drawn on the top of the magnifier, or
451      * {@code null} if no overlay should be drawn.
452      * @return the overlay
453      * @see Magnifier.Builder#setOverlay(Drawable)
454      */
455     @Nullable
getOverlay()456     public Drawable getOverlay() {
457         return mOverlay;
458     }
459 
460     /**
461      * Returns whether the magnifier position will be adjusted such that the magnifier will be
462      * fully within the bounds of the main application window, by also avoiding any overlap
463      * with system insets (such as the one corresponding to the status bar) i.e. whether the
464      * area where the magnifier can be positioned will be clipped to the main application window
465      * and the system insets.
466      * @return whether the magnifier position will be adjusted
467      * @see Magnifier.Builder#setClippingEnabled(boolean)
468      */
isClippingEnabled()469     public boolean isClippingEnabled() {
470         return mClippingEnabled;
471     }
472 
473     /**
474      * Returns the top left coordinates of the magnifier, relative to the main application
475      * window. They will be determined by the coordinates of the last {@link #show(float, float)}
476      * or {@link #show(float, float, float, float)} call, adjusted to take into account any
477      * potential clamping behavior. The method can be used immediately after a #show
478      * call to find out where the magnifier will be positioned. However, the position of the
479      * magnifier will not be updated visually in the same frame, due to the async nature of
480      * the content copying and of the magnifier rendering.
481      * The method will return {@code null} if #show has not yet been called, or if the last
482      * operation performed was a #dismiss.
483      *
484      * @return the top left coordinates of the magnifier
485      */
486     @Nullable
getPosition()487     public Point getPosition() {
488         if (mWindow == null) {
489             return null;
490         }
491         final Point position = getCurrentClampedWindowCoordinates();
492         position.offset(-mParentSurface.mInsets.left, -mParentSurface.mInsets.top);
493         return new Point(position);
494     }
495 
496     /**
497      * Returns the top left coordinates of the magnifier source (i.e. the view region going to
498      * be magnified and copied to the magnifier), relative to the window or surface the content
499      * is copied from. The content will be copied:
500      * - if the magnified view is a {@link SurfaceView}, from the surface backing it
501      * - otherwise, from the surface backing the main application window, and the coordinates
502      *   returned will be relative to the main application window
503      * The method will return {@code null} if #show has not yet been called, or if the last
504      * operation performed was a #dismiss.
505      *
506      * @return the top left coordinates of the magnifier source
507      */
508     @Nullable
getSourcePosition()509     public Point getSourcePosition() {
510         if (mWindow == null) {
511             return null;
512         }
513         final Point position = new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top);
514         position.offset(-mContentCopySurface.mInsets.left, -mContentCopySurface.mInsets.top);
515         return new Point(position);
516     }
517 
518     /**
519      * Retrieves the surfaces used by the magnifier:
520      * - a parent surface for the magnifier surface. This will usually be the main app window.
521      * - a surface where the magnified content will be copied from. This will be the main app
522      *   window unless the magnified view is a SurfaceView, in which case its backing surface
523      *   will be used.
524      */
obtainSurfaces()525     private void obtainSurfaces() {
526         // Get the main window surface.
527         SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
528         if (mView.getViewRootImpl() != null) {
529             final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
530             final Surface mainWindowSurface = viewRootImpl.mSurface;
531             if (mainWindowSurface != null && mainWindowSurface.isValid()) {
532                 final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
533                 final int surfaceWidth =
534                         viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
535                 final int surfaceHeight =
536                         viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
537                 validMainWindowSurface =
538                         new SurfaceInfo(viewRootImpl.getSurfaceControl(), mainWindowSurface,
539                                 surfaceWidth, surfaceHeight, surfaceInsets, true);
540             }
541         }
542         // Get the surface backing the magnified view, if it is a SurfaceView.
543         SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
544         if (mView instanceof SurfaceView) {
545             final SurfaceControl sc = ((SurfaceView) mView).getSurfaceControl();
546             final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
547             final Surface surfaceViewSurface = surfaceHolder.getSurface();
548 
549             if (sc != null && sc.isValid()) {
550                 final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
551                 validSurfaceViewSurface = new SurfaceInfo(sc, surfaceViewSurface,
552                         surfaceFrame.right, surfaceFrame.bottom, new Rect(), false);
553             }
554         }
555 
556         // Choose the parent surface for the magnifier and the source surface for the content.
557         mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
558                 ? validMainWindowSurface : validSurfaceViewSurface;
559         mContentCopySurface = mView instanceof SurfaceView
560                 ? validSurfaceViewSurface : validMainWindowSurface;
561     }
562 
563     /**
564      * Computes the coordinates of the center of the content going to be displayed in the
565      * magnifier. These are relative to the surface the content is copied from.
566      */
obtainContentCoordinates(final float xPosInView, final float yPosInView)567     private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
568         final int prevViewXInSurface = mViewCoordinatesInSurface[0];
569         final int prevViewYInSurface = mViewCoordinatesInSurface[1];
570         mView.getLocationInSurface(mViewCoordinatesInSurface);
571         if (mViewCoordinatesInSurface[0] != prevViewXInSurface
572                 || mViewCoordinatesInSurface[1] != prevViewYInSurface) {
573             mDirtyState = true;
574         }
575 
576         final int zoomCenterX;
577         final int zoomCenterY;
578         if (mView instanceof SurfaceView) {
579             // No offset required if the backing Surface matches the size of the SurfaceView.
580             zoomCenterX = Math.round(xPosInView);
581             zoomCenterY = Math.round(yPosInView);
582         } else {
583             zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]);
584             zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]);
585         }
586 
587         final Rect[] bounds = new Rect[2]; // [MAX_IN_SURFACE, MAX_VISIBLE]
588         // Obtain the surface bounds rectangle.
589         final Rect surfaceBounds = new Rect(0, 0,
590                 mContentCopySurface.mWidth, mContentCopySurface.mHeight);
591         bounds[0] = surfaceBounds;
592         // Obtain the visible view region rectangle.
593         final Rect viewVisibleRegion = new Rect();
594         mView.getGlobalVisibleRect(viewVisibleRegion);
595         if (mView.getViewRootImpl() != null) {
596             // Clamping coordinates relative to the surface, not to the window.
597             final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
598             viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
599         }
600         if (mView instanceof SurfaceView) {
601             // If we copy content from a SurfaceView, clamp coordinates relative to it.
602             viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
603         }
604         bounds[1] = viewVisibleRegion;
605 
606         // Aggregate the above to obtain the bounds where the content copy will be restricted.
607         int resolvedLeft = Integer.MIN_VALUE;
608         for (int i = mLeftContentBound; i >= 0; --i) {
609             resolvedLeft = Math.max(resolvedLeft, bounds[i].left);
610         }
611         int resolvedTop = Integer.MIN_VALUE;
612         for (int i = mTopContentBound; i >= 0; --i) {
613             resolvedTop = Math.max(resolvedTop, bounds[i].top);
614         }
615         int resolvedRight = Integer.MAX_VALUE;
616         for (int i = mRightContentBound; i >= 0; --i) {
617             resolvedRight = Math.min(resolvedRight, bounds[i].right);
618         }
619         int resolvedBottom = Integer.MAX_VALUE;
620         for (int i = mBottomContentBound; i >= 0; --i) {
621             resolvedBottom = Math.min(resolvedBottom, bounds[i].bottom);
622         }
623         // Adjust <left-right> and <top-bottom> pairs of bounds to make sense.
624         resolvedLeft = Math.min(resolvedLeft, mContentCopySurface.mWidth - mSourceWidth);
625         resolvedTop = Math.min(resolvedTop, mContentCopySurface.mHeight - mSourceHeight);
626         if (resolvedLeft < 0 || resolvedTop < 0) {
627             Log.e(TAG, "Magnifier's content is copied from a surface smaller than"
628                     + "the content requested size. The magnifier will be dismissed.");
629         }
630         resolvedRight = Math.max(resolvedRight, resolvedLeft + mSourceWidth);
631         resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight);
632 
633         // Finally compute the coordinates of the source center.
634         mClampedCenterZoomCoords.x = Math.max(resolvedLeft + mSourceWidth / 2, Math.min(
635                 zoomCenterX, resolvedRight - mSourceWidth / 2));
636         mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min(
637                 zoomCenterY, resolvedBottom - mSourceHeight / 2));
638     }
639 
640     /**
641      * Computes the coordinates of the top left corner of the magnifier window.
642      * These are relative to the surface the magnifier window is attached to.
643      */
obtainWindowCoordinates(final float xWindowPos, final float yWindowPos)644     private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) {
645         final int windowCenterX;
646         final int windowCenterY;
647         if (mView instanceof SurfaceView) {
648             // No offset required if the backing Surface matches the size of the SurfaceView.
649             windowCenterX = Math.round(xWindowPos);
650             windowCenterY = Math.round(yWindowPos);
651         } else {
652             windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]);
653             windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]);
654         }
655 
656         mWindowCoords.x = windowCenterX - mWindowWidth / 2;
657         mWindowCoords.y = windowCenterY - mWindowHeight / 2;
658         if (mParentSurface != mContentCopySurface) {
659             mWindowCoords.x += mViewCoordinatesInSurface[0];
660             mWindowCoords.y += mViewCoordinatesInSurface[1];
661         }
662     }
663 
performPixelCopy(final int startXInSurface, final int startYInSurface, final boolean updateWindowPosition)664     private void performPixelCopy(final int startXInSurface, final int startYInSurface,
665             final boolean updateWindowPosition) {
666         if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
667             onPixelCopyFailed();
668             return;
669         }
670 
671         // Clamp window coordinates inside the parent surface, to avoid displaying
672         // the magnifier out of screen or overlapping with system insets.
673         final Point windowCoords = getCurrentClampedWindowCoordinates();
674 
675         // Perform the pixel copy.
676         mPixelCopyRequestRect.set(startXInSurface,
677                 startYInSurface,
678                 startXInSurface + mSourceWidth,
679                 startYInSurface + mSourceHeight);
680         final InternalPopupWindow currentWindowInstance = mWindow;
681         final Bitmap bitmap =
682                 Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ARGB_8888);
683         PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
684                 result -> {
685                     if (result != PixelCopy.SUCCESS) {
686                         onPixelCopyFailed();
687                         return;
688                     }
689                     synchronized (mLock) {
690                         if (mWindow != currentWindowInstance) {
691                             // The magnifier was dismissed (and maybe shown again) in the meantime.
692                             return;
693                         }
694                         if (updateWindowPosition) {
695                             // TODO: pull the position update outside #performPixelCopy
696                             mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
697                         }
698                         mWindow.updateContent(bitmap);
699                     }
700                 },
701                 sPixelCopyHandlerThread.getThreadHandler());
702         mPrevStartCoordsInSurface.x = startXInSurface;
703         mPrevStartCoordsInSurface.y = startYInSurface;
704         mDirtyState = false;
705     }
706 
onPixelCopyFailed()707     private void onPixelCopyFailed() {
708         Log.e(TAG, "Magnifier failed to copy content from the view Surface. It will be dismissed.");
709         // Post to make sure #dismiss is done on the main thread.
710         Handler.getMain().postAtFrontOfQueue(() -> {
711             dismiss();
712             if (mCallback != null) {
713                 mCallback.onOperationComplete();
714             }
715         });
716     }
717 
718     /**
719      * Clamp window coordinates inside the surface the magnifier is attached to, to avoid
720      * displaying the magnifier out of screen or overlapping with system insets.
721      * @return the current window coordinates, after they are clamped inside the parent surface
722      */
getCurrentClampedWindowCoordinates()723     private Point getCurrentClampedWindowCoordinates() {
724         if (!mClippingEnabled) {
725             // No position adjustment should be done, so return the raw coordinates.
726             return new Point(mWindowCoords);
727         }
728 
729         final Rect windowBounds;
730         if (mParentSurface.mIsMainWindowSurface) {
731             final Insets systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
732             windowBounds = new Rect(
733                     systemInsets.left + mParentSurface.mInsets.left,
734                     systemInsets.top + mParentSurface.mInsets.top,
735                     mParentSurface.mWidth - systemInsets.right - mParentSurface.mInsets.right,
736                     mParentSurface.mHeight - systemInsets.bottom
737                             - mParentSurface.mInsets.bottom
738             );
739         } else {
740             windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
741         }
742         final int windowCoordsX = Math.max(windowBounds.left,
743                 Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
744         final int windowCoordsY = Math.max(windowBounds.top,
745                 Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
746         return new Point(windowCoordsX, windowCoordsY);
747     }
748 
749     /**
750      * Contains a surface and metadata corresponding to it.
751      */
752     private static class SurfaceInfo {
753         public static final SurfaceInfo NULL = new SurfaceInfo(null, null, 0, 0, null, false);
754 
755         private Surface mSurface;
756         private SurfaceControl mSurfaceControl;
757         private int mWidth;
758         private int mHeight;
759         private Rect mInsets;
760         private boolean mIsMainWindowSurface;
761 
SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface, final int width, final int height, final Rect insets, final boolean isMainWindowSurface)762         SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface,
763                 final int width, final int height, final Rect insets,
764                 final boolean isMainWindowSurface) {
765             mSurfaceControl = surfaceControl;
766             mSurface = surface;
767             mWidth = width;
768             mHeight = height;
769             mInsets = insets;
770             mIsMainWindowSurface = isMainWindowSurface;
771         }
772     }
773 
774     /**
775      * Magnifier's own implementation of PopupWindow-similar floating window.
776      * This exists to ensure frame-synchronization between window position updates and window
777      * content updates. By using a PopupWindow, these events would happen in different frames,
778      * producing a shakiness effect for the magnifier content.
779      */
780     private static class InternalPopupWindow {
781         // The z of the magnifier surface, defining its z order in the list of
782         // siblings having the same parent surface (usually the main app surface).
783         private static final int SURFACE_Z = 5;
784 
785         // Display associated to the view the magnifier is attached to.
786         private final Display mDisplay;
787         // The size of the content of the magnifier.
788         private final int mContentWidth;
789         private final int mContentHeight;
790         // The size of the allocated surface.
791         private final int mSurfaceWidth;
792         private final int mSurfaceHeight;
793         // The insets of the content inside the allocated surface.
794         private final int mOffsetX;
795         private final int mOffsetY;
796         // The overlay to be drawn on the top of the content.
797         private final Drawable mOverlay;
798         // The surface we allocate for the magnifier content + shadow.
799         private final SurfaceSession mSurfaceSession;
800         private final SurfaceControl mSurfaceControl;
801         private final Surface mSurface;
802         // The renderer used for the allocated surface.
803         private final ThreadedRenderer.SimpleRenderer mRenderer;
804         // The RenderNode used to draw the magnifier content in the surface.
805         private final RenderNode mBitmapRenderNode;
806         // The RenderNode used to draw the overlay over the magnifier content.
807         private final RenderNode mOverlayRenderNode;
808         // The job that will be post'd to apply the pending magnifier updates to the surface.
809         private final Runnable mMagnifierUpdater;
810         // The handler where the magnifier updater jobs will be post'd.
811         private final Handler mHandler;
812         // The callback to be run after the next draw.
813         private Callback mCallback;
814         // The position of the magnifier content when the last draw was requested.
815         private int mLastDrawContentPositionX;
816         private int mLastDrawContentPositionY;
817 
818         // Members below describe the state of the magnifier. Reads/writes to them
819         // have to be synchronized between the UI thread and the thread that handles
820         // the pixel copy results. This is the purpose of mLock.
821         private final Object mLock;
822         // Whether a magnifier frame draw is currently pending in the UI thread queue.
823         private boolean mFrameDrawScheduled;
824         // The content bitmap, as returned by pixel copy.
825         private Bitmap mBitmap;
826         // Whether the next draw will be the first one for the current instance.
827         private boolean mFirstDraw = true;
828         // The window position in the parent surface. Might be applied during the next draw,
829         // when mPendingWindowPositionUpdate is true.
830         private int mWindowPositionX;
831         private int mWindowPositionY;
832         private boolean mPendingWindowPositionUpdate;
833 
834         // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing.
835         private Bitmap mCurrentContent;
836 
InternalPopupWindow(final Context context, final Display display, final SurfaceControl parentSurfaceControl, final int width, final int height, final float elevation, final float cornerRadius, final Drawable overlay, final Handler handler, final Object lock, final Callback callback)837         InternalPopupWindow(final Context context, final Display display,
838                 final SurfaceControl parentSurfaceControl, final int width, final int height,
839                 final float elevation, final float cornerRadius, final Drawable overlay,
840                 final Handler handler, final Object lock, final Callback callback) {
841             mDisplay = display;
842             mOverlay = overlay;
843             mLock = lock;
844             mCallback = callback;
845 
846             mContentWidth = width;
847             mContentHeight = height;
848             mOffsetX = (int) (1.05f * elevation);
849             mOffsetY = (int) (1.05f * elevation);
850             // Setup the surface we will use for drawing the content and shadow.
851             mSurfaceWidth = mContentWidth + 2 * mOffsetX;
852             mSurfaceHeight = mContentHeight + 2 * mOffsetY;
853             mSurfaceSession = new SurfaceSession();
854             mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
855                     .setFormat(PixelFormat.TRANSLUCENT)
856                     .setBufferSize(mSurfaceWidth, mSurfaceHeight)
857                     .setName("magnifier surface")
858                     .setFlags(SurfaceControl.HIDDEN)
859                     .setParent(parentSurfaceControl)
860                     .build();
861             mSurface = new Surface();
862             mSurface.copyFrom(mSurfaceControl);
863 
864             // Setup the RenderNode tree. The root has two children, one containing the bitmap
865             // and one containing the overlay. We use a separate render node for the overlay
866             // to avoid drawing this as the same rate we do for content.
867             mRenderer = new ThreadedRenderer.SimpleRenderer(
868                     context,
869                     "magnifier renderer",
870                     mSurface
871             );
872             mBitmapRenderNode = createRenderNodeForBitmap(
873                     "magnifier content",
874                     elevation,
875                     cornerRadius
876             );
877             mOverlayRenderNode = createRenderNodeForOverlay(
878                     "magnifier overlay",
879                     cornerRadius
880             );
881             setupOverlay();
882 
883             final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height);
884             try {
885                 canvas.insertReorderBarrier();
886                 canvas.drawRenderNode(mBitmapRenderNode);
887                 canvas.insertInorderBarrier();
888                 canvas.drawRenderNode(mOverlayRenderNode);
889                 canvas.insertInorderBarrier();
890             } finally {
891                 mRenderer.getRootNode().endRecording();
892             }
893             if (mCallback != null) {
894                 mCurrentContent =
895                         Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888);
896                 updateCurrentContentForTesting();
897             }
898 
899             // Initialize the update job and the handler where this will be post'd.
900             mHandler = handler;
901             mMagnifierUpdater = this::doDraw;
902             mFrameDrawScheduled = false;
903         }
904 
createRenderNodeForBitmap(final String name, final float elevation, final float cornerRadius)905         private RenderNode createRenderNodeForBitmap(final String name,
906                 final float elevation, final float cornerRadius) {
907             final RenderNode bitmapRenderNode = RenderNode.create(name, null);
908 
909             // Define the position of the bitmap in the parent render node. The surface regions
910             // outside the bitmap are used to draw elevation.
911             bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
912                     mOffsetX + mContentWidth, mOffsetY + mContentHeight);
913             bitmapRenderNode.setElevation(elevation);
914 
915             final Outline outline = new Outline();
916             outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
917             outline.setAlpha(1.0f);
918             bitmapRenderNode.setOutline(outline);
919             bitmapRenderNode.setClipToOutline(true);
920 
921             // Create a placeholder draw, which will be replaced later with real drawing.
922             final RecordingCanvas canvas = bitmapRenderNode.beginRecording(
923                     mContentWidth, mContentHeight);
924             try {
925                 canvas.drawColor(0xFF00FF00);
926             } finally {
927                 bitmapRenderNode.endRecording();
928             }
929 
930             return bitmapRenderNode;
931         }
932 
createRenderNodeForOverlay(final String name, final float cornerRadius)933         private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) {
934             final RenderNode overlayRenderNode = RenderNode.create(name, null);
935 
936             // Define the position of the overlay in the parent render node.
937             // This coincides with the position of the content.
938             overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
939                     mOffsetX + mContentWidth, mOffsetY + mContentHeight);
940 
941             final Outline outline = new Outline();
942             outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
943             outline.setAlpha(1.0f);
944             overlayRenderNode.setOutline(outline);
945             overlayRenderNode.setClipToOutline(true);
946 
947             return overlayRenderNode;
948         }
949 
setupOverlay()950         private void setupOverlay() {
951             drawOverlay();
952 
953             mOverlay.setCallback(new Drawable.Callback() {
954                 @Override
955                 public void invalidateDrawable(Drawable who) {
956                     // When the overlay drawable is invalidated, redraw it to the render node.
957                     drawOverlay();
958                     if (mCallback != null) {
959                         updateCurrentContentForTesting();
960                     }
961                 }
962 
963                 @Override
964                 public void scheduleDrawable(Drawable who, Runnable what, long when) {
965                     Handler.getMain().postAtTime(what, who, when);
966                 }
967 
968                 @Override
969                 public void unscheduleDrawable(Drawable who, Runnable what) {
970                     Handler.getMain().removeCallbacks(what, who);
971                 }
972             });
973         }
974 
drawOverlay()975         private void drawOverlay() {
976             // Draw the drawable to the render node. This happens once during
977             // initialization and whenever the overlay drawable is invalidated.
978             final RecordingCanvas canvas =
979                     mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight);
980             try {
981                 mOverlay.setBounds(0, 0, mContentWidth, mContentHeight);
982                 mOverlay.draw(canvas);
983             } finally {
984                 mOverlayRenderNode.endRecording();
985             }
986         }
987 
988         /**
989          * Sets the position of the magnifier content relative to the parent surface.
990          * The position update will happen in the same frame with the next draw.
991          * The method has to be called in a context that holds {@link #mLock}.
992          *
993          * @param contentX the x coordinate of the content
994          * @param contentY the y coordinate of the content
995          */
setContentPositionForNextDraw(final int contentX, final int contentY)996         public void setContentPositionForNextDraw(final int contentX, final int contentY) {
997             mWindowPositionX = contentX - mOffsetX;
998             mWindowPositionY = contentY - mOffsetY;
999             mPendingWindowPositionUpdate = true;
1000             requestUpdate();
1001         }
1002 
1003         /**
1004          * Sets the content that should be displayed in the magnifier.
1005          * The update happens immediately, and possibly triggers a pending window movement set
1006          * by {@link #setContentPositionForNextDraw(int, int)}.
1007          * The method has to be called in a context that holds {@link #mLock}.
1008          *
1009          * @param bitmap the content bitmap
1010          */
updateContent(final @NonNull Bitmap bitmap)1011         public void updateContent(final @NonNull Bitmap bitmap) {
1012             if (mBitmap != null) {
1013                 mBitmap.recycle();
1014             }
1015             mBitmap = bitmap;
1016             requestUpdate();
1017         }
1018 
requestUpdate()1019         private void requestUpdate() {
1020             if (mFrameDrawScheduled) {
1021                 return;
1022             }
1023             final Message request = Message.obtain(mHandler, mMagnifierUpdater);
1024             request.setAsynchronous(true);
1025             request.sendToTarget();
1026             mFrameDrawScheduled = true;
1027         }
1028 
1029         /**
1030          * Destroys this instance. The method has to be called in a context holding {@link #mLock}.
1031          */
destroy()1032         public void destroy() {
1033             // Destroy the renderer. This will not proceed until pending frame callbacks complete.
1034             mRenderer.destroy();
1035             mSurface.destroy();
1036             new SurfaceControl.Transaction().remove(mSurfaceControl).apply();
1037             mSurfaceSession.kill();
1038             mHandler.removeCallbacks(mMagnifierUpdater);
1039             if (mBitmap != null) {
1040                 mBitmap.recycle();
1041             }
1042         }
1043 
doDraw()1044         private void doDraw() {
1045             final ThreadedRenderer.FrameDrawingCallback callback;
1046 
1047             // Draw the current bitmap to the surface, and prepare the callback which updates the
1048             // surface position. These have to be in the same synchronized block, in order to
1049             // guarantee the consistency between the bitmap content and the surface position.
1050             synchronized (mLock) {
1051                 if (!mSurface.isValid()) {
1052                     // Probably #destroy() was called for the current instance, so we skip the draw.
1053                     return;
1054                 }
1055 
1056                 final RecordingCanvas canvas =
1057                         mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight);
1058                 try {
1059                     final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1060                     final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
1061                     final Paint paint = new Paint();
1062                     paint.setFilterBitmap(true);
1063                     canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
1064                 } finally {
1065                     mBitmapRenderNode.endRecording();
1066                 }
1067 
1068                 if (mPendingWindowPositionUpdate || mFirstDraw) {
1069                     // If the window has to be shown or moved, defer this until the next draw.
1070                     final boolean firstDraw = mFirstDraw;
1071                     mFirstDraw = false;
1072                     final boolean updateWindowPosition = mPendingWindowPositionUpdate;
1073                     mPendingWindowPositionUpdate = false;
1074                     final int pendingX = mWindowPositionX;
1075                     final int pendingY = mWindowPositionY;
1076 
1077                     callback = frame -> {
1078                         if (!mSurface.isValid()) {
1079                             return;
1080                         }
1081                         // Show or move the window at the content draw frame.
1082                         SurfaceControl.openTransaction();
1083                         mSurfaceControl.deferTransactionUntil(mSurface, frame);
1084                         if (updateWindowPosition) {
1085                             mSurfaceControl.setPosition(pendingX, pendingY);
1086                         }
1087                         if (firstDraw) {
1088                             mSurfaceControl.setLayer(SURFACE_Z);
1089                             mSurfaceControl.show();
1090                         }
1091                         SurfaceControl.closeTransaction();
1092                     };
1093                     mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
1094                 } else {
1095                     callback = null;
1096                 }
1097 
1098                 mLastDrawContentPositionX = mWindowPositionX + mOffsetX;
1099                 mLastDrawContentPositionY = mWindowPositionY + mOffsetY;
1100                 mFrameDrawScheduled = false;
1101             }
1102 
1103             mRenderer.draw(callback);
1104             if (mCallback != null) {
1105                 // The current content bitmap is only used in testing, so, for performance,
1106                 // we only want to update it when running tests. For this, we check that
1107                 // mCallback is not null, as it can only be set from a @TestApi.
1108                 updateCurrentContentForTesting();
1109                 mCallback.onOperationComplete();
1110             }
1111         }
1112 
1113         /**
1114          * Updates mCurrentContent, which reproduces what is currently supposed to be
1115          * drawn in the magnifier. mCurrentContent is only used for testing, so this method
1116          * should only be called otherwise.
1117          */
updateCurrentContentForTesting()1118         private void updateCurrentContentForTesting() {
1119             final Canvas canvas = new Canvas(mCurrentContent);
1120             final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight);
1121             if (mBitmap != null && !mBitmap.isRecycled()) {
1122                 final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1123                 canvas.drawBitmap(mBitmap, originalBounds, bounds, null);
1124             }
1125             mOverlay.setBounds(bounds);
1126             mOverlay.draw(canvas);
1127         }
1128     }
1129 
1130     /**
1131      * Builder class for {@link Magnifier} objects.
1132      */
1133     public static final class Builder {
1134         private @NonNull View mView;
1135         private @Px @IntRange(from = 0) int mWidth;
1136         private @Px @IntRange(from = 0) int mHeight;
1137         private float mZoom;
1138         private @FloatRange(from = 0f) float mElevation;
1139         private @FloatRange(from = 0f) float mCornerRadius;
1140         private @Nullable Drawable mOverlay;
1141         private int mHorizontalDefaultSourceToMagnifierOffset;
1142         private int mVerticalDefaultSourceToMagnifierOffset;
1143         private boolean mClippingEnabled;
1144         private @SourceBound int mLeftContentBound;
1145         private @SourceBound int mTopContentBound;
1146         private @SourceBound int mRightContentBound;
1147         private @SourceBound int  mBottomContentBound;
1148 
1149         /**
1150          * Construct a new builder for {@link Magnifier} objects.
1151          * @param view the view this magnifier is attached to
1152          */
Builder(@onNull View view)1153         public Builder(@NonNull View view) {
1154             mView = Preconditions.checkNotNull(view);
1155             applyDefaults();
1156         }
1157 
applyDefaults()1158         private void applyDefaults() {
1159             final Resources resources = mView.getContext().getResources();
1160             mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width);
1161             mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height);
1162             mElevation = resources.getDimension(R.dimen.default_magnifier_elevation);
1163             mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius);
1164             mZoom = resources.getFloat(R.dimen.default_magnifier_zoom);
1165             mHorizontalDefaultSourceToMagnifierOffset =
1166                     resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset);
1167             mVerticalDefaultSourceToMagnifierOffset =
1168                     resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset);
1169             mOverlay = new ColorDrawable(resources.getColor(
1170                     R.color.default_magnifier_color_overlay, null));
1171             mClippingEnabled = true;
1172             mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
1173             mTopContentBound = SOURCE_BOUND_MAX_VISIBLE;
1174             mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
1175             mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE;
1176         }
1177 
1178         /**
1179          * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
1180          * Note that the size of the content being magnified and copied to the magnifier
1181          * will be computed as (window width / zoom, window height / zoom).
1182          * @param width the window width to be set
1183          * @param height the window height to be set
1184          */
1185         @NonNull
setSize(@x @ntRangefrom = 0) int width, @Px @IntRange(from = 0) int height)1186         public Builder setSize(@Px @IntRange(from = 0) int width,
1187                 @Px @IntRange(from = 0) int height) {
1188             Preconditions.checkArgumentPositive(width, "Width should be positive");
1189             Preconditions.checkArgumentPositive(height, "Height should be positive");
1190             mWidth = width;
1191             mHeight = height;
1192             return this;
1193         }
1194 
1195         /**
1196          * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
1197          * A content of size (content_width, content_height) will be magnified to
1198          * (content_width * zoom, content_height * zoom), which will coincide with the size
1199          * of the magnifier. A zoom of 1 will translate to no magnification (the content will
1200          * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
1201          * Note that the zoom can also be changed after the instance is built, using the
1202          * {@link Magnifier#setZoom(float)} method.
1203          * @param zoom the zoom to be set
1204          */
1205         @NonNull
setInitialZoom(@loatRangefrom = 0f) float zoom)1206         public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) {
1207             Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
1208             mZoom = zoom;
1209             return this;
1210         }
1211 
1212         /**
1213          * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
1214          * @param elevation the elevation to be set
1215          */
1216         @NonNull
setElevation(@x @loatRangefrom = 0) float elevation)1217         public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
1218             Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
1219             mElevation = elevation;
1220             return this;
1221         }
1222 
1223         /**
1224          * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp.
1225          * @param cornerRadius the corner radius to be set
1226          */
1227         @NonNull
setCornerRadius(@x @loatRangefrom = 0) float cornerRadius)1228         public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
1229             Preconditions.checkArgumentNonNegative(cornerRadius,
1230                     "Corner radius should be non-negative");
1231             mCornerRadius = cornerRadius;
1232             return this;
1233         }
1234 
1235         /**
1236          * Sets an overlay that will be drawn on the top of the magnifier.
1237          * In general, the overlay should not be opaque, in order to let the magnified
1238          * content be partially visible in the magnifier. The default overlay is {@code null}
1239          * (no overlay). As an example, TextView applies a white {@link ColorDrawable}
1240          * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark
1241          * application regions. To disable the overlay, the parameter should be set
1242          * to {@code null}. If not null, the overlay will be automatically redrawn
1243          * when the drawable is invalidated. To achieve this, the magnifier will set a new
1244          * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable,
1245          * so keep in mind that any existing one set by the application will be lost.
1246          * @param overlay the overlay to be drawn on top
1247          */
1248         @NonNull
setOverlay(@ullable Drawable overlay)1249         public Builder setOverlay(@Nullable Drawable overlay) {
1250             mOverlay = overlay;
1251             return this;
1252         }
1253 
1254         /**
1255          * Sets an offset that should be added to the content source center to obtain
1256          * the position of the magnifier window, when the {@link #show(float, float)}
1257          * method is called. The offset is ignored when {@link #show(float, float, float, float)}
1258          * is used. The offset can be negative. It defaults to (0dp, 0dp).
1259          * @param horizontalOffset the horizontal component of the offset
1260          * @param verticalOffset the vertical component of the offset
1261          */
1262         @NonNull
setDefaultSourceToMagnifierOffset(@x int horizontalOffset, @Px int verticalOffset)1263         public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
1264                 @Px int verticalOffset) {
1265             mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
1266             mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
1267             return this;
1268         }
1269 
1270         /**
1271          * Defines the behavior of the magnifier when it is requested to position outside the
1272          * surface of the main application window. The default value is {@code true}, which means
1273          * that the position will be adjusted such that the magnifier will be fully within the
1274          * bounds of the main application window, while also avoiding any overlap with system insets
1275          * (such as the one corresponding to the status bar). If this flag is set to {@code false},
1276          * the area where the magnifier can be positioned will no longer be clipped, so the
1277          * magnifier will be able to extend outside the main application window boundaries (and also
1278          * overlap the system insets). This can be useful if you require a custom behavior, but it
1279          * should be handled with care, when passing coordinates to {@link #show(float, float)};
1280          * note that:
1281          * <ul>
1282          *   <li>in a multiwindow context, if the magnifier crosses the boundary between the two
1283          *   windows, it will not be able to show over the window of the other application</li>
1284          *   <li>if the magnifier overlaps the status bar, there is no guarantee about which one
1285          *   will be displayed on top. This should be handled with care.</li>
1286          * </ul>
1287          * @param clip whether the magnifier position will be adjusted
1288          */
1289         @NonNull
setClippingEnabled(boolean clip)1290         public Builder setClippingEnabled(boolean clip) {
1291             mClippingEnabled = clip;
1292             return this;
1293         }
1294 
1295         /**
1296          * Defines the bounds of the rectangle where the magnifier will be able to copy its content
1297          * from. The content will always be copied from the {@link Surface} of the main application
1298          * window unless the magnified view is a {@link SurfaceView}, in which case its backing
1299          * surface will be used. Each bound can have a different behavior, with the options being:
1300          * <ul>
1301          *   <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible
1302          *   while remaining in the visible region of the magnified view, as given by
1303          *   {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into
1304          *   account the case when the view is contained in a scrollable container, and the
1305          *   magnifier will refuse to copy content outside of the visible view region</li>
1306          *   <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much
1307          *   as possible while remaining inside the surface the content is copied from.</li>
1308          * </ul>
1309          * Note that if either of the first three options is used, the bound will be compared to
1310          * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used),
1311          * and the more restrictive one will be chosen. In other words, no attempt to copy content
1312          * from outside the surface will be permitted. If two opposite bounds are not well-behaved
1313          * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top
1314          * bounds will have priority and the others will be extended accordingly. If the pairs
1315          * obtained this way still remain out of bounds, the smallest possible offset will be added
1316          * to the pairs to bring them inside the surface bounds. If this is impossible
1317          * (i.e. the surface is too small for the size of the content we try to copy on either
1318          * dimension), an error will be logged and the magnifier content will look distorted.
1319          * The default values assumed by the builder for the source bounds are
1320          * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE},
1321          * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}.
1322          * @param left the left bound for content copy
1323          * @param top the top bound for content copy
1324          * @param right the right bound for content copy
1325          * @param bottom the bottom bound for content copy
1326          */
1327         @NonNull
setSourceBounds(@ourceBound int left, @SourceBound int top, @SourceBound int right, @SourceBound int bottom)1328         public Builder setSourceBounds(@SourceBound int left, @SourceBound int top,
1329                 @SourceBound int right, @SourceBound int bottom) {
1330             mLeftContentBound = left;
1331             mTopContentBound = top;
1332             mRightContentBound = right;
1333             mBottomContentBound = bottom;
1334             return this;
1335         }
1336 
1337         /**
1338          * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
1339          */
build()1340         public @NonNull Magnifier build() {
1341             return new Magnifier(this);
1342         }
1343     }
1344 
1345     /**
1346      * A source bound that will extend as much as possible, while remaining within the surface
1347      * the content is copied from.
1348      */
1349     public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0;
1350 
1351     /**
1352      * A source bound that will extend as much as possible, while remaining within the
1353      * visible region of the magnified view, as determined by
1354      * {@link View#getGlobalVisibleRect(Rect)}.
1355      */
1356     public static final int SOURCE_BOUND_MAX_VISIBLE = 1;
1357 
1358 
1359     /**
1360      * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed
1361      * to be copied from. For more details, see method
1362      * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)}
1363      *
1364      * @hide
1365      */
1366     @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE})
1367     @Retention(RetentionPolicy.SOURCE)
1368     public @interface SourceBound {}
1369 
1370     // The rest of the file consists of test APIs and methods relevant for tests.
1371 
1372     /**
1373      * See {@link #setOnOperationCompleteCallback(Callback)}.
1374      */
1375     @TestApi
1376     private Callback mCallback;
1377 
1378     /**
1379      * Sets a callback which will be invoked at the end of the next
1380      * {@link #show(float, float)} or {@link #update()} operation.
1381      *
1382      * @hide
1383      */
1384     @TestApi
setOnOperationCompleteCallback(final Callback callback)1385     public void setOnOperationCompleteCallback(final Callback callback) {
1386         mCallback = callback;
1387         if (mWindow != null) {
1388             mWindow.mCallback = callback;
1389         }
1390     }
1391 
1392     /**
1393      * @return the drawing being currently displayed in the magnifier, as bitmap
1394      *
1395      * @hide
1396      */
1397     @TestApi
getContent()1398     public @Nullable Bitmap getContent() {
1399         if (mWindow == null) {
1400             return null;
1401         }
1402         synchronized (mWindow.mLock) {
1403             return mWindow.mCurrentContent;
1404         }
1405     }
1406 
1407     /**
1408      * Returns a bitmap containing the content that was magnified and drew to the
1409      * magnifier, at its original size, without the overlay applied.
1410      * @return the content that is magnified, as bitmap
1411      *
1412      * @hide
1413      */
1414     @TestApi
getOriginalContent()1415     public @Nullable Bitmap getOriginalContent() {
1416         if (mWindow == null) {
1417             return null;
1418         }
1419         synchronized (mWindow.mLock) {
1420             return Bitmap.createBitmap(mWindow.mBitmap);
1421         }
1422     }
1423 
1424     /**
1425      * @return the size of the magnifier window in dp
1426      *
1427      * @hide
1428      */
1429     @TestApi
getMagnifierDefaultSize()1430     public static PointF getMagnifierDefaultSize() {
1431         final Resources resources = Resources.getSystem();
1432         final float density = resources.getDisplayMetrics().density;
1433         final PointF size = new PointF();
1434         size.x = resources.getDimension(R.dimen.default_magnifier_width) / density;
1435         size.y = resources.getDimension(R.dimen.default_magnifier_height) / density;
1436         return size;
1437     }
1438 
1439     /**
1440      * @hide
1441      */
1442     @TestApi
1443     public interface Callback {
1444         /**
1445          * Callback called after the drawing for a magnifier update has happened.
1446          */
onOperationComplete()1447         void onOperationComplete();
1448     }
1449 }
1450