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