diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoDisplay.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoDisplay.java | 528 |
1 files changed, 528 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoDisplay.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoDisplay.java new file mode 100644 index 0000000000..1fc34cb8bb --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoDisplay.java @@ -0,0 +1,528 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * vim: ts=4 sw=4 expandtab: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.view.Surface; +import android.view.SurfaceControl; +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import org.mozilla.gecko.util.ThreadUtils; + +/** + * Applications use a GeckoDisplay instance to provide {@link GeckoSession} with a {@link Surface} + * for displaying content. To ensure drawing only happens on a valid {@link Surface}, {@link + * GeckoSession} will only use the provided {@link Surface} after {@link + * #surfaceChanged(SurfaceInfo)} is called and before {@link #surfaceDestroyed()} returns. + */ +public class GeckoDisplay { + private final GeckoSession mSession; + + protected GeckoDisplay(final GeckoSession session) { + mSession = session; + } + + /** + * Interface that allows Gecko the request a new Surface from the application. An implementation + * of this should be set on the {@link GeckoDisplay.SurfaceInfo} object passed to {@link + * GeckoDisplay#surfaceChanged(SurfaceInfo)}, by using {@link + * GeckoDisplay.SurfaceInfo.Builder#newSurfaceProvider(NewSurfaceProvider)}. + */ + public interface NewSurfaceProvider { + /** + * Called by Gecko to request a new Surface from the application. + * + * <p>Occasionally the Surface provided to Gecko via {@link #surfaceChanged(SurfaceInfo)} is + * invalid and Gecko is unable to render in to it. This function will be called in such + * circumstances. It is the implementation's responsibility to ensure that {@link + * #surfaceChanged(SurfaceInfo)} gets called soon afterwards with a new Surface, allowing Gecko + * to resume rendering. + * + * <p>Failure to implement this function may result in Gecko either crashing or not rendering + * correctly should it encounter an invalid Surface. + */ + @UiThread + void requestNewSurface(); + } + + /** + * Wrapper class containing a Surface and associated information that the compositor should render + * in to. Should be constructed using {@link SurfaceInfo.Builder}. + */ + public static class SurfaceInfo { + /* package */ final @NonNull Surface mSurface; + /* package */ final @Nullable SurfaceControl mSurfaceControl; + /* package */ final @Nullable NewSurfaceProvider mNewSurfaceProvider; + /* package */ final int mLeft; + /* package */ final int mTop; + /* package */ final int mWidth; + /* package */ final int mHeight; + + private SurfaceInfo(final @NonNull Builder builder) { + mSurface = builder.mSurface; + mSurfaceControl = builder.mSurfaceControl; + mNewSurfaceProvider = builder.mNewSurfaceProvider; + mLeft = builder.mLeft; + mTop = builder.mTop; + mWidth = builder.mWidth; + mHeight = builder.mHeight; + } + + /** Helper class for constructing a {@link SurfaceInfo} object. */ + public static class Builder { + private Surface mSurface; + private SurfaceControl mSurfaceControl; + private NewSurfaceProvider mNewSurfaceProvider; + private int mLeft; + private int mTop; + private int mWidth; + private int mHeight; + + /** + * Creates a new Builder and sets the new Surface. + * + * @param surface The new Surface. + */ + public Builder(final @NonNull Surface surface) { + mSurface = surface; + } + + /** + * Sets the SurfaceControl associated with the new Surface's SurfaceView. + * + * <p>This must be called when rendering in to a {@link android.view.SurfaceView} on SDK level + * 29 or above. On earlier SDK levels, or when rendering in to something other than a + * SurfaceView, this call can be omitted or the value can be null. + * + * @param surfaceControl The SurfaceControl associated with the new Surface's SurfaceView, or + * null. + * @return The builder object + */ + @UiThread + public @NonNull Builder surfaceControl(final @Nullable SurfaceControl surfaceControl) { + mSurfaceControl = surfaceControl; + return this; + } + + /** + * Sets a NewSurfaceProvider from which Gecko can request a new Surface. + * + * <p>This allows Gecko to recover from situations where the current Surface is for whatever + * reason invalid and Gecko is unable to render in to it. Failure to set this field correctly + * may result in Gecko either crashing or not rendering correctly should it encounter an + * invalid Surface. + * + * @param newSurfaceProvider A NewSurfaceProvider from which Gecko can request a new Surface. + * @return The builder object + */ + @UiThread + public @NonNull Builder newSurfaceProvider( + final @Nullable NewSurfaceProvider newSurfaceProvider) { + mNewSurfaceProvider = newSurfaceProvider; + return this; + } + + /** + * Sets the new compositor origin offset. + * + * @param left The compositor origin offset in the X axis. Can not be negative. + * @param top The compositor origin offset in the Y axis. Can not be negative. + * @return The builder object + */ + @UiThread + public @NonNull Builder offset(final int left, final int top) { + mLeft = left; + mTop = top; + return this; + } + + /** + * Sets the new surface size. + * + * @param width New width of the Surface. Can not be negative. + * @param height New height of the Surface. Can not be negative. + * @return The builder object + */ + @UiThread + public @NonNull Builder size(final int width, final int height) { + mWidth = width; + mHeight = height; + return this; + } + + /** + * Builds the {@link SurfaceInfo} object with the specified properties. + * + * @return The SurfaceInfo object + */ + @UiThread + public @NonNull SurfaceInfo build() { + if ((mLeft < 0) || (mTop < 0)) { + throw new IllegalArgumentException("Left and Top offsets can not be negative."); + } + + return new SurfaceInfo(this); + } + } + } + + /** + * Sets a surface for the compositor render a surface. + * + * <p>Required call. The display's Surface has been created or changed. Must be called on the + * application main thread. GeckoSession may block this call to ensure the Surface is valid while + * resuming drawing. + * + * <p>If rendering in to a {@link android.view.SurfaceView} on SDK level 29 or above, please + * ensure that the SurfaceControl field of the {@link SurfaceInfo} object is set. + * + * @param surfaceInfo Information about the new Surface. + */ + @UiThread + public void surfaceChanged(@NonNull final SurfaceInfo surfaceInfo) { + ThreadUtils.assertOnUiThread(); + + if (mSession.getDisplay() == this) { + mSession.onSurfaceChanged(surfaceInfo); + } + } + + /** + * Removes the current surface registered with the compositor. + * + * <p>Required call. The display's Surface has been destroyed. Must be called on the application + * main thread. GeckoSession may block this call to ensure the Surface is valid while pausing + * drawing. + */ + @UiThread + public void surfaceDestroyed() { + ThreadUtils.assertOnUiThread(); + + if (mSession.getDisplay() == this) { + mSession.onSurfaceDestroyed(); + } + } + + /** + * Update the position of the surface on the screen. + * + * <p>Optional call. The display's coordinates on the screen has changed. Must be called on the + * application main thread. + * + * @param left The X coordinate of the display on the screen, in screen pixels. + * @param top The Y coordinate of the display on the screen, in screen pixels. + */ + @UiThread + public void screenOriginChanged(final int left, final int top) { + ThreadUtils.assertOnUiThread(); + + if (mSession.getDisplay() == this) { + mSession.onScreenOriginChanged(left, top); + } + } + + /** + * Update the safe area insets of the surface on the screen. + * + * @param left left margin of safe area + * @param top top margin of safe area + * @param right right margin of safe area + * @param bottom bottom margin of safe area + */ + @UiThread + public void safeAreaInsetsChanged( + final int top, final int right, final int bottom, final int left) { + ThreadUtils.assertOnUiThread(); + + if (mSession.getDisplay() == this) { + mSession.onSafeAreaInsetsChanged(top, right, bottom, left); + } + } + + /** + * Set the maximum height of the dynamic toolbar(s). + * + * <p>If the toolbar is dynamic, this function needs to be called with the maximum possible + * toolbar height so that Gecko can make the ICB static even during the dynamic toolbar height is + * being changed. + * + * @param height The maximum height of the dynamic toolbar(s). + */ + @UiThread + public void setDynamicToolbarMaxHeight(final int height) { + ThreadUtils.assertOnUiThread(); + + if (mSession != null) { + mSession.setDynamicToolbarMaxHeight(height); + } + } + + /** + * Update the amount of vertical space that is clipped or visibly obscured in the bottom portion + * of the display. Tells gecko where to put bottom fixed elements so they are fully visible. + * + * <p>Optional call. The display's visible vertical space has changed. Must be called on the + * application main thread. + * + * @param clippingHeight The height of the bottom clipped space in screen pixels. + */ + @UiThread + public void setVerticalClipping(final int clippingHeight) { + ThreadUtils.assertOnUiThread(); + + if (mSession != null) { + mSession.setFixedBottomOffset(clippingHeight); + } + } + + /** + * Return whether the display should be pinned on the screen. + * + * <p>When pinned, the display should not be moved on the screen due to animation, scrolling, etc. + * A common reason for the display being pinned is when the user is dragging a selection caret + * inside the display; normal user interaction would be disrupted in that case if the display was + * moved on screen. + * + * @return True if display should be pinned on the screen. + */ + @UiThread + public boolean shouldPinOnScreen() { + ThreadUtils.assertOnUiThread(); + return mSession.getDisplay() == this && mSession.shouldPinOnScreen(); + } + + /** + * Request a {@link Bitmap} of the visible portion of the web page currently being rendered. + * + * <p>Returned {@link Bitmap} will have the same dimensions as the {@link Surface} the {@link + * GeckoDisplay} is currently using. + * + * <p>If the {@link GeckoSession#isCompositorReady} is false the {@link GeckoResult} will complete + * with an {@link IllegalStateException}. + * + * <p>This function must be called on the UI thread. + * + * @return A {@link GeckoResult} that completes with a {@link Bitmap} containing the pixels and + * size information of the currently visible rendered web page. + */ + @UiThread + public @NonNull GeckoResult<Bitmap> capturePixels() { + return screenshot().capture(); + } + + /** Builder to construct screenshot requests. */ + public static final class ScreenshotBuilder { + private static final int NONE = 0; + private static final int SCALE = 1; + private static final int ASPECT = 2; + private static final int FULL = 3; + private static final int RECYCLE = 4; + + private final GeckoSession mSession; + private int mOffsetX; + private int mOffsetY; + private int mSrcWidth; + private int mSrcHeight; + private int mOutWidth; + private int mOutHeight; + private int mAspectPreservingWidth; + private float mScale; + private Bitmap mRecycle; + private int mSizeType; + + /* package */ ScreenshotBuilder(final GeckoSession session) { + this.mSizeType = NONE; + this.mSession = session; + } + + /** + * The screenshot will be of a region instead of the entire screen + * + * @param x Left most pixel of the source region. + * @param y Top most pixel of the source region. + * @param width Width of the source region in screen pixels + * @param height Height of the source region in screen pixels + * @return The builder + */ + @AnyThread + public @NonNull ScreenshotBuilder source( + final int x, final int y, final int width, final int height) { + mOffsetX = x; + mOffsetY = y; + mSrcWidth = width; + mSrcHeight = height; + return this; + } + + /** + * The screenshot will be of a region instead of the entire screen + * + * @param source Region of the screen to capture in screen pixels + * @return The builder + */ + @AnyThread + public @NonNull ScreenshotBuilder source(final @NonNull Rect source) { + mOffsetX = source.left; + mOffsetY = source.top; + mSrcWidth = source.width(); + mSrcHeight = source.height(); + return this; + } + + private void checkAndSetSizeType(final int sizeType) { + if (mSizeType != NONE) { + throw new IllegalStateException("Size has already been set."); + } + mSizeType = sizeType; + } + + /** + * The width of the bitmap to create when taking the screenshot. The height will be calculated + * to match the aspect ratio of the source as closely as possible. The source screenshot will be + * scaled into the resulting Bitmap. + * + * @param width of the result Bitmap in screen pixels. + * @return The builder + * @throws IllegalStateException if the size has already been set in some other way. + */ + @AnyThread + public @NonNull ScreenshotBuilder aspectPreservingSize(final int width) { + checkAndSetSizeType(ASPECT); + mAspectPreservingWidth = width; + return this; + } + + /** + * The scale of the bitmap relative to the source. The height and width of the output bitmap + * will be within one pixel of this multiple of the source dimensions. The source screenshot + * will be scaled into the resulting Bitmap. + * + * @param scale of the result Bitmap relative to the source. + * @return The builder + * @throws IllegalStateException if the size has already been set in some other way. + */ + @AnyThread + public @NonNull ScreenshotBuilder scale(final float scale) { + checkAndSetSizeType(SCALE); + mScale = scale; + return this; + } + + /** + * Size of the bitmap to create when taking the screenshot. The source screenshot will be scaled + * into the resulting Bitmap + * + * @param width of the result Bitmap in screen pixels. + * @param height of the result Bitmap in screen pixels. + * @return The builder + * @throws IllegalStateException if the size has already been set in some other way. + */ + @AnyThread + public @NonNull ScreenshotBuilder size(final int width, final int height) { + checkAndSetSizeType(FULL); + mOutWidth = width; + mOutHeight = height; + return this; + } + + /** + * Instead of creating a new Bitmap for the result, the builder will use the passed Bitmap. + * + * @param bitmap The Bitmap to use in the result. + * @return The builder. + * @throws IllegalStateException if the size has already been set in some other way. + */ + @AnyThread + public @NonNull ScreenshotBuilder bitmap(final @Nullable Bitmap bitmap) { + checkAndSetSizeType(RECYCLE); + mRecycle = bitmap; + return this; + } + + /** + * Request a {@link Bitmap} of the requested portion of the web page currently being rendered + * using any parameters specified with the builder. + * + * <p>This function must be called on the UI thread. + * + * @return A {@link GeckoResult} that completes with a {@link Bitmap} containing the pixels and + * size information of the requested portion of the visible web page. + */ + @UiThread + public @NonNull GeckoResult<Bitmap> capture() { + ThreadUtils.assertOnUiThread(); + if (!mSession.isCompositorReady()) { + throw new IllegalStateException("Compositor must be ready before pixels can be captured"); + } + + final GeckoResult<Bitmap> result = new GeckoResult<>(); + final Bitmap target; + final Rect rect = new Rect(); + + if (mSrcWidth == 0 || mSrcHeight == 0) { + // Source is unset or invalid, use defaults. + mSession.getSurfaceBounds(rect); + mSrcWidth = rect.width(); + mSrcHeight = rect.height(); + } + + switch (mSizeType) { + case NONE: + mOutWidth = mSrcWidth; + mOutHeight = mSrcHeight; + break; + case SCALE: + mSession.getSurfaceBounds(rect); + mOutWidth = (int) (rect.width() * mScale); + mOutHeight = (int) (rect.height() * mScale); + break; + case ASPECT: + mSession.getSurfaceBounds(rect); + mOutWidth = mAspectPreservingWidth; + mOutHeight = (int) (rect.height() * (mAspectPreservingWidth / (double) rect.width())); + break; + case RECYCLE: + mOutWidth = mRecycle.getWidth(); + mOutHeight = mRecycle.getHeight(); + break; + // case FULL does not need to be handled, as width and height are already set. + } + + if (mRecycle == null) { + try { + target = Bitmap.createBitmap(mOutWidth, mOutHeight, Bitmap.Config.ARGB_8888); + } catch (final Throwable e) { + if (e instanceof NullPointerException || e instanceof OutOfMemoryError) { + return GeckoResult.fromException( + new OutOfMemoryError("Not enough memory to allocate for bitmap")); + } + return GeckoResult.fromException(new Throwable("Failed to create bitmap", e)); + } + } else { + target = mRecycle; + } + + mSession.mCompositor.requestScreenPixels( + result, target, mOffsetX, mOffsetY, mSrcWidth, mSrcHeight, mOutWidth, mOutHeight); + + return result; + } + } + + /** + * Creates a new screenshot builder. + * + * @return The new {@link ScreenshotBuilder} + */ + @UiThread + public @NonNull ScreenshotBuilder screenshot() { + return new ScreenshotBuilder(mSession); + } +} |