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