diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java | 7146 |
1 files changed, 7146 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java new file mode 100644 index 0000000000..6fc9abdc1c --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java @@ -0,0 +1,7146 @@ +/* -*- 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.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.IInterface; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; +import android.view.PointerIcon; +import android.view.Surface; +import android.view.View; +import android.view.ViewStructure; +import android.view.WindowManager; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.widget.Magnifier; +import androidx.annotation.AnyThread; +import androidx.annotation.IntDef; +import androidx.annotation.LongDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringDef; +import androidx.annotation.UiThread; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.AbstractSequentialList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.IGeckoEditableParent; +import org.mozilla.gecko.MagnifiableSurfaceView; +import org.mozilla.gecko.NativeQueue; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.mozglue.JNIObject; +import org.mozilla.gecko.util.BundleEventListener; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.GeckoBundle; +import org.mozilla.gecko.util.IntentUtils; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.geckoview.GeckoDisplay.SurfaceInfo; + +public class GeckoSession { + private static final String LOGTAG = "GeckoSession"; + private static final boolean DEBUG = false; + + // Type of changes given to onWindowChanged. + // Window has been cleared due to the session being closed. + private static final int WINDOW_CLOSE = 0; + // Window has been set due to the session being opened. + private static final int WINDOW_OPEN = 1; // Window has been opened. + // Window has been cleared due to the session being transferred to another session. + private static final int WINDOW_TRANSFER_OUT = 2; // Window has been transfer. + // Window has been set due to another session being transferred to this one. + private static final int WINDOW_TRANSFER_IN = 3; + + private static final int DATA_URI_MAX_LENGTH = 2 * 1024 * 1024; + + // Delay running compositor memory pressure by 10s to avoid interfering with tab switching. + private static final int NOTIFY_MEMORY_PRESSURE_DELAY_MS = 10 * 1000; + + private final Runnable mNotifyMemoryPressure = + new Runnable() { + @Override + public void run() { + if (mCompositorReady) { + mCompositor.notifyMemoryPressure(); + } + } + }; + + private enum State implements NativeQueue.State { + INITIAL(0), + READY(1); + + private final int mRank; + + private State(final int rank) { + mRank = rank; + } + + @Override + public boolean is(final NativeQueue.State other) { + return this == other; + } + + @Override + public boolean isAtLeast(final NativeQueue.State other) { + return (other instanceof State) && mRank >= ((State) other).mRank; + } + } + + private final NativeQueue mNativeQueue = new NativeQueue(State.INITIAL, State.READY); + + private final EventDispatcher mEventDispatcher = new EventDispatcher(mNativeQueue); + + private final SessionTextInput mTextInput = new SessionTextInput(this, mNativeQueue); + private SessionAccessibility mAccessibility; + private SessionFinder mFinder; + private SessionPdfFileSaver mPdfFileSaver; + + /** {@code SessionMagnifier} handles magnifying glass. */ + /* package */ interface SessionMagnifier { + /** + * Get the current {@link android.view.View} for magnifying glass. + * + * @return Current View for magnifying glass or null if not set. + */ + @UiThread + default @Nullable View getView() { + return null; + } + + /** + * Set the current {@link android.view.View} for magnifying glass. + * + * @param view View for magnifying glass or null to clear current View. + */ + @UiThread + default void setView(final @NonNull View view) {} + + /** + * Show magnifying glass. + * + * @param sourceCenter The source center of view that magnifying glass is attached + */ + @UiThread + default void show(final @NonNull PointF sourceCenter) {} + + /** Dismiss magnifying glass. */ + @UiThread + default void dismiss() {} + } + + @TargetApi(Build.VERSION_CODES.P) + private class SessionMagnifierP implements GeckoSession.SessionMagnifier { + private @Nullable View mView; + private @Nullable Magnifier mMagnifier; + private final @NonNull Compositor mCompositor; + + private SessionMagnifierP(final Compositor compositor) { + mCompositor = compositor; + } + + @Override + @UiThread + public @Nullable View getView() { + ThreadUtils.assertOnUiThread(); + + return mView; + } + + @Override + @UiThread + public void setView(final @NonNull View view) { + ThreadUtils.assertOnUiThread(); + + if (mMagnifier != null) { + mMagnifier.dismiss(); + mMagnifier = null; + } + mView = view; + } + + @Override + @UiThread + public void show(final @NonNull PointF sourceCenter) { + ThreadUtils.assertOnUiThread(); + + if (mView == null) { + return; + } + if (mMagnifier == null) { + mMagnifier = new Magnifier(mView); + } + + if (mView instanceof MagnifiableSurfaceView) { + final MagnifiableSurfaceView view = (MagnifiableSurfaceView) mView; + view.setMagnifierSurface(mCompositor.getMagnifiableSurface()); + } + mMagnifier.show(sourceCenter.x, sourceCenter.y); + if (mView instanceof MagnifiableSurfaceView) { + final MagnifiableSurfaceView view = (MagnifiableSurfaceView) mView; + view.setMagnifierSurface(null); + } + } + + @Override + @UiThread + public void dismiss() { + ThreadUtils.assertOnUiThread(); + + if (mMagnifier == null) { + return; + } + + mMagnifier.dismiss(); + mMagnifier = null; + } + } + + private SessionMagnifier mMagnifier; + + private String mId; + + /* package */ String getId() { + return mId; + } + + private boolean mShouldPinOnScreen; + + // All fields are accessed on UI thread only. + private PanZoomController mPanZoomController = new PanZoomController(this); + private OverscrollEdgeEffect mOverscroll; + private CompositorController mController; + private Autofill.Support mAutofillSupport; + + private boolean mAttachedCompositor; + private boolean mCompositorReady; + private SurfaceInfo mSurfaceInfo; + private GeckoDisplay.NewSurfaceProvider mNewSurfaceProvider; + + // All fields of coordinates are in screen units. + private int mLeft; + private int mTop; // Top of the surface (including toolbar); + private int mClientTop; // Top of the client area (i.e. excluding toolbar); + private int mWidth; + private int mHeight; // Height of the surface (including toolbar); + private int mClientHeight; // Height of the client area (i.e. excluding toolbar); + private int mFixedBottomOffset = + 0; // The margin for fixed elements attached to the bottom of the viewport. + private int mDynamicToolbarMaxHeight = 0; // The maximum height of the dynamic toolbar + private float mViewportLeft; + private float mViewportTop; + private float mViewportZoom = 1.0f; + + // + // NOTE: These values are also defined in + // gfx/layers/ipc/UiCompositorControllerMessageTypes.h and must be kept in sync. Any + // new AnimatorMessageType added here must also be added there. + // + // Sent from compositor after first paint + /* package */ static final int FIRST_PAINT = 0; + // Sent from compositor when a layer has been updated + /* package */ static final int LAYERS_UPDATED = 1; + // Special message sent from UiCompositorControllerChild once it is open + /* package */ static final int COMPOSITOR_CONTROLLER_OPEN = 2; + // Special message sent from controller to query if the compositor controller is open. + /* package */ static final int IS_COMPOSITOR_CONTROLLER_OPEN = 3; + + /* protected */ class Compositor extends JNIObject { + public boolean isReady() { + return GeckoSession.this.isCompositorReady(); + } + + @WrapForJNI(calledFrom = "ui") + private void onCompositorAttached() { + GeckoSession.this.onCompositorAttached(); + } + + @WrapForJNI(calledFrom = "ui") + private void onCompositorDetached() { + // Clear out any pending calls on the UI thread. + GeckoSession.this.onCompositorDetached(); + } + + @WrapForJNI(dispatchTo = "gecko") + @Override + protected native void disposeNative(); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") + public native void attachNPZC(PanZoomController.NativeProvider npzc); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") + public native void onBoundsChanged(int left, int top, int width, int height); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") + public native void setDynamicToolbarMaxHeight(int height); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") + public native void notifyMemoryPressure(); + + // Gecko thread pauses compositor; blocks UI thread. + @WrapForJNI(calledFrom = "ui", dispatchTo = "current") + public native void syncPauseCompositor(); + + // UI thread resumes compositor and notifies Gecko thread; does not block UI thread. + @WrapForJNI(calledFrom = "ui", dispatchTo = "current") + public native void syncResumeResizeCompositor( + int x, int y, int width, int height, Object surface, Object surfaceControl); + + // Returns a Surface that content has been rendered in to, which should be used when the + // magnifier is shown. This may differ from the Surface we have passed to + // syncResumeResizeCompositor(). + @WrapForJNI(calledFrom = "ui", dispatchTo = "current") + public native Surface getMagnifiableSurface(); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "current") + public native void setMaxToolbarHeight(int height); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") + public native void setFixedBottomOffset(int offset); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "current") + public native void sendToolbarAnimatorMessage(int message); + + @WrapForJNI(calledFrom = "ui") + private void recvToolbarAnimatorMessage(final int message) { + GeckoSession.this.handleCompositorMessage(message); + } + + @WrapForJNI(calledFrom = "ui") + private void requestNewSurface() { + final GeckoDisplay.NewSurfaceProvider provider = GeckoSession.this.mNewSurfaceProvider; + if (provider != null) { + provider.requestNewSurface(); + } else { + Log.w(LOGTAG, "Cannot request new Surface: No NewSurfaceProvider set."); + } + } + + @WrapForJNI(calledFrom = "ui", dispatchTo = "current") + public native void setDefaultClearColor(int color); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "current") + /* package */ native void requestScreenPixels( + final GeckoResult<Bitmap> result, + final Bitmap target, + final int x, + final int y, + final int srcWidth, + final int srcHeight, + final int outWidth, + final int outHeight); + + @WrapForJNI(calledFrom = "ui", dispatchTo = "current") + public native void enableLayerUpdateNotifications(boolean enable); + + // The compositor invokes this function just before compositing a frame where the + // document is different from the document composited on the last frame. In these + // cases, the viewport information we have in Java is no longer valid and needs to + // be replaced with the new viewport information provided. + @WrapForJNI(calledFrom = "ui") + private void updateRootFrameMetrics( + final float scrollX, final float scrollY, final float zoom) { + GeckoSession.this.onMetricsChanged(scrollX, scrollY, zoom); + } + + @WrapForJNI(calledFrom = "ui") + private void updateOverscrollVelocity(final float x, final float y) { + GeckoSession.this.updateOverscrollVelocity(x, y); + } + + @WrapForJNI(calledFrom = "ui") + private void updateOverscrollOffset(final float x, final float y) { + GeckoSession.this.updateOverscrollOffset(x, y); + } + + @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") + public native void onSafeAreaInsetsChanged(int top, int right, int bottom, int left); + + @WrapForJNI(calledFrom = "ui") + public void setPointerIcon( + final int defaultCursor, final Bitmap customCursor, final float x, final float y) { + GeckoSession.this.setPointerIcon(defaultCursor, customCursor, x, y); + } + + @Override + protected void finalize() throws Throwable { + disposeNative(); + } + } + + /* package */ final Compositor mCompositor = new Compositor(); + + @WrapForJNI(stubName = "GetCompositor", calledFrom = "ui") + private Object getCompositorFromNative() { + // Only used by native code. + return mCompositorReady ? mCompositor : null; + } + + private final GeckoSessionHandler<HistoryDelegate> mHistoryHandler = + new GeckoSessionHandler<HistoryDelegate>( + "GeckoViewHistory", + this, + new String[] { + "GeckoView:OnVisited", "GeckoView:GetVisited", "GeckoView:StateUpdated", + }) { + @Override + public void handleMessage( + final HistoryDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + if ("GeckoView:OnVisited".equals(event)) { + final GeckoResult<Boolean> result = + delegate.onVisited( + GeckoSession.this, + message.getString("url"), + message.getString("lastVisitedURL"), + message.getInt("flags")); + + if (result == null) { + callback.sendSuccess(false); + return; + } + + result.accept( + visited -> callback.sendSuccess(visited.booleanValue()), + exception -> callback.sendSuccess(false)); + } else if ("GeckoView:GetVisited".equals(event)) { + final String[] urls = message.getStringArray("urls"); + + final GeckoResult<boolean[]> result = delegate.getVisited(GeckoSession.this, urls); + + if (result == null) { + callback.sendSuccess(null); + return; + } + + result.accept( + visited -> callback.sendSuccess(visited), + exception -> callback.sendError("Failed to fetch visited statuses for URIs")); + } else if ("GeckoView:StateUpdated".equals(event)) { + + final GeckoBundle update = message.getBundle("data"); + + if (update == null) { + return; + } + final int previousHistorySize = mStateCache.size(); + mStateCache.updateSessionState(update); + + final ProgressDelegate progressDelegate = getProgressDelegate(); + if (progressDelegate != null) { + final SessionState state = new SessionState(mStateCache); + if (!state.isEmpty()) { + progressDelegate.onSessionStateChange(GeckoSession.this, state); + } + } + + if (update.getBundle("historychange") != null) { + final SessionState state = new SessionState(mStateCache); + + delegate.onHistoryStateChange(GeckoSession.this, state); + + // If the previous history was larger than one entry and the new size is one, it means + // the + // History has been purged and the navigation delegate needs to be update. + if ((previousHistorySize > 1) + && (state.size() == 1) + && mNavigationHandler.getDelegate() != null) { + mNavigationHandler.getDelegate().onCanGoForward(GeckoSession.this, false); + mNavigationHandler.getDelegate().onCanGoBack(GeckoSession.this, false); + } + } + } + } + }; + + private final WebExtension.SessionController mWebExtensionController; + + private final GeckoSessionHandler<ContentDelegate> mContentHandler = + new GeckoSessionHandler<ContentDelegate>( + "GeckoViewContent", + this, + new String[] { + "GeckoView:ContentCrash", + "GeckoView:ContentKill", + "GeckoView:ContextMenu", + "GeckoView:DOMMetaViewportFit", + "GeckoView:PageTitleChanged", + "GeckoView:DOMWindowClose", + "GeckoView:ExternalResponse", + "GeckoView:FocusRequest", + "GeckoView:FullScreenEnter", + "GeckoView:FullScreenExit", + "GeckoView:WebAppManifest", + "GeckoView:FirstContentfulPaint", + "GeckoView:PaintStatusReset", + "GeckoView:PreviewImage", + "GeckoView:CookieBannerEvent:Detected", + "GeckoView:CookieBannerEvent:Handled", + "GeckoView:SavePdf", + "GeckoView:GetNimbusFeature" + }) { + @Override + public void handleMessage( + final ContentDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + if ("GeckoView:ContentCrash".equals(event)) { + close(); + delegate.onCrash(GeckoSession.this); + } else if ("GeckoView:ContentKill".equals(event)) { + close(); + delegate.onKill(GeckoSession.this); + } else if ("GeckoView:ContextMenu".equals(event)) { + final ContentDelegate.ContextElement elem = + new ContentDelegate.ContextElement( + message.getString("baseUri"), + message.getString("uri"), + message.getString("title"), + message.getString("alt"), + message.getString("elementType"), + message.getString("elementSrc"), + message.getString("textContent")); + + delegate.onContextMenu( + GeckoSession.this, message.getInt("screenX"), message.getInt("screenY"), elem); + + } else if ("GeckoView:DOMMetaViewportFit".equals(event)) { + delegate.onMetaViewportFitChange(GeckoSession.this, message.getString("viewportfit")); + } else if ("GeckoView:PageTitleChanged".equals(event)) { + delegate.onTitleChange(GeckoSession.this, message.getString("title")); + } else if ("GeckoView:FocusRequest".equals(event)) { + delegate.onFocusRequest(GeckoSession.this); + } else if ("GeckoView:DOMWindowClose".equals(event)) { + delegate.onCloseRequest(GeckoSession.this); + } else if ("GeckoView:FullScreenEnter".equals(event)) { + delegate.onFullScreen(GeckoSession.this, true); + } else if ("GeckoView:FullScreenExit".equals(event)) { + delegate.onFullScreen(GeckoSession.this, false); + } else if ("GeckoView:WebAppManifest".equals(event)) { + final GeckoBundle manifest = message.getBundle("manifest"); + if (manifest == null) { + return; + } + + try { + delegate.onWebAppManifest( + GeckoSession.this, fixupWebAppManifest(manifest.toJSONObject())); + } catch (final JSONException e) { + Log.e(LOGTAG, "Failed to convert web app manifest to JSON", e); + } + } else if ("GeckoView:FirstContentfulPaint".equals(event)) { + delegate.onFirstContentfulPaint(GeckoSession.this); + } else if ("GeckoView:PaintStatusReset".equals(event)) { + delegate.onPaintStatusReset(GeckoSession.this); + } else if ("GeckoView:PreviewImage".equals(event)) { + delegate.onPreviewImage(GeckoSession.this, message.getString("previewImageUrl")); + } else if ("GeckoView:CookieBannerEvent:Detected".equals(event)) { + delegate.onCookieBannerDetected(GeckoSession.this); + } else if ("GeckoView:CookieBannerEvent:Handled".equals(event)) { + delegate.onCookieBannerHandled(GeckoSession.this); + } else if ("GeckoView:SavePdf".equals(event)) { + final GeckoResult<WebResponse> result = + SessionPdfFileSaver.createResponse( + GeckoSession.this, + message.getString("url"), + message.getString("filename"), + message.getString("originalUrl"), + message.getBoolean("skipConfirmation"), + message.getBoolean("requestExternalApp")); + if (result == null) { + callback.sendError("Failed to create response"); + return; + } + result.accept( + response -> + ThreadUtils.runOnUiThread( + () -> delegate.onExternalResponse(GeckoSession.this, response)), + exception -> callback.sendError("Failed to create response")); + } else if ("GeckoView:GetNimbusFeature".equals(event)) { + final String featureId = message.getString("featureId"); + final JSONObject res = delegate.onGetNimbusFeature(GeckoSession.this, featureId); + if (res == null) { + callback.sendError("No Nimbus data for the feature " + featureId); + return; + } + try { + callback.sendSuccess(GeckoBundle.fromJSONObject(res)); + } catch (final JSONException e) { + callback.sendError( + "No Nimbus data for the feature " + featureId + ": conversion failed."); + } + } + } + }; + + private final GeckoSessionHandler<NavigationDelegate> mNavigationHandler = + new GeckoSessionHandler<NavigationDelegate>( + "GeckoViewNavigation", + this, + new String[] {"GeckoView:LocationChange", "GeckoView:OnNewSession"}, + new String[] { + "GeckoView:OnLoadError", "GeckoView:OnLoadRequest", + }) { + // This needs to match nsIBrowserDOMWindow.idl + private int convertGeckoTarget(final int geckoTarget) { + switch (geckoTarget) { + case 0: // OPEN_DEFAULTWINDOW + case 1: // OPEN_CURRENTWINDOW + return NavigationDelegate.TARGET_WINDOW_CURRENT; + default: // OPEN_NEWWINDOW, OPEN_NEWTAB + return NavigationDelegate.TARGET_WINDOW_NEW; + } + } + + @Override + public void handleDefaultMessage( + final String event, final GeckoBundle message, final EventCallback callback) { + + if ("GeckoView:OnLoadRequest".equals(event)) { + callback.sendSuccess(false); + } else if ("GeckoView:OnLoadError".equals(event)) { + callback.sendSuccess(null); + } else { + super.handleDefaultMessage(event, message, callback); + } + } + + // For .isOpen(), the linter is not smart enough to figure out we're asserting that we're on + // the UI thread. + @SuppressLint("WrongThread") + @Override + public void handleMessage( + final NavigationDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + Log.d(LOGTAG, "handleMessage " + event + " uri=" + message.getString("uri")); + if ("GeckoView:LocationChange".equals(event)) { + if (message.getBoolean("isTopLevel")) { + final GeckoBundle[] perms = message.getBundleArray("permissions"); + final List<PermissionDelegate.ContentPermission> permList = + PermissionDelegate.ContentPermission.fromBundleArray(perms); + delegate.onLocationChange(GeckoSession.this, message.getString("uri"), permList); + } + delegate.onCanGoBack(GeckoSession.this, message.getBoolean("canGoBack")); + delegate.onCanGoForward(GeckoSession.this, message.getBoolean("canGoForward")); + } else if ("GeckoView:OnLoadRequest".equals(event)) { + final NavigationDelegate.LoadRequest request = + new NavigationDelegate.LoadRequest( + message.getString("uri"), + message.getString("triggerUri"), + message.getInt("where"), + message.getInt("flags"), + message.getBoolean("hasUserGesture"), + /* isDirectNavigation */ false); + + if (!IntentUtils.isUriSafeForScheme(request.uri)) { + callback.sendError("Blocked unsafe intent URI"); + + delegate.onLoadError( + GeckoSession.this, + request.uri, + new WebRequestError( + WebRequestError.ERROR_MALFORMED_URI, + WebRequestError.ERROR_CATEGORY_URI, + null)); + + return; + } + + final GeckoResult<AllowOrDeny> result = + delegate.onLoadRequest(GeckoSession.this, request); + + if (result == null) { + callback.sendSuccess(null); + return; + } + + callback.resolveTo( + result.map( + value -> { + ThreadUtils.assertOnUiThread(); + if (value == AllowOrDeny.ALLOW) { + return false; + } + if (value == AllowOrDeny.DENY) { + return true; + } + throw new IllegalArgumentException("Invalid response"); + })); + } else if ("GeckoView:OnLoadError".equals(event)) { + final String uri = message.getString("uri"); + final long errorCode = message.getLong("error"); + final int errorModule = message.getInt("errorModule"); + final int errorClass = message.getInt("errorClass"); + + final WebRequestError err = + WebRequestError.fromGeckoError(errorCode, errorModule, errorClass, null); + + final GeckoResult<String> result = delegate.onLoadError(GeckoSession.this, uri, err); + if (result == null) { + callback.sendError("abort"); + return; + } + + callback.resolveTo( + result.map( + url -> { + if (url == null) { + throw new IllegalArgumentException("abort"); + } + return url; + })); + } else if ("GeckoView:OnNewSession".equals(event)) { + final String uri = message.getString("uri"); + final GeckoResult<GeckoSession> result = delegate.onNewSession(GeckoSession.this, uri); + if (result == null) { + callback.sendSuccess(false); + return; + } + + final String newSessionId = message.getString("newSessionId"); + callback.resolveTo( + result.map( + session -> { + ThreadUtils.assertOnUiThread(); + if (session == null) { + return false; + } + + if (session.isOpen()) { + throw new AssertionError("Must use an unopened GeckoSession instance"); + } + + if (GeckoSession.this.mWindow == null) { + throw new IllegalArgumentException("Session is not attached to a window"); + } + + session.open(GeckoSession.this.mWindow.runtime, newSessionId); + return true; + })); + } + } + }; + + private final GeckoSessionHandler<PrintDelegate> mPrintHandler = + new GeckoSessionHandler<PrintDelegate>( + "GeckoViewPrint", this, new String[] {"GeckoView:DotPrintRequest"}) { + @Override + public void handleMessage( + final PrintDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + + if ("GeckoView:DotPrintRequest".equals(event)) { + final Long cbcId = message.getLong("canonicalBrowsingContextId"); + final GeckoResult<InputStream> pdfResult = saveAsPdfByBrowsingContext(cbcId); + final GeckoBundle bundle = new GeckoBundle(); + pdfResult + .accept( + pdfStream -> { + final GeckoResult<Boolean> dialogFinished = + delegate.onPrintWithStatus(pdfStream); + try { + dialogFinished + .accept( + isDialogFinished -> { + bundle.putBoolean("isPdfSuccessful", true); + mEventDispatcher.dispatch("GeckoView:DotPrintFinish", bundle); + }) + .exceptionally( + e -> { + bundle.putBoolean("isPdfSuccessful", false); + if (e instanceof GeckoPrintException) { + bundle.putInt("errorReason", ((GeckoPrintException) e).code); + } + mEventDispatcher.dispatch("GeckoView:DotPrintFinish", bundle); + return null; + }); + } catch (final Exception e) { + bundle.putBoolean("isPdfSuccessful", false); + mEventDispatcher.dispatch("GeckoView:DotPrintFinish", bundle); + Log.e(LOGTAG, "Print delegate needs to be fully implemented to print.", e); + } + }) + .exceptionally( + e -> { + bundle.putBoolean("isPdfSuccessful", false); + if (e instanceof GeckoPrintException) { + bundle.putInt("errorReason", ((GeckoPrintException) e).code); + } + mEventDispatcher.dispatch("GeckoView:DotPrintFinish", bundle); + Log.e(LOGTAG, "Could not complete DotPrintRequest.", e); + return null; + }); + } + } + }; + + private final GeckoSessionHandler<ContentDelegate> mProcessHangHandler = + new GeckoSessionHandler<ContentDelegate>( + "GeckoViewProcessHangMonitor", this, new String[] {"GeckoView:HangReport"}) { + + @Override + protected void handleMessage( + final ContentDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback eventCallback) { + Log.d(LOGTAG, "handleMessage " + event + " uri=" + message.getString("uri")); + + final GeckoResult<SlowScriptResponse> result = + delegate.onSlowScript(GeckoSession.this, message.getString("scriptFileName")); + if (result != null) { + final int mReportId = message.getInt("hangId"); + result.accept( + stopOrContinue -> { + if (stopOrContinue != null) { + final GeckoBundle bundle = new GeckoBundle(); + bundle.putInt("hangId", mReportId); + switch (stopOrContinue) { + case STOP: + mEventDispatcher.dispatch("GeckoView:HangReportStop", bundle); + break; + case CONTINUE: + mEventDispatcher.dispatch("GeckoView:HangReportWait", bundle); + break; + } + } + }); + } else { + // default to stopping the script + final GeckoBundle bundle = new GeckoBundle(); + bundle.putInt("hangId", message.getInt("hangId")); + mEventDispatcher.dispatch("GeckoView:HangReportStop", bundle); + } + } + }; + + private final GeckoSessionHandler<ProgressDelegate> mProgressHandler = + new GeckoSessionHandler<ProgressDelegate>( + "GeckoViewProgress", + this, + new String[] { + "GeckoView:PageStart", + "GeckoView:PageStop", + "GeckoView:ProgressChanged", + "GeckoView:SecurityChanged", + "GeckoView:StateUpdated", + }) { + @Override + public void handleMessage( + final ProgressDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + Log.d(LOGTAG, "handleMessage " + event + " uri=" + message.getString("uri")); + if ("GeckoView:PageStart".equals(event)) { + delegate.onPageStart(GeckoSession.this, message.getString("uri")); + } else if ("GeckoView:PageStop".equals(event)) { + delegate.onPageStop(GeckoSession.this, message.getBoolean("success")); + } else if ("GeckoView:ProgressChanged".equals(event)) { + delegate.onProgressChange(GeckoSession.this, message.getInt("progress")); + } else if ("GeckoView:SecurityChanged".equals(event)) { + final GeckoBundle identity = message.getBundle("identity"); + delegate.onSecurityChange( + GeckoSession.this, new ProgressDelegate.SecurityInformation(identity)); + } else if ("GeckoView:StateUpdated".equals(event)) { + final GeckoBundle update = message.getBundle("data"); + if (update != null) { + if (getHistoryDelegate() == null) { + mStateCache.updateSessionState(update); + final SessionState state = new SessionState(mStateCache); + if (!state.isEmpty()) { + delegate.onSessionStateChange(GeckoSession.this, state); + } + } + } + } + } + }; + + private final GeckoSessionHandler<ScrollDelegate> mScrollHandler = + new GeckoSessionHandler<ScrollDelegate>( + "GeckoViewScroll", this, new String[] {"GeckoView:ScrollChanged"}) { + @Override + public void handleMessage( + final ScrollDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + + if ("GeckoView:ScrollChanged".equals(event)) { + delegate.onScrollChanged( + GeckoSession.this, message.getInt("scrollX"), message.getInt("scrollY")); + } + } + }; + + private final GeckoSessionHandler<ContentBlocking.Delegate> mContentBlockingHandler = + new GeckoSessionHandler<ContentBlocking.Delegate>( + "GeckoViewContentBlocking", this, new String[] {"GeckoView:ContentBlockingEvent"}) { + @Override + public void handleMessage( + final ContentBlocking.Delegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + + if ("GeckoView:ContentBlockingEvent".equals(event)) { + final ContentBlocking.BlockEvent be = ContentBlocking.BlockEvent.fromBundle(message); + if (be.isBlocking()) { + delegate.onContentBlocked(GeckoSession.this, be); + } else { + delegate.onContentLoaded(GeckoSession.this, be); + } + } + } + }; + + private final GeckoSessionHandler<PermissionDelegate> mPermissionHandler = + new GeckoSessionHandler<PermissionDelegate>( + "GeckoViewPermission", + this, + new String[] { + "GeckoView:AndroidPermission", + "GeckoView:ContentPermission", + "GeckoView:MediaPermission" + }) { + @Override + public void handleMessage( + final PermissionDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + Log.d(LOGTAG, "handleMessage: " + event); + if (delegate == null) { + callback.sendSuccess(/* granted */ false); + return; + } + if ("GeckoView:AndroidPermission".equals(event)) { + delegate.onAndroidPermissionsRequest( + GeckoSession.this, + message.getStringArray("perms"), + new PermissionCallback("android", callback)); + } else if ("GeckoView:ContentPermission".equals(event)) { + final GeckoResult<Integer> res = + delegate.onContentPermissionRequest( + GeckoSession.this, new PermissionDelegate.ContentPermission(message)); + if (res == null) { + callback.sendSuccess(PermissionDelegate.ContentPermission.VALUE_PROMPT); + return; + } + + callback.resolveTo(res); + } else if ("GeckoView:MediaPermission".equals(event)) { + final GeckoBundle[] videoBundles = message.getBundleArray("video"); + final GeckoBundle[] audioBundles = message.getBundleArray("audio"); + PermissionDelegate.MediaSource[] videos = null; + PermissionDelegate.MediaSource[] audios = null; + + if (videoBundles != null) { + videos = new PermissionDelegate.MediaSource[videoBundles.length]; + for (int i = 0; i < videoBundles.length; i++) { + videos[i] = new PermissionDelegate.MediaSource(videoBundles[i]); + } + } + + if (audioBundles != null) { + audios = new PermissionDelegate.MediaSource[audioBundles.length]; + for (int i = 0; i < audioBundles.length; i++) { + audios[i] = new PermissionDelegate.MediaSource(audioBundles[i]); + } + } + + delegate.onMediaPermissionRequest( + GeckoSession.this, + message.getString("uri"), + videos, + audios, + new PermissionCallback("media", callback)); + } + } + }; + + private final GeckoSessionHandler<SelectionActionDelegate> mSelectionActionDelegate = + new GeckoSessionHandler<SelectionActionDelegate>( + "GeckoViewSelectionAction", + this, + new String[] { + "GeckoView:HideSelectionAction", + "GeckoView:ShowSelectionAction", + "GeckoView:HideMagnifier", + "GeckoView:ShowMagnifier", + "GeckoView:ClipboardPermissionRequest", + "GeckoView:DismissClipboardPermissionRequest", + }) { + @Override + public void handleMessage( + final SelectionActionDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + Log.d(LOGTAG, "handleMessage: " + event); + if ("GeckoView:ShowSelectionAction".equals(event)) { + final @SelectionActionDelegateAction HashSet<String> actionsSet = + new HashSet<>(Arrays.asList(message.getStringArray("actions"))); + final SelectionActionDelegate.Selection selection = + new SelectionActionDelegate.Selection(message, actionsSet, mEventDispatcher); + + delegate.onShowActionRequest(GeckoSession.this, selection); + + } else if ("GeckoView:HideSelectionAction".equals(event)) { + final String reasonString = message.getString("reason"); + final int reason; + if ("invisibleselection".equals(reasonString)) { + reason = SelectionActionDelegate.HIDE_REASON_INVISIBLE_SELECTION; + } else if ("presscaret".equals(reasonString)) { + reason = SelectionActionDelegate.HIDE_REASON_ACTIVE_SELECTION; + } else if ("scroll".equals(reasonString)) { + reason = SelectionActionDelegate.HIDE_REASON_ACTIVE_SCROLL; + } else if ("visibilitychange".equals(reasonString)) { + reason = SelectionActionDelegate.HIDE_REASON_NO_SELECTION; + } else { + throw new IllegalArgumentException(); + } + + delegate.onHideAction(GeckoSession.this, reason); + } else if ("GeckoView:ShowMagnifier".equals(event)) { + final PointF point = message.getPointF("screenPoint"); + if (point == null) { + throw new IllegalArgumentException("Invalid argument"); + } + + // Magnifier is surface coordinate. + point.x -= GeckoSession.this.mLeft; + point.y -= GeckoSession.this.mClientTop; + GeckoSession.this.getMagnifier().show(point); + } else if ("GeckoView:HideMagnifier".equals(event)) { + GeckoSession.this.getMagnifier().dismiss(); + } else if ("GeckoView:ClipboardPermissionRequest".equals(event)) { + final SelectionActionDelegate.ClipboardPermission permission = + new SelectionActionDelegate.ClipboardPermission(message); + + final GeckoResult<AllowOrDeny> result = + delegate.onShowClipboardPermissionRequest(GeckoSession.this, permission); + callback.resolveTo( + result.map( + value -> { + if (value == AllowOrDeny.ALLOW) { + return true; + } + if (value == AllowOrDeny.DENY) { + return false; + } + throw new IllegalArgumentException("Invalid response"); + })); + } else if ("GeckoView:DismissClipboardPermissionRequest".equals(event)) { + delegate.onDismissClipboardPermissionRequest(GeckoSession.this); + } + } + }; + + private final GeckoSessionHandler<MediaDelegate> mMediaHandler = + new GeckoSessionHandler<MediaDelegate>( + "GeckoViewMedia", + this, + new String[] { + "GeckoView:MediaRecordingStatusChanged", + }) { + @Override + public void handleMessage( + final MediaDelegate delegate, + final String event, + final GeckoBundle message, + final EventCallback callback) { + if ("GeckoView:MediaRecordingStatusChanged".equals(event)) { + final GeckoBundle[] deviceBundles = message.getBundleArray("devices"); + final MediaDelegate.RecordingDevice[] devices = + new MediaDelegate.RecordingDevice[deviceBundles.length]; + for (int i = 0; i < deviceBundles.length; i++) { + devices[i] = new MediaDelegate.RecordingDevice(deviceBundles[i]); + } + delegate.onRecordingStatusChanged(GeckoSession.this, devices); + return; + } + } + }; + + private final MediaSession.Handler mMediaSessionHandler = new MediaSession.Handler(this); + + /* package */ int handlersCount; + + private final GeckoSessionHandler<?>[] mSessionHandlers = + new GeckoSessionHandler<?>[] { + mContentHandler, + mHistoryHandler, + mMediaHandler, + mNavigationHandler, + mPermissionHandler, + mPrintHandler, + mProcessHangHandler, + mProgressHandler, + mScrollHandler, + mSelectionActionDelegate, + mContentBlockingHandler, + mMediaSessionHandler + }; + + private static class PermissionCallback + implements PermissionDelegate.Callback, PermissionDelegate.MediaCallback { + + private final String mType; + private EventCallback mCallback; + + public PermissionCallback(final String type, final EventCallback callback) { + mType = type; + mCallback = callback; + } + + private void submit(final Object response) { + if (mCallback != null) { + mCallback.sendSuccess(response); + mCallback = null; + } + } + + @Override // PermissionDelegate.Callback + public void grant() { + if ("media".equals(mType)) { + throw new UnsupportedOperationException(); + } + submit(/* response */ true); + } + + @Override // PermissionDelegate.Callback, PermissionDelegate.MediaCallback + public void reject() { + submit(/* response */ false); + } + + @Override // PermissionDelegate.MediaCallback + public void grant(final String video, final String audio) { + if (!"media".equals(mType)) { + throw new UnsupportedOperationException(); + } + final GeckoBundle response = new GeckoBundle(2); + response.putString("video", video); + response.putString("audio", audio); + submit(response); + } + + @Override // PermissionDelegate.MediaCallback + public void grant( + final PermissionDelegate.MediaSource video, final PermissionDelegate.MediaSource audio) { + grant(video != null ? video.id : null, audio != null ? audio.id : null); + } + } + + /** + * Get the current user agent string for this GeckoSession. + * + * @return a {@link GeckoResult} containing the UserAgent string + */ + @AnyThread + public @NonNull GeckoResult<String> getUserAgent() { + return mEventDispatcher.queryString("GeckoView:GetUserAgent"); + } + + /** + * Get the default user agent for this GeckoView build. + * + * <p>This method does not account for any override that might have been applied to the user agent + * string. + * + * @return the default user agent string + */ + @AnyThread + public static @NonNull String getDefaultUserAgent() { + return BuildConfig.USER_AGENT_GECKOVIEW_MOBILE; + } + + /** + * Get the current permission delegate for this GeckoSession. + * + * @return PermissionDelegate instance or null if using default delegate. + */ + @UiThread + public @Nullable PermissionDelegate getPermissionDelegate() { + ThreadUtils.assertOnUiThread(); + return mPermissionHandler.getDelegate(); + } + + /** + * Set the current permission delegate for this GeckoSession. + * + * @param delegate PermissionDelegate instance or null to use the default delegate. + */ + @UiThread + public void setPermissionDelegate(final @Nullable PermissionDelegate delegate) { + ThreadUtils.assertOnUiThread(); + mPermissionHandler.setDelegate(delegate, this); + } + + private PromptDelegate mPromptDelegate; + + private final Listener mListener = new Listener(); + + /* package */ static final class Window extends JNIObject implements IInterface { + public final GeckoRuntime runtime; + private WeakReference<GeckoSession> mOwner; + private NativeQueue mNativeQueue; + private Binder mBinder; + + public Window( + final @NonNull GeckoRuntime runtime, + final @NonNull GeckoSession owner, + final @NonNull NativeQueue nativeQueue) { + this.runtime = runtime; + mOwner = new WeakReference<>(owner); + mNativeQueue = nativeQueue; + } + + @Override // IInterface + public Binder asBinder() { + if (mBinder == null) { + mBinder = new Binder(); + mBinder.attachInterface(this, Window.class.getName()); + } + return mBinder; + } + + // Create a new Gecko window and assign an initial set of Java session objects to it. + @WrapForJNI(dispatchTo = "proxy") + public static native void open( + Window instance, + NativeQueue queue, + Compositor compositor, + EventDispatcher dispatcher, + SessionAccessibility.NativeProvider sessionAccessibility, + GeckoBundle initData, + String id, + String chromeUri, + boolean privateMode); + + @Override // JNIObject + public void disposeNative() { + if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { + nativeDisposeNative(); + } else { + GeckoThread.queueNativeCallUntil( + GeckoThread.State.PROFILE_READY, this, "nativeDisposeNative"); + } + } + + @WrapForJNI(dispatchTo = "proxy", stubName = "DisposeNative") + private native void nativeDisposeNative(); + + // Force the underlying Gecko window to close and release assigned Java objects. + public void close() { + // Reset our queue, so we don't end up with queued calls on a disposed object. + synchronized (this) { + if (mNativeQueue == null) { + // Already closed elsewhere. + return; + } + mNativeQueue.reset(State.INITIAL); + mNativeQueue = null; + mOwner = new WeakReference<>(null); + } + + // Detach ourselves from the binder as well, to prevent this window from being + // read from any parcels. + asBinder().attachInterface(null, Window.class.getName()); + + if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { + nativeClose(); + } else { + GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, this, "nativeClose"); + } + } + + @WrapForJNI(dispatchTo = "proxy", stubName = "Close") + private native void nativeClose(); + + @WrapForJNI(dispatchTo = "proxy", stubName = "Transfer") + private native void nativeTransfer( + NativeQueue queue, + Compositor compositor, + EventDispatcher dispatcher, + SessionAccessibility.NativeProvider sessionAccessibility, + GeckoBundle initData); + + @WrapForJNI(dispatchTo = "proxy") + public native void attachEditable(IGeckoEditableParent parent); + + @WrapForJNI(dispatchTo = "proxy") + public native void attachAccessibility( + SessionAccessibility.NativeProvider sessionAccessibility); + + @WrapForJNI(dispatchTo = "proxy") + public native void printToPdf(GeckoResult<InputStream> geckoResult); + + @WrapForJNI(dispatchTo = "proxy") + private native void printToPdf(GeckoResult<InputStream> geckoResult, long browserContextId); + + @WrapForJNI(calledFrom = "gecko") + private synchronized void onReady(final @Nullable NativeQueue queue) { + // onReady is called the first time the Gecko window is ready, with a null queue + // argument. In this case, we simply set the current queue to ready state. + // + // After the initial call, onReady is called again every time Window.transfer() + // is called, with a non-null queue argument. In this case, we only set the + // current queue to ready state _if_ the current queue matches the given queue, + // because if the queues don't match, we know there is another onReady call coming. + + if ((queue == null && mNativeQueue == null) || (queue != null && mNativeQueue != queue)) { + return; + } + + if (mNativeQueue.checkAndSetState(State.INITIAL, State.READY) && queue == null) { + Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() + " - chrome startup finished"); + } + } + + @Override + protected void finalize() throws Throwable { + close(); + disposeNative(); + } + + @WrapForJNI(calledFrom = "gecko") + private GeckoResult<Boolean> onLoadRequest( + final @NonNull String uri, + final int windowType, + final int flags, + final @Nullable String triggeringUri, + final boolean hasUserGesture, + final boolean isTopLevel) { + final ProfilerController profilerController = runtime.getProfilerController(); + final Double onLoadRequestProfilerStartTime = profilerController.getProfilerTime(); + final Runnable addMarker = + () -> + profilerController.addMarker( + "GeckoSession.onLoadRequest", onLoadRequestProfilerStartTime); + + final GeckoSession session = mOwner.get(); + if (session == null) { + // Don't handle any load request if we can't get the session for some reason. + return GeckoResult.fromValue(false); + } + final GeckoResult<Boolean> res = new GeckoResult<>(); + + ThreadUtils.postToUiThread( + new Runnable() { + @Override + public void run() { + final NavigationDelegate delegate = session.getNavigationDelegate(); + + if (delegate == null) { + res.complete(false); + addMarker.run(); + return; + } + + if (!IntentUtils.isUriSafeForScheme(uri)) { + delegate.onLoadError( + session, + uri, + new WebRequestError( + WebRequestError.ERROR_MALFORMED_URI, + WebRequestError.ERROR_CATEGORY_URI, + null)); + res.complete(true); + addMarker.run(); + return; + } + + final String trigger = TextUtils.isEmpty(triggeringUri) ? null : triggeringUri; + final NavigationDelegate.LoadRequest req = + new NavigationDelegate.LoadRequest( + uri, + trigger, + windowType, + flags, + hasUserGesture, + false /* isDirectNavigation */); + final GeckoResult<AllowOrDeny> reqResponse = + isTopLevel + ? delegate.onLoadRequest(session, req) + : delegate.onSubframeLoadRequest(session, req); + + if (reqResponse == null) { + res.complete(false); + addMarker.run(); + return; + } + + reqResponse.accept( + value -> { + if (value == AllowOrDeny.DENY) { + res.complete(true); + } else { + res.complete(false); + } + addMarker.run(); + }, + ex -> { + // This is incredibly ugly and unreadable because checkstyle sucks. + res.complete(false); + addMarker.run(); + }); + } + }); + + return res; + } + + @WrapForJNI(calledFrom = "ui") + private void passExternalWebResponse(final WebResponse response) { + final GeckoSession session = mOwner.get(); + if (session == null) { + return; + } + final ContentDelegate delegate = session.getContentDelegate(); + if (delegate != null) { + delegate.onExternalResponse(session, response); + } + } + + @WrapForJNI(calledFrom = "gecko") + private void onShowDynamicToolbar() { + final Window self = this; + ThreadUtils.runOnUiThread( + () -> { + final GeckoSession session = self.mOwner.get(); + if (session == null) { + return; + } + final ContentDelegate delegate = session.getContentDelegate(); + if (delegate != null) { + delegate.onShowDynamicToolbar(session); + } + }); + } + + @WrapForJNI(calledFrom = "gecko") + private void onUpdateSessionStore(final GeckoBundle aBundle) { + ThreadUtils.runOnUiThread( + () -> { + final GeckoSession session = mOwner.get(); + if (session == null) { + return; + } + GeckoBundle scroll = aBundle.getBundle("scroll"); + if (scroll == null) { + scroll = new GeckoBundle(); + aBundle.putBundle("scroll", scroll); + } + + // Here we unfortunately need to do some re-mapping since `zoom` is passed in a separate + // bunds and we wish to keep the bundle format. + scroll.putBundle("zoom", aBundle.getBundle("zoom")); + final SessionState stateCache = session.mStateCache; + stateCache.updateSessionState(aBundle); + final SessionState state = new SessionState(stateCache); + if (!state.isEmpty()) { + final ProgressDelegate progressDelegate = session.getProgressDelegate(); + if (progressDelegate != null) { + progressDelegate.onSessionStateChange(session, state); + } else { + } + } + }); + } + } + + private class Listener implements BundleEventListener { + /* package */ void registerListeners() { + getEventDispatcher() + .registerUiThreadListener( + this, + "GeckoView:PinOnScreen", + "GeckoView:Prompt", + "GeckoView:Prompt:Dismiss", + "GeckoView:Prompt:Update", + null); + } + + @Override + public void handleMessage( + final String event, final GeckoBundle message, final EventCallback callback) { + Log.d(LOGTAG, "handleMessage " + event); + + if ("GeckoView:PinOnScreen".equals(event)) { + GeckoSession.this.setShouldPinOnScreen(message.getBoolean("pinned")); + } else if ("GeckoView:Prompt".equals(event)) { + mPromptController.handleEvent(GeckoSession.this, message.getBundle("prompt"), callback); + } else if ("GeckoView:Prompt:Dismiss".equals(event)) { + mPromptController.dismissPrompt(message.getString("id")); + } else if ("GeckoView:Prompt:Update".equals(event)) { + mPromptController.updatePrompt(message.getBundle("prompt")); + } + } + } + + private final PromptController mPromptController; + + protected @Nullable Window mWindow; + private GeckoSessionSettings mSettings; + + @SuppressWarnings("checkstyle:javadocmethod") + public GeckoSession() { + this(null); + } + + @SuppressWarnings("checkstyle:javadocmethod") + public GeckoSession(final @Nullable GeckoSessionSettings settings) { + mSettings = new GeckoSessionSettings(settings, this); + mListener.registerListeners(); + + mWebExtensionController = new WebExtension.SessionController(this); + mPromptController = new PromptController(); + + mAutofillSupport = new Autofill.Support(this); + mAutofillSupport.registerListeners(); + + if (BuildConfig.DEBUG_BUILD && handlersCount != mSessionHandlers.length) { + throw new AssertionError("Add new handler to handlers list"); + } + } + + /* package */ @Nullable + GeckoRuntime getRuntime() { + if (mWindow == null) { + return null; + } + return mWindow.runtime; + } + + /* package */ synchronized void abandonWindow() { + if (mWindow == null) { + return; + } + + onWindowChanged(WINDOW_TRANSFER_OUT, /* inProgress */ true); + mWindow = null; + onWindowChanged(WINDOW_TRANSFER_OUT, /* inProgress */ false); + } + + /** + * Return whether this session is open. + * + * @return True if session is open. + * @see #open + * @see #close + */ + @UiThread + public boolean isOpen() { + ThreadUtils.assertOnUiThread(); + return mWindow != null; + } + + /* package */ boolean isReady() { + return mNativeQueue.isReady(); + } + + private GeckoBundle createInitData() { + final GeckoBundle initData = new GeckoBundle(2); + initData.putBundle("settings", mSettings.toBundle()); + + final GeckoBundle modules = new GeckoBundle(mSessionHandlers.length); + for (final GeckoSessionHandler<?> handler : mSessionHandlers) { + modules.putBoolean(handler.getName(), handler.isEnabled()); + } + initData.putBundle("modules", modules); + return initData; + } + + /** + * Opens the session. + * + * <p>Call this when you are ready to use a GeckoSession instance. + * + * <p>The session is in a 'closed' state when first created. Opening it creates the underlying + * Gecko objects necessary to load a page, etc. Most GeckoSession methods only take affect on an + * open session, and are queued until the session is opened here. Opening a session is an + * asynchronous operation. + * + * @param runtime The Gecko runtime to attach this session to. + * @see #close + * @see #isOpen + */ + @UiThread + public void open(final @NonNull GeckoRuntime runtime) { + open(runtime, UUID.randomUUID().toString().replace("-", "")); + } + + /* package */ void open(final @NonNull GeckoRuntime runtime, final String id) { + ThreadUtils.assertOnUiThread(); + + if (isOpen()) { + // We will leak the existing Window if we open another one. + throw new IllegalStateException("Session is open"); + } + + final String chromeUri = mSettings.getChromeUri(); + final boolean isPrivate = mSettings.getUsePrivateMode(); + + mId = id; + mWindow = new Window(runtime, this, mNativeQueue); + mWebExtensionController.setRuntime(runtime); + + onWindowChanged(WINDOW_OPEN, /* inProgress */ true); + + if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { + Window.open( + mWindow, + mNativeQueue, + mCompositor, + mEventDispatcher, + mAccessibility != null ? mAccessibility.nativeProvider : null, + createInitData(), + mId, + chromeUri, + isPrivate); + } else { + GeckoThread.queueNativeCallUntil( + GeckoThread.State.PROFILE_READY, + Window.class, + "open", + Window.class, + mWindow, + NativeQueue.class, + mNativeQueue, + Compositor.class, + mCompositor, + EventDispatcher.class, + mEventDispatcher, + SessionAccessibility.NativeProvider.class, + mAccessibility != null ? mAccessibility.nativeProvider : null, + GeckoBundle.class, + createInitData(), + String.class, + mId, + String.class, + chromeUri, + isPrivate); + } + + onWindowChanged(WINDOW_OPEN, /* inProgress */ false); + } + + /** + * Closes the session. + * + * <p>This frees the underlying Gecko objects and unloads the current page. The session may be + * reopened later, but page state is not restored. Call this when you are finished using a + * GeckoSession instance. + * + * @see #open + * @see #isOpen + */ + @UiThread + public void close() { + ThreadUtils.assertOnUiThread(); + + if (!isOpen()) { + Log.w(LOGTAG, "Attempted to close a GeckoSession that was already closed."); + return; + } + + onWindowChanged(WINDOW_CLOSE, /* inProgress */ true); + + // We need to ensure the compositor releases any Surface it currently holds. + onSurfaceDestroyed(); + + mWindow.close(); + mWindow.disposeNative(); + // Can't access the compositor after we dispose of the window + mCompositorReady = false; + mWindow = null; + + onWindowChanged(WINDOW_CLOSE, /* inProgress */ false); + } + + private void onWindowChanged(final int change, final boolean inProgress) { + if ((change == WINDOW_OPEN || change == WINDOW_TRANSFER_IN) && !inProgress) { + mTextInput.onWindowChanged(mWindow); + } + if ((change == WINDOW_CLOSE || change == WINDOW_TRANSFER_OUT) && !inProgress) { + getAutofillSupport().clear(); + } + } + + /** + * Get the SessionTextInput instance for this session. May be called on any thread. + * + * @return SessionTextInput instance. + */ + @AnyThread + public @NonNull SessionTextInput getTextInput() { + // May be called on any thread. + return mTextInput; + } + + /** + * Get the SessionAccessibility instance for this session. + * + * @return SessionAccessibility instance. + */ + @UiThread + public @NonNull SessionAccessibility getAccessibility() { + ThreadUtils.assertOnUiThread(); + if (mAccessibility != null) { + return mAccessibility; + } + + mAccessibility = new SessionAccessibility(this); + if (mWindow != null) { + if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { + mWindow.attachAccessibility(mAccessibility.nativeProvider); + } else { + GeckoThread.queueNativeCallUntil( + GeckoThread.State.PROFILE_READY, + mWindow, + "attachAccessibility", + SessionAccessibility.NativeProvider.class, + mAccessibility.nativeProvider); + } + } + return mAccessibility; + } + + /** + * Get the SessionMagnifier instance for this session. + * + * @return SessionMagnifier instance. + */ + @UiThread + /* package */ @NonNull + SessionMagnifier getMagnifier() { + ThreadUtils.assertOnUiThread(); + if (mMagnifier == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + mMagnifier = new SessionMagnifierP(mCompositor); + } else { + mMagnifier = new SessionMagnifier() {}; + } + } + + return mMagnifier; + } + + // The priority of the GeckoSession, either default or high. + @Retention(RetentionPolicy.SOURCE) + @IntDef({PRIORITY_DEFAULT, PRIORITY_HIGH}) + public @interface Priority {} + + /** Value for Priority when it is default. */ + public static final int PRIORITY_DEFAULT = 0; + + /** Value for Priority when it is high. */ + public static final int PRIORITY_HIGH = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + LOAD_FLAGS_NONE, + LOAD_FLAGS_BYPASS_CACHE, + LOAD_FLAGS_BYPASS_PROXY, + LOAD_FLAGS_EXTERNAL, + LOAD_FLAGS_ALLOW_POPUPS, + LOAD_FLAGS_FORCE_ALLOW_DATA_URI, + LOAD_FLAGS_REPLACE_HISTORY, + LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE, + }) + public @interface LoadFlags {} + + // These flags follow similarly named ones in Gecko's nsIWebNavigation.idl + // https://searchfox.org/mozilla-central/source/docshell/base/nsIWebNavigation.idl + // + // We do not use the same values directly in order to insulate ourselves from + // changes in Gecko. Instead, the flags are converted in GeckoViewNavigation.jsm. + + /** Default load flag, no special considerations. */ + public static final int LOAD_FLAGS_NONE = 0; + + /** Bypass the cache. */ + public static final int LOAD_FLAGS_BYPASS_CACHE = 1 << 0; + + /** Bypass the proxy, if one has been configured. */ + public static final int LOAD_FLAGS_BYPASS_PROXY = 1 << 1; + + /** The load is coming from an external app. Perform additional checks. */ + public static final int LOAD_FLAGS_EXTERNAL = 1 << 2; + + /** Popup blocking will be disabled for this load */ + public static final int LOAD_FLAGS_ALLOW_POPUPS = 1 << 3; + + /** Bypass the URI classifier (content blocking and Safe Browsing). */ + public static final int LOAD_FLAGS_BYPASS_CLASSIFIER = 1 << 4; + + /** + * Allows a top-level data: navigation to occur. E.g. view-image is an explicit user action which + * should be allowed. + */ + public static final int LOAD_FLAGS_FORCE_ALLOW_DATA_URI = 1 << 5; + + /** This flag specifies that any existing history entry should be replaced. */ + public static final int LOAD_FLAGS_REPLACE_HISTORY = 1 << 6; + + /** This load should bypass the NavigationDelegate.onLoadRequest. */ + public static final int LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE = 1 << 7; + + /** + * Filter headers according to the CORS safelisted rules. + * + * <p>See <a + * href="https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header"> + * CORS-safelisted request header </a>. + */ + public static final int HEADER_FILTER_CORS_SAFELISTED = 1; + + /** + * Allows most headers. + * + * <p>Note: the <code>Host</code> and <code>Connection</code> headers are still ignored. + * + * <p>This should only be used when input is hard-coded from the app or when properly sanitized, + * as some headers could cause unexpected consequences and security issues. + * + * <p>Only use this if you know what you're doing. + */ + public static final int HEADER_FILTER_UNRESTRICTED_UNSAFE = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {HEADER_FILTER_CORS_SAFELISTED, HEADER_FILTER_UNRESTRICTED_UNSAFE}) + public @interface HeaderFilter {} + + /** + * Main entry point for loading URIs into a {@link GeckoSession}. + * + * <p>The simplest use case is loading a URIs with no extra options, this can be accomplished by + * specifying the URI in {@link #uri} and then calling {@link #load}, e.g. + * + * <pre><code> + * session.load(new Loader().uri("http://mozilla.org")); + * </code></pre> + * + * This class can also be used to load <code>data:</code> URIs, either from a <code>byte[]</code> + * array or a <code>String</code> using {@link #data}, e.g. + * + * <pre><code> + * session.load(new Loader().data("the data:1234,5678", "text/plain")); + * </code></pre> + * + * This class also allows you to specify some extra data, e.g. you can set a referrer using {@link + * #referrer} which can either be a {@link GeckoSession} or a plain URL string. You can also + * specify some Load Flags using {@link #flags}. + * + * <p>The class is structured as a Builder, so method calls can be easily chained, e.g. + * + * <pre><code> + * session.load(new Loader() + * .url("http://mozilla.org") + * .referrer("http://my-referrer.com") + * .flags(...)); + * </code></pre> + */ + @AnyThread + public static class Loader { + private String mUri; + private GeckoSession mReferrerSession; + private String mReferrerUri; + private GeckoBundle mHeaders; + private @LoadFlags int mLoadFlags = LOAD_FLAGS_NONE; + private boolean mIsDataUri; + private @HeaderFilter int mHeaderFilter = HEADER_FILTER_CORS_SAFELISTED; + + private static @NonNull String createDataUri( + @NonNull final byte[] bytes, @Nullable final String mimeType) { + return String.format( + "data:%s;base64,%s", + mimeType != null ? mimeType : "", Base64.encodeToString(bytes, Base64.NO_WRAP)); + } + + private static @NonNull String createDataUri( + @NonNull final String data, @Nullable final String mimeType) { + return String.format("data:%s,%s", mimeType != null ? mimeType : "", data); + } + + @Override + public int hashCode() { + // Move to Objects.hashCode once our MIN_SDK >= 19 + return Arrays.hashCode( + new Object[] { + mUri, mReferrerSession, mReferrerUri, mHeaders, mLoadFlags, mIsDataUri, mHeaderFilter + }); + } + + private static boolean equals(final Object a, final Object b) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return Objects.equals(a, b); + } + + return (a == b) || (a != null && a.equals(b)); + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (!(obj instanceof Loader)) { + return false; + } + + final Loader other = (Loader) obj; + return equals(mUri, other.mUri) + && equals(mReferrerSession, other.mReferrerSession) + && equals(mReferrerUri, other.mReferrerUri) + && equals(mHeaders, other.mHeaders) + && equals(mLoadFlags, other.mLoadFlags) + && equals(mIsDataUri, other.mIsDataUri) + && equals(mHeaderFilter, other.mHeaderFilter); + } + + /** + * Set the URI of the resource to load. + * + * @param uri a String containg the URI + * @return this {@link Loader} instance. + */ + @NonNull + public Loader uri(final @NonNull String uri) { + mUri = uri; + mIsDataUri = false; + return this; + } + + /** + * Set the URI of the resource to load. + * + * @param uri a {@link Uri} instance + * @return this {@link Loader} instance. + */ + @NonNull + public Loader uri(final @NonNull Uri uri) { + mUri = uri.toString(); + mIsDataUri = false; + return this; + } + + /** + * Set the data URI of the resource to load. + * + * @param bytes a <code>byte</code> array containing the data to load. + * @param mimeType a <code>String</code> containing the mime type for this data URI, e.g. + * "text/plain" + * @return this {@link Loader} instance. + */ + @NonNull + public Loader data(final @NonNull byte[] bytes, final @Nullable String mimeType) { + mUri = createDataUri(bytes, mimeType); + mIsDataUri = true; + return this; + } + + /** + * Set the data URI of the resource to load. + * + * @param data a <code>String</code> array containing the data to load. + * @param mimeType a <code>String</code> containing the mime type for this data URI, e.g. + * "text/plain" + * @return this {@link Loader} instance. + */ + @NonNull + public Loader data(final @NonNull String data, final @Nullable String mimeType) { + mUri = createDataUri(data, mimeType); + mIsDataUri = true; + return this; + } + + /** + * Set the referrer for this load. + * + * @param referrer a <code>GeckoSession</code> that will be used as the referrer + * @return this {@link Loader} instance. + */ + @NonNull + public Loader referrer(final @NonNull GeckoSession referrer) { + mReferrerSession = referrer; + return this; + } + + /** + * Set the referrer for this load. + * + * @param referrerUri a {@link Uri} that will be used as the referrer + * @return this {@link Loader} instance. + */ + @NonNull + public Loader referrer(final @NonNull Uri referrerUri) { + mReferrerUri = referrerUri != null ? referrerUri.toString() : null; + return this; + } + + /** + * Set the referrer for this load. + * + * @param referrerUri a <code>String</code> containing the URI that will be used as the referrer + * @return this {@link Loader} instance. + */ + @NonNull + public Loader referrer(final @NonNull String referrerUri) { + mReferrerUri = referrerUri; + return this; + } + + /** + * Add headers for this load. + * + * <p>Note: only CORS safelisted headers are allowed by default. To modify this behavior use + * {@link #headerFilter}. + * + * <p>See <a + * href="https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header"> + * CORS-safelisted request header </a>. + * + * @param headers a <code>Map</code> containing headers that will be added to this load. + * @return this {@link Loader} instance. + */ + @NonNull + public Loader additionalHeaders(final @NonNull Map<String, String> headers) { + final GeckoBundle bundle = new GeckoBundle(headers.size()); + for (final Map.Entry<String, String> entry : headers.entrySet()) { + if (entry.getKey() == null) { + // Ignore null keys + continue; + } + bundle.putString(entry.getKey(), entry.getValue()); + } + mHeaders = bundle; + return this; + } + + /** + * Modify the header filter behavior. By default only CORS safelisted headers are allowed. + * + * @param filter one of the {@link GeckoSession#HEADER_FILTER_CORS_SAFELISTED HEADER_FILTER_*} + * constants. + * @return this {@link Loader} instance. + */ + @NonNull + public Loader headerFilter(final @HeaderFilter int filter) { + mHeaderFilter = filter; + return this; + } + + /** + * Set the load flags for this load. + * + * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*} + * that will be used as the referrer + * @return this {@link Loader} instance. + */ + @NonNull + public Loader flags(final @LoadFlags int flags) { + mLoadFlags = flags; + return this; + } + } + + /** + * Load page using the {@link Loader} specified. + * + * @param request Loader for this request. + * @see Loader + */ + @AnyThread + public void load(final @NonNull Loader request) { + if (request.mUri == null) { + throw new IllegalArgumentException( + "You need to specify at least one between `uri` and `data`."); + } + + if (request.mReferrerUri != null && request.mReferrerSession != null) { + throw new IllegalArgumentException( + "Cannot specify both a referrer session and a referrer URI."); + } + + final NavigationDelegate navDelegate = mNavigationHandler.getDelegate(); + final boolean isDataUriTooLong = !maybeCheckDataUriLength(request); + if (navDelegate == null && isDataUriTooLong) { + throw new IllegalArgumentException("data URI is too long"); + } + + final int loadFlags = + request.mIsDataUri + // If this is a data: load then we need to force allow it. + ? request.mLoadFlags | LOAD_FLAGS_FORCE_ALLOW_DATA_URI + : request.mLoadFlags; + + // For performance reasons we short-circuit the delegate here + // instead of making Gecko call it for direct load calls. + final NavigationDelegate.LoadRequest loadRequest = + new NavigationDelegate.LoadRequest( + request.mUri, + null, /* triggerUri */ + 1, /* geckoTarget: OPEN_CURRENTWINDOW */ + 0, /* flags */ + false, /* hasUserGesture */ + true /* isDirectNavigation */); + + shouldLoadUri(loadRequest, loadFlags) + .getOrAccept( + allowOrDeny -> { + if (allowOrDeny == AllowOrDeny.DENY) { + return; + } + + if (isDataUriTooLong) { + ThreadUtils.runOnUiThread( + () -> { + navDelegate.onLoadError( + this, + request.mUri, + new WebRequestError( + WebRequestError.ERROR_DATA_URI_TOO_LONG, + WebRequestError.ERROR_CATEGORY_URI, + null)); + }); + return; + } + + final GeckoBundle msg = new GeckoBundle(); + msg.putString("uri", request.mUri); + msg.putInt("flags", loadFlags); + msg.putInt("headerFilter", request.mHeaderFilter); + + if (request.mReferrerUri != null) { + msg.putString("referrerUri", request.mReferrerUri); + } + + if (request.mReferrerSession != null) { + msg.putString("referrerSessionId", request.mReferrerSession.mId); + } + + if (request.mHeaders != null) { + msg.putBundle("headers", request.mHeaders); + } + + mEventDispatcher.dispatch("GeckoView:LoadUri", msg); + }); + } + + /** + * Load the given URI. + * + * <p>Convenience method for + * + * <pre><code> + * session.load(new Loader().uri(uri)); + * </code></pre> + * + * @param uri The URI of the resource to load. + */ + @AnyThread + public void loadUri(final @NonNull String uri) { + load(new Loader().uri(uri)); + } + + private GeckoResult<AllowOrDeny> shouldLoadUri( + final NavigationDelegate.LoadRequest request, final int loadFlags) { + final NavigationDelegate delegate = mNavigationHandler.getDelegate(); + if (delegate == null || (loadFlags & LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE) != 0) { + return GeckoResult.allow(); + } + + // Always run the callback on the UI thread regardless of what thread we were called in. + final GeckoResult<AllowOrDeny> result = new GeckoResult<>(ThreadUtils.getUiHandler()); + + ThreadUtils.runOnUiThread( + () -> { + final GeckoResult<AllowOrDeny> delegateResult = delegate.onLoadRequest(this, request); + + if (delegateResult == null) { + result.complete(AllowOrDeny.ALLOW); + } else { + delegateResult.getOrAccept( + allowOrDeny -> result.complete(allowOrDeny), + error -> result.completeExceptionally(error)); + } + }); + + return result; + } + + /** Reload the current URI. */ + @AnyThread + public void reload() { + reload(LOAD_FLAGS_NONE); + } + + /** + * Reload the current URI. + * + * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*} + */ + @AnyThread + public void reload(final @LoadFlags int flags) { + final GeckoBundle msg = new GeckoBundle(); + msg.putInt("flags", flags); + mEventDispatcher.dispatch("GeckoView:Reload", msg); + } + + /** Stop loading. */ + @AnyThread + public void stop() { + mEventDispatcher.dispatch("GeckoView:Stop", null); + } + + /** + * Go back in history and assumes the call was based on a user interaction. + * + * @see #goBack(boolean) + */ + @AnyThread + public void goBack() { + goBack(true); + } + + /** + * Go back in history. + * + * @param userInteraction Whether the action was invoked by a user interaction. + */ + @AnyThread + public void goBack(final boolean userInteraction) { + final GeckoBundle msg = new GeckoBundle(1); + msg.putBoolean("userInteraction", userInteraction); + mEventDispatcher.dispatch("GeckoView:GoBack", msg); + } + + /** + * Go forward in history and assumes the call was based on a user interaction. + * + * @see #goForward(boolean) + */ + @AnyThread + public void goForward() { + goForward(true); + } + + /** + * Go forward in history. + * + * @param userInteraction Whether the action was invoked by a user interaction. + */ + @AnyThread + public void goForward(final boolean userInteraction) { + final GeckoBundle msg = new GeckoBundle(1); + msg.putBoolean("userInteraction", userInteraction); + mEventDispatcher.dispatch("GeckoView:GoForward", msg); + } + + /** + * Navigate to an index in browser history; the index of the currently viewed page can be + * retrieved from an up-to-date HistoryList by calling {@link + * HistoryDelegate.HistoryList#getCurrentIndex()}. + * + * @param index The index of the location in browser history you want to navigate to. + */ + @AnyThread + public void gotoHistoryIndex(final int index) { + final GeckoBundle msg = new GeckoBundle(1); + msg.putInt("index", index); + mEventDispatcher.dispatch("GeckoView:GotoHistoryIndex", msg); + } + + /** + * Returns a WebExtensionController for this GeckoSession. Delegates attached to this controller + * will receive events specific to this session. + * + * @return an instance of {@link WebExtension.SessionController}. + */ + @UiThread + public @NonNull WebExtension.SessionController getWebExtensionController() { + return mWebExtensionController; + } + + /** + * Purge history for the session. The session history is used for back and forward history. + * Purging the session history means {@link NavigationDelegate#onCanGoBack(GeckoSession, boolean)} + * and {@link NavigationDelegate#onCanGoForward(GeckoSession, boolean)} will be false. + */ + @AnyThread + public void purgeHistory() { + mEventDispatcher.dispatch("GeckoView:PurgeHistory", null); + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FINDER_FIND_BACKWARDS, + FINDER_FIND_LINKS_ONLY, + FINDER_FIND_MATCH_CASE, + FINDER_FIND_WHOLE_WORD + }) + public @interface FinderFindFlags {} + + /** Go backwards when finding the next match. */ + public static final int FINDER_FIND_BACKWARDS = 1; + + /** Perform case-sensitive match; default is to perform a case-insensitive match. */ + public static final int FINDER_FIND_MATCH_CASE = 1 << 1; + + /** Must match entire words; default is to allow matching partial words. */ + public static final int FINDER_FIND_WHOLE_WORD = 1 << 2; + + /** Limit matches to links on the page. */ + public static final int FINDER_FIND_LINKS_ONLY = 1 << 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FINDER_DISPLAY_HIGHLIGHT_ALL, + FINDER_DISPLAY_DIM_PAGE, + FINDER_DISPLAY_DRAW_LINK_OUTLINE + }) + public @interface FinderDisplayFlags {} + + /** Highlight all find-in-page matches. */ + public static final int FINDER_DISPLAY_HIGHLIGHT_ALL = 1; + + /** Dim the rest of the page when showing a find-in-page match. */ + public static final int FINDER_DISPLAY_DIM_PAGE = 1 << 1; + + /** Draw outlines around matching links. */ + public static final int FINDER_DISPLAY_DRAW_LINK_OUTLINE = 1 << 2; + + /** Represent the result of a find-in-page operation. */ + @AnyThread + public static class FinderResult { + /** Whether a match was found. */ + public final boolean found; + + /** Whether the search wrapped around the top or bottom of the page. */ + public final boolean wrapped; + + /** Ordinal number of the current match starting from 1, or 0 if no match. */ + public final int current; + + /** Total number of matches found so far, or -1 if unknown. */ + public final int total; + + /** Search string. */ + @NonNull public final String searchString; + + /** + * Flags used for the search; either 0 or a combination of {@link #FINDER_FIND_BACKWARDS + * FINDER_FIND_*} flags. + */ + @FinderFindFlags public final int flags; + + /** URI of the link, if the current match is a link, or null otherwise. */ + @Nullable public final String linkUri; + + /** Bounds of the current match in client coordinates, or null if unknown. */ + @Nullable public final RectF clientRect; + + /* package */ FinderResult(@NonNull final GeckoBundle bundle) { + found = bundle.getBoolean("found"); + wrapped = bundle.getBoolean("wrapped"); + current = bundle.getInt("current", 0); + total = bundle.getInt("total", -1); + searchString = bundle.getString("searchString"); + flags = SessionFinder.getFlagsFromBundle(bundle.getBundle("flags")); + linkUri = bundle.getString("linkURL"); + clientRect = bundle.getRectF("clientRect"); + } + + /** Empty constructor for tests */ + protected FinderResult() { + found = false; + wrapped = false; + current = 0; + total = 0; + flags = 0; + searchString = ""; + linkUri = ""; + clientRect = null; + } + } + + /** + * Get the SessionFinder instance for this session, to perform find-in-page operations. + * + * @return SessionFinder instance. + */ + @AnyThread + public @NonNull SessionFinder getFinder() { + if (mFinder == null) { + mFinder = new SessionFinder(getEventDispatcher()); + } + return mFinder; + } + + /** + * Checks whether we have a rule for this session. Uses the browsing context or any of its + * children, calls nsICookieBannerService.hasRuleForBrowsingContextTree + * + * @return {@link GeckoResult} with boolean + */ + @AnyThread + public @NonNull GeckoResult<Boolean> hasCookieBannerRuleForBrowsingContextTree() { + return mEventDispatcher.queryBoolean("GeckoView:HasCookieBannerRuleForBrowsingContextTree"); + } + + /** + * Get the SessionPdfFileSaver instance for this session, to save a pdf document. + * + * @return SessionPdfFileSaver instance. + */ + @AnyThread + public @NonNull SessionPdfFileSaver getPdfFileSaver() { + if (mPdfFileSaver == null) { + mPdfFileSaver = new SessionPdfFileSaver(this); + } + return mPdfFileSaver; + } + + /** Represent the result of a save-pdf operation. */ + @AnyThread + public static class PdfSaveResult { + /** Binary data representing a PDF. */ + @NonNull public final byte[] bytes; + + /** PDF file name. */ + @NonNull public final String filename; + + public final boolean isPrivate; + + /* package */ PdfSaveResult(@NonNull final GeckoBundle bundle) { + filename = bundle.getString("filename"); + isPrivate = bundle.getBoolean("isPrivate"); + bytes = bundle.getByteArray("bytes"); + } + + /** Empty constructor for tests */ + protected PdfSaveResult() { + filename = ""; + isPrivate = false; + bytes = new byte[0]; + } + } + + /** + * Check if the document being viewed is a pdf. + * + * @return Result of the check operation as a {@link GeckoResult} object. + */ + @AnyThread + public @NonNull GeckoResult<Boolean> isPdfJs() { + return mEventDispatcher.queryBoolean("GeckoView:IsPdfJs"); + } + + /** + * Set this GeckoSession as active or inactive, which represents if the session is currently + * visible or not. Setting a GeckoSession to inactive will significantly reduce its memory + * footprint, but should only be done if the GeckoSession is not currently visible. Note that a + * session can be active (i.e. visible) but not focused. When a session is set inactive, it will + * flush the session state and trigger a `ProgressDelegate.onSessionStateChange` callback. + * + * @param active A boolean determining whether the GeckoSession is active. + * @see #setFocused + */ + @AnyThread + public void setActive(final boolean active) { + final GeckoBundle msg = new GeckoBundle(1); + msg.putBoolean("active", active); + mEventDispatcher.dispatch("GeckoView:SetActive", msg); + + if (!active) { + mEventDispatcher.dispatch("GeckoView:FlushSessionState", null); + ThreadUtils.postToUiThreadDelayed(mNotifyMemoryPressure, NOTIFY_MEMORY_PRESSURE_DELAY_MS); + } else { + // Delete any pending memory pressure events since we're active again. + ThreadUtils.removeUiThreadCallbacks(mNotifyMemoryPressure); + } + + ThreadUtils.runOnUiThread(() -> getAutofillSupport().onActiveChanged(active)); + } + + /** + * Move focus to this session or away from this session. Only one session has focus at a given + * time. Note that a session can be unfocused but still active (i.e. visible). + * + * @param focused True if the session should gain focus or false if the session should lose focus. + * @see #setActive + */ + @AnyThread + public void setFocused(final boolean focused) { + final GeckoBundle msg = new GeckoBundle(1); + msg.putBoolean("focused", focused); + mEventDispatcher.dispatch("GeckoView:SetFocused", msg); + } + + /** + * Notify GeckoView of the priority for this GeckoSession. + * + * <p>Set this GeckoSession to high priority (PRIORITY_HIGH) whenever the app wants to signal to + * GeckoView that this GeckoSession is important to the app. GeckoView will keep the session state + * as long as possible. Set this to default priority (PRIORITY_DEFAULT) in any other case. + * + * @param priorityHint Priority of the geckosession, either high priority or default. + */ + @AnyThread + public void setPriorityHint(final @Priority int priorityHint) { + final GeckoBundle msg = new GeckoBundle(1); + msg.putInt("priorityHint", priorityHint); + mEventDispatcher.dispatch("GeckoView:SetPriorityHint", msg); + } + + /** Class representing a saved session state. */ + @AnyThread + public static class SessionState extends AbstractSequentialList<HistoryDelegate.HistoryItem> + implements HistoryDelegate.HistoryList, Parcelable { + private GeckoBundle mState; + + private class SessionStateItem implements HistoryDelegate.HistoryItem { + private final GeckoBundle mItem; + + private SessionStateItem(final @NonNull GeckoBundle item) { + mItem = item; + } + + @Override /* HistoryItem */ + public String getUri() { + return mItem.getString("url"); + } + + @Override /* HistoryItem */ + public String getTitle() { + return mItem.getString("title"); + } + } + + private class SessionStateIterator implements ListIterator<HistoryDelegate.HistoryItem> { + private final SessionState mState; + private int mIndex; + + private SessionStateIterator(final @NonNull SessionState state) { + this(state, 0); + } + + private SessionStateIterator(final @NonNull SessionState state, final int index) { + mIndex = index; + mState = state; + } + + @Override /* ListIterator */ + public void add(final HistoryDelegate.HistoryItem item) { + throw new UnsupportedOperationException(); + } + + @Override /* ListIterator */ + public boolean hasNext() { + final GeckoBundle[] entries = mState.getHistoryEntries(); + + if (entries == null) { + Log.w(LOGTAG, "No history entries found."); + return false; + } + + if (mIndex >= mState.getHistoryEntries().length) { + return false; + } + return true; + } + + @Override /* ListIterator */ + public boolean hasPrevious() { + if (mIndex <= 0) { + return false; + } + return true; + } + + @Override /* ListIterator */ + public HistoryDelegate.HistoryItem next() { + if (hasNext()) { + mIndex++; + return new SessionStateItem(mState.getHistoryEntries()[mIndex - 1]); + } else { + throw new NoSuchElementException(); + } + } + + @Override /* ListIterator */ + public int nextIndex() { + return mIndex; + } + + @Override /* ListIterator */ + public HistoryDelegate.HistoryItem previous() { + if (hasPrevious()) { + mIndex--; + return new SessionStateItem(mState.getHistoryEntries()[mIndex]); + } else { + throw new NoSuchElementException(); + } + } + + @Override /* ListIterator */ + public int previousIndex() { + return mIndex - 1; + } + + @Override /* ListIterator */ + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override /* ListIterator */ + public void set(final @NonNull HistoryDelegate.HistoryItem item) { + throw new UnsupportedOperationException(); + } + } + + private SessionState() { + mState = new GeckoBundle(3); + } + + private SessionState(final @NonNull GeckoBundle state) { + mState = new GeckoBundle(state); + } + + @SuppressWarnings("checkstyle:javadocmethod") + public SessionState(final @NonNull SessionState state) { + mState = new GeckoBundle(state.mState); + } + + /* package */ void updateSessionState(final @NonNull GeckoBundle updateData) { + if (updateData == null) { + Log.w(LOGTAG, "Session state update has no data field."); + return; + } + + final GeckoBundle history = updateData.getBundle("historychange"); + final GeckoBundle scroll = updateData.getBundle("scroll"); + final GeckoBundle formdata = updateData.getBundle("formdata"); + + if (history != null) { + mState.putBundle("history", history); + } + + if (scroll != null) { + mState.putBundle("scrolldata", scroll); + } + + if (formdata != null) { + mState.putBundle("formdata", formdata); + } + + return; + } + + @Override + public int hashCode() { + return mState.hashCode(); + } + + @Override + public boolean equals(final Object other) { + if (other == null || !(other instanceof SessionState)) { + return false; + } + + final SessionState otherState = (SessionState) other; + + return this.mState.equals(otherState.mState); + } + + /** + * Creates a new SessionState instance from a value previously returned by {@link #toString()}. + * + * @param value The serialized SessionState in String form. + * @return A new SessionState instance if input is valid; otherwise null. + */ + public static @Nullable SessionState fromString(final @Nullable String value) { + final GeckoBundle bundleState; + try { + bundleState = GeckoBundle.fromJSONObject(new JSONObject(value)); + } catch (final Exception e) { + Log.e(LOGTAG, "String does not represent valid session state."); + return null; + } + + if (bundleState == null) { + return null; + } + + return new SessionState(bundleState); + } + + @Override + public @Nullable String toString() { + if (mState == null) { + Log.w(LOGTAG, "Can't convert SessionState with null state to string"); + return null; + } + + String res; + try { + res = mState.toJSONObject().toString(); + } catch (final JSONException e) { + Log.e(LOGTAG, "Could not convert session state to string."); + res = null; + } + + return res; + } + + @Override // Parcelable + public int describeContents() { + return 0; + } + + @Override // Parcelable + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeString(toString()); + } + + // AIDL code may call readFromParcel even though it's not part of Parcelable. + @SuppressWarnings("checkstyle:javadocmethod") + public void readFromParcel(final @NonNull Parcel source) { + if (source.readString() == null) { + Log.w(LOGTAG, "Can't reproduce session state from Parcel"); + } + + try { + mState = GeckoBundle.fromJSONObject(new JSONObject(source.readString())); + } catch (final JSONException e) { + Log.e(LOGTAG, "Could not convert string to session state."); + mState = null; + } + } + + public static final Parcelable.Creator<SessionState> CREATOR = + new Parcelable.Creator<SessionState>() { + @Override + public SessionState createFromParcel(final Parcel source) { + if (source.readString() == null) { + Log.w(LOGTAG, "Can't create session state from Parcel"); + } + + GeckoBundle res; + try { + res = GeckoBundle.fromJSONObject(new JSONObject(source.readString())); + } catch (final JSONException e) { + Log.e(LOGTAG, "Could not convert parcel to session state."); + res = null; + } + + return new SessionState(res); + } + + @Override + public SessionState[] newArray(final int size) { + return new SessionState[size]; + } + }; + + @Override /* AbstractSequentialList */ + public @NonNull HistoryDelegate.HistoryItem get(final int index) { + final GeckoBundle[] entries = getHistoryEntries(); + + if (entries == null || index < 0 || index >= entries.length) { + throw new NoSuchElementException(); + } + + return new SessionStateItem(entries[index]); + } + + @Override /* AbstractSequentialList */ + public @NonNull Iterator<HistoryDelegate.HistoryItem> iterator() { + return listIterator(0); + } + + @Override /* AbstractSequentialList */ + public @NonNull ListIterator<HistoryDelegate.HistoryItem> listIterator(final int index) { + return new SessionStateIterator(this, index); + } + + @Override /* AbstractSequentialList */ + public int size() { + final GeckoBundle[] entries = getHistoryEntries(); + + if (entries == null) { + Log.w(LOGTAG, "No history entries found."); + return 0; + } + + return entries.length; + } + + @Override /* HistoryList */ + public int getCurrentIndex() { + final GeckoBundle history = getHistory(); + + if (history == null) { + throw new IllegalStateException("No history state exists."); + } + + return history.getInt("index") + history.getInt("fromIdx"); + } + + // Some helpers for common code. + private GeckoBundle getHistory() { + if (mState == null) { + return null; + } + + return mState.getBundle("history"); + } + + private GeckoBundle[] getHistoryEntries() { + final GeckoBundle history = getHistory(); + + if (history == null) { + return null; + } + + return history.getBundleArray("entries"); + } + } + + private SessionState mStateCache = new SessionState(); + + /** + * Restore a saved state to this GeckoSession; only data that is saved (history, scroll position, + * zoom, and form data) will be restored. These will overwrite the corresponding state of this + * GeckoSession. + * + * @param state A saved session state; this should originate from onSessionStateChange(). + */ + @AnyThread + public void restoreState(final @NonNull SessionState state) { + mEventDispatcher.dispatch("GeckoView:RestoreState", state.mState); + } + + /** + * Get whether this GeckoSession has form data. + * + * @return a {@link GeckoResult} result of if there is existing form data. + */ + @AnyThread + public @NonNull GeckoResult<Boolean> containsFormData() { + return mEventDispatcher.queryBoolean("GeckoView:ContainsFormData"); + } + + // This is the GeckoDisplay acquired via acquireDisplay(), if any. + private GeckoDisplay mDisplay; + + /* package */ interface Owner { + void onRelease(); + } + + private static final WeakReference<Owner> NO_OWNER = new WeakReference<>(null); + private WeakReference<Owner> mOwner = NO_OWNER; + + @UiThread + /* package */ void releaseOwner() { + ThreadUtils.assertOnUiThread(); + mOwner = NO_OWNER; + } + + @UiThread + /* package */ void setOwner(final Owner owner) { + ThreadUtils.assertOnUiThread(); + final Owner oldOwner = mOwner.get(); + if (oldOwner != null && owner != oldOwner) { + oldOwner.onRelease(); + } + mOwner = new WeakReference<>(owner); + } + + /* package */ GeckoDisplay getDisplay() { + return mDisplay; + } + + /** + * Acquire the GeckoDisplay instance for providing the session with a drawing Surface. Be sure to + * call {@link GeckoDisplay#surfaceChanged(SurfaceInfo)} on the acquired display if there is + * already a valid Surface. + * + * @return GeckoDisplay instance. + * @see #releaseDisplay(GeckoDisplay) + */ + @UiThread + public @NonNull GeckoDisplay acquireDisplay() { + ThreadUtils.assertOnUiThread(); + + if (mDisplay != null) { + throw new IllegalStateException("Display already acquired"); + } + + mDisplay = new GeckoDisplay(this); + return mDisplay; + } + + /** + * Release an acquired GeckoDisplay instance. Be sure to call {@link + * GeckoDisplay#surfaceDestroyed()} before releasing the display if it still has a valid Surface. + * + * @param display Acquired GeckoDisplay instance. + * @see #acquireDisplay() + */ + @UiThread + public void releaseDisplay(final @NonNull GeckoDisplay display) { + ThreadUtils.assertOnUiThread(); + + if (display != mDisplay) { + throw new IllegalArgumentException("Display not attached"); + } + + mDisplay = null; + } + + @AnyThread + @SuppressWarnings("checkstyle:javadocmethod") + public @NonNull GeckoSessionSettings getSettings() { + return mSettings; + } + + /** Exits fullscreen mode */ + @AnyThread + public void exitFullScreen() { + mEventDispatcher.dispatch("GeckoViewContent:ExitFullScreen", null); + } + + /** + * Set the content callback handler. This will replace the current handler. + * + * @param delegate An implementation of ContentDelegate. + */ + @UiThread + public void setContentDelegate(final @Nullable ContentDelegate delegate) { + ThreadUtils.assertOnUiThread(); + mContentHandler.setDelegate(delegate, this); + mProcessHangHandler.setDelegate(delegate, this); + } + + /** + * Get the content callback handler. + * + * @return The current content callback handler. + */ + @UiThread + public @Nullable ContentDelegate getContentDelegate() { + ThreadUtils.assertOnUiThread(); + return mContentHandler.getDelegate(); + } + + /** + * Set the progress callback handler. This will replace the current handler. + * + * @param delegate An implementation of ProgressDelegate. + */ + @UiThread + public void setProgressDelegate(final @Nullable ProgressDelegate delegate) { + ThreadUtils.assertOnUiThread(); + mProgressHandler.setDelegate(delegate, this); + } + + /** + * Get the progress callback handler. + * + * @return The current progress callback handler. + */ + @UiThread + public @Nullable ProgressDelegate getProgressDelegate() { + ThreadUtils.assertOnUiThread(); + return mProgressHandler.getDelegate(); + } + + /** + * Set the navigation callback handler. This will replace the current handler. + * + * @param delegate An implementation of NavigationDelegate. + */ + @UiThread + public void setNavigationDelegate(final @Nullable NavigationDelegate delegate) { + ThreadUtils.assertOnUiThread(); + mNavigationHandler.setDelegate(delegate, this); + } + + /** + * Get the navigation callback handler. + * + * @return The current navigation callback handler. + */ + @UiThread + public @Nullable NavigationDelegate getNavigationDelegate() { + ThreadUtils.assertOnUiThread(); + return mNavigationHandler.getDelegate(); + } + + /** + * Set the content scroll callback handler. This will replace the current handler. + * + * @param delegate An implementation of ScrollDelegate. + */ + @UiThread + public void setScrollDelegate(final @Nullable ScrollDelegate delegate) { + ThreadUtils.assertOnUiThread(); + mScrollHandler.setDelegate(delegate, this); + } + + @UiThread + @SuppressWarnings("checkstyle:javadocmethod") + public @Nullable ScrollDelegate getScrollDelegate() { + ThreadUtils.assertOnUiThread(); + return mScrollHandler.getDelegate(); + } + + /** + * Set the history tracking delegate for this session, replacing the current delegate if one is + * set. + * + * @param delegate The history tracking delegate, or {@code null} to unset. + */ + @AnyThread + public void setHistoryDelegate(final @Nullable HistoryDelegate delegate) { + mHistoryHandler.setDelegate(delegate, this); + } + + /** + * @return The history tracking delegate for this session. + */ + @AnyThread + public @Nullable HistoryDelegate getHistoryDelegate() { + return mHistoryHandler.getDelegate(); + } + + /** + * Set the content blocking callback handler. This will replace the current handler. + * + * @param delegate An implementation of {@link ContentBlocking.Delegate}. + */ + @AnyThread + public void setContentBlockingDelegate(final @Nullable ContentBlocking.Delegate delegate) { + mContentBlockingHandler.setDelegate(delegate, this); + } + + /** + * Get the content blocking callback handler. + * + * @return The current content blocking callback handler. + */ + @AnyThread + public @Nullable ContentBlocking.Delegate getContentBlockingDelegate() { + return mContentBlockingHandler.getDelegate(); + } + + /** + * Set the current prompt delegate for this GeckoSession. + * + * @param delegate PromptDelegate instance or null to use the built-in delegate. + */ + @AnyThread + public void setPromptDelegate(final @Nullable PromptDelegate delegate) { + mPromptDelegate = delegate; + } + + /** + * Get the current prompt delegate for this GeckoSession. + * + * @return PromptDelegate instance or null if using built-in delegate. + */ + @AnyThread + public @Nullable PromptDelegate getPromptDelegate() { + return mPromptDelegate; + } + + /** + * Set the current selection action delegate for this GeckoSession. + * + * @param delegate SelectionActionDelegate instance or null to unset. + */ + @UiThread + public void setSelectionActionDelegate(final @Nullable SelectionActionDelegate delegate) { + ThreadUtils.assertOnUiThread(); + + if (getSelectionActionDelegate() != null) { + // When the delegate is changed or cleared, make sure onHideAction is called + // one last time to hide any existing selection action UI. Gecko doesn't keep + // track of the old delegate, so we can't rely on Gecko to do that for us. + getSelectionActionDelegate() + .onHideAction(this, GeckoSession.SelectionActionDelegate.HIDE_REASON_NO_SELECTION); + } + mSelectionActionDelegate.setDelegate(delegate, this); + } + + /** + * Set the media callback handler. This will replace the current handler. + * + * @param delegate An implementation of MediaDelegate. + */ + @AnyThread + public void setMediaDelegate(final @Nullable MediaDelegate delegate) { + mMediaHandler.setDelegate(delegate, this); + } + + /** + * Get the Media callback handler. + * + * @return The current Media callback handler. + */ + @AnyThread + public @Nullable MediaDelegate getMediaDelegate() { + return mMediaHandler.getDelegate(); + } + + /** + * Set the media session delegate. This will replace the current handler. + * + * @param delegate An implementation of {@link MediaSession.Delegate}. + */ + @AnyThread + public void setMediaSessionDelegate(final @Nullable MediaSession.Delegate delegate) { + mMediaSessionHandler.setDelegate(delegate, this); + } + + /** + * Get the media session delegate. + * + * @return The current media session delegate. + */ + @AnyThread + public @Nullable MediaSession.Delegate getMediaSessionDelegate() { + return mMediaSessionHandler.getDelegate(); + } + + /** + * Get the current selection action delegate for this GeckoSession. + * + * @return SelectionActionDelegate instance or null if not set. + */ + @AnyThread + public @Nullable SelectionActionDelegate getSelectionActionDelegate() { + return mSelectionActionDelegate.getDelegate(); + } + + @UiThread + protected void setShouldPinOnScreen(final boolean pinned) { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + mShouldPinOnScreen = pinned; + } + + /* package */ boolean shouldPinOnScreen() { + ThreadUtils.assertOnUiThread(); + return mShouldPinOnScreen; + } + + @AnyThread + /* package */ @NonNull + EventDispatcher getEventDispatcher() { + return mEventDispatcher; + } + + public interface ProgressDelegate { + /** Class representing security information for a site. */ + public class SecurityInformation { + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURITY_MODE_UNKNOWN, SECURITY_MODE_IDENTIFIED, SECURITY_MODE_VERIFIED}) + public @interface SecurityMode {} + + public static final int SECURITY_MODE_UNKNOWN = 0; + public static final int SECURITY_MODE_IDENTIFIED = 1; + public static final int SECURITY_MODE_VERIFIED = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({CONTENT_UNKNOWN, CONTENT_BLOCKED, CONTENT_LOADED}) + public @interface ContentType {} + + public static final int CONTENT_UNKNOWN = 0; + public static final int CONTENT_BLOCKED = 1; + public static final int CONTENT_LOADED = 2; + + /** Indicates whether or not the site is secure. */ + public final boolean isSecure; + + /** Indicates whether or not the site is a security exception. */ + public final boolean isException; + + /** Contains the origin of the certificate. */ + public final @Nullable String origin; + + /** Contains the host associated with the certificate. */ + public final @NonNull String host; + + /** The server certificate in use, if any. */ + public final @Nullable X509Certificate certificate; + + /** + * Indicates the security level of the site; possible values are SECURITY_MODE_UNKNOWN, + * SECURITY_MODE_IDENTIFIED, and SECURITY_MODE_VERIFIED. SECURITY_MODE_IDENTIFIED indicates + * domain validation only, while SECURITY_MODE_VERIFIED indicates extended validation. + */ + public final @SecurityMode int securityMode; + + /** + * Indicates the presence of passive mixed content; possible values are CONTENT_UNKNOWN, + * CONTENT_BLOCKED, and CONTENT_LOADED. + */ + public final @ContentType int mixedModePassive; + + /** + * Indicates the presence of active mixed content; possible values are CONTENT_UNKNOWN, + * CONTENT_BLOCKED, and CONTENT_LOADED. + */ + public final @ContentType int mixedModeActive; + + /* package */ SecurityInformation(final GeckoBundle identityData) { + final GeckoBundle mode = identityData.getBundle("mode"); + + mixedModePassive = mode.getInt("mixed_display"); + mixedModeActive = mode.getInt("mixed_active"); + + securityMode = mode.getInt("identity"); + + isSecure = identityData.getBoolean("secure"); + isException = identityData.getBoolean("securityException"); + origin = identityData.getString("origin"); + host = identityData.getString("host"); + + X509Certificate decodedCert = null; + try { + final CertificateFactory factory = CertificateFactory.getInstance("X.509"); + final String certString = identityData.getString("certificate"); + if (certString != null) { + final byte[] certBytes = Base64.decode(certString, Base64.NO_WRAP); + decodedCert = + (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes)); + } + } catch (final CertificateException e) { + Log.e(LOGTAG, "Failed to decode certificate", e); + } + + certificate = decodedCert; + } + + /** Empty constructor for tests */ + protected SecurityInformation() { + mixedModePassive = CONTENT_UNKNOWN; + mixedModeActive = CONTENT_UNKNOWN; + securityMode = SECURITY_MODE_UNKNOWN; + isSecure = false; + isException = false; + origin = ""; + host = ""; + certificate = null; + } + } + + /** + * A View has started loading content from the network. + * + * @param session GeckoSession that initiated the callback. + * @param url The resource being loaded. + */ + @UiThread + default void onPageStart(@NonNull final GeckoSession session, @NonNull final String url) {} + + /** + * A View has finished loading content from the network. + * + * @param session GeckoSession that initiated the callback. + * @param success Whether the page loaded successfully or an error occurred. + */ + @UiThread + default void onPageStop(@NonNull final GeckoSession session, final boolean success) {} + + /** + * Page loading has progressed. + * + * @param session GeckoSession that initiated the callback. + * @param progress Current page load progress value [0, 100]. + */ + @UiThread + default void onProgressChange(@NonNull final GeckoSession session, final int progress) {} + + /** + * The security status has been updated. + * + * @param session GeckoSession that initiated the callback. + * @param securityInfo The new security information. + */ + @UiThread + default void onSecurityChange( + @NonNull final GeckoSession session, @NonNull final SecurityInformation securityInfo) {} + + /** + * The browser session state has changed. This can happen in response to navigation, scrolling, + * or form data changes; the session state passed includes the most up to date information on + * all of these. + * + * @param session GeckoSession that initiated the callback. + * @param sessionState SessionState representing the latest browser state. + */ + @UiThread + default void onSessionStateChange( + @NonNull final GeckoSession session, @NonNull final SessionState sessionState) {} + } + + /** WebResponseInfo contains information about a single web response. */ + @AnyThread + public static class WebResponseInfo { + /** The URI of the response. Cannot be null. */ + @NonNull public final String uri; + + /** The content type (mime type) of the response. May be null. */ + @Nullable public final String contentType; + + /** The content length of the response. May be 0 if unknokwn. */ + @Nullable public final long contentLength; + + /** The filename obtained from the content disposition, if any. May be null. */ + @Nullable public final String filename; + + /* package */ WebResponseInfo(final GeckoBundle message) { + uri = message.getString("uri"); + if (uri == null) { + throw new IllegalArgumentException("URI cannot be null"); + } + + contentType = message.getString("contentType"); + contentLength = message.getLong("contentLength"); + filename = message.getString("filename"); + } + + /** Empty constructor for tests. */ + protected WebResponseInfo() { + uri = ""; + contentType = ""; + contentLength = 0; + filename = ""; + } + } + + public interface ContentDelegate { + /** + * A page title was discovered in the content or updated after the content loaded. + * + * @param session The GeckoSession that initiated the callback. + * @param title The title sent from the content. + */ + @UiThread + default void onTitleChange(@NonNull final GeckoSession session, @Nullable final String title) {} + + /** + * A preview image was discovered in the content after the content loaded. + * + * @param session The GeckoSession that initiated the callback. + * @param previewImageUrl The preview image URL sent from the content. + */ + @UiThread + default void onPreviewImage( + @NonNull final GeckoSession session, @NonNull final String previewImageUrl) {} + + /** + * A page has requested focus. Note that window.focus() in content will not result in this being + * called. + * + * @param session The GeckoSession that initiated the callback. + */ + @UiThread + default void onFocusRequest(@NonNull final GeckoSession session) {} + + /** + * A page has requested to close + * + * @param session The GeckoSession that initiated the callback. + */ + @UiThread + default void onCloseRequest(@NonNull final GeckoSession session) {} + + /** + * A page has entered or exited full screen mode. Typically, the implementation would set the + * Activity containing the GeckoSession to full screen when the page is in full screen mode. + * + * @param session The GeckoSession that initiated the callback. + * @param fullScreen True if the page is in full screen mode. + */ + @UiThread + default void onFullScreen(@NonNull final GeckoSession session, final boolean fullScreen) {} + + /** + * A viewport-fit was discovered in the content or updated after the content. + * + * @param session The GeckoSession that initiated the callback. + * @param viewportFit The value of viewport-fit of meta element in content. + * @see <a href="https://drafts.csswg.org/css-round-display/#viewport-fit-descriptor">4.1. The + * viewport-fit descriptor</a> + */ + @UiThread + default void onMetaViewportFitChange( + @NonNull final GeckoSession session, @NonNull final String viewportFit) {} + + /** Element details for onContextMenu callbacks. */ + public static class ContextElement { + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_NONE, TYPE_IMAGE, TYPE_VIDEO, TYPE_AUDIO}) + public @interface Type {} + + public static final int TYPE_NONE = 0; + public static final int TYPE_IMAGE = 1; + public static final int TYPE_VIDEO = 2; + public static final int TYPE_AUDIO = 3; + + /** The base URI of the element's document. */ + public final @Nullable String baseUri; + + /** The absolute link URI (href) of the element. */ + public final @Nullable String linkUri; + + /** The title text of the element. */ + public final @Nullable String title; + + /** The alternative text (alt) for the element. */ + public final @Nullable String altText; + + /** The type of the element. One of the {@link ContextElement#TYPE_NONE} flags. */ + public final @Type int type; + + /** The source URI (src) of the element. Set for (nested) media elements. */ + public final @Nullable String srcUri; + + /** The text content of the element */ + public final @Nullable String textContent; + + // TODO: Bug 1595822 make public + final List<WebExtension.Menu> extensionMenus; + + /** + * ContextElement constructor. + * + * @param baseUri The base URI. + * @param linkUri The absolute link URI (href). + * @param title The title text. + * @param altText The alternative text (alt). + * @param typeStr The type of the element. + * @param srcUri The source URI (src). + * @param textContent The text content. + */ + protected ContextElement( + final @Nullable String baseUri, + final @Nullable String linkUri, + final @Nullable String title, + final @Nullable String altText, + final @NonNull String typeStr, + final @Nullable String srcUri, + final @Nullable String textContent) { + this.baseUri = baseUri; + this.linkUri = linkUri; + this.title = title; + this.altText = altText; + this.type = getType(typeStr); + this.srcUri = srcUri; + this.textContent = textContent; + this.extensionMenus = null; + } + + protected ContextElement( + final @Nullable String baseUri, + final @Nullable String linkUri, + final @Nullable String title, + final @Nullable String altText, + final @NonNull String typeStr, + final @Nullable String srcUri) { + this(baseUri, linkUri, title, altText, typeStr, srcUri, null); + } + + private static int getType(final String name) { + if ("HTMLImageElement".equals(name)) { + return TYPE_IMAGE; + } else if ("HTMLVideoElement".equals(name)) { + return TYPE_VIDEO; + } else if ("HTMLAudioElement".equals(name)) { + return TYPE_AUDIO; + } + return TYPE_NONE; + } + } + + /** + * A user has initiated the context menu via long-press. This event is fired on links, (nested) + * images and (nested) media elements. + * + * @param session The GeckoSession that initiated the callback. + * @param screenX The screen coordinates of the press. + * @param screenY The screen coordinates of the press. + * @param element The details for the pressed element. + */ + @UiThread + default void onContextMenu( + @NonNull final GeckoSession session, + final int screenX, + final int screenY, + @NonNull final ContextElement element) {} + + /** + * This is fired when there is a response that cannot be handled by Gecko (e.g., a download). + * + * @param session the GeckoSession that received the external response. + * @param response the external WebResponse. + */ + @UiThread + default void onExternalResponse( + @NonNull final GeckoSession session, @NonNull final WebResponse response) {} + + /** + * The content process hosting this GeckoSession has crashed. The GeckoSession is now closed and + * unusable. You may call {@link #open(GeckoRuntime)} to recover the session, but no state is + * preserved. Most applications will want to call {@link #load} or {@link + * #restoreState(SessionState)} at this point. + * + * @param session The GeckoSession for which the content process has crashed. + */ + @UiThread + default void onCrash(@NonNull final GeckoSession session) {} + + /** + * The content process hosting this GeckoSession has been killed. The GeckoSession is now closed + * and unusable. You may call {@link #open(GeckoRuntime)} to recover the session, but no state + * is preserved. Most applications will want to call {@link #load} or {@link + * #restoreState(SessionState)} at this point. + * + * @param session The GeckoSession for which the content process has been killed. + */ + @UiThread + default void onKill(@NonNull final GeckoSession session) {} + + /** + * Notification that the first content composition has occurred. This callback is invoked for + * the first content composite after either a start or a restart of the compositor. + * + * @param session The GeckoSession that had a first paint event. + */ + @UiThread + default void onFirstComposite(@NonNull final GeckoSession session) {} + + /** + * Notification that the first content paint has occurred. This callback is invoked for the + * first content paint after a page has been loaded, or after a {@link + * #onPaintStatusReset(GeckoSession)} event. The function {@link + * #onFirstComposite(GeckoSession)} will be called once the compositor has started rendering. + * However, it is possible for the compositor to start rendering before there is any content to + * render. onFirstContentfulPaint() is called once some content has been rendered. It may be + * nothing more than the page background color. It is not an indication that the whole page has + * been rendered. + * + * @param session The GeckoSession that had a first paint event. + */ + @UiThread + default void onFirstContentfulPaint(@NonNull final GeckoSession session) {} + + /** + * Notification that the paint status has been reset. + * + * <p>This callback is invoked whenever the painted content is no longer being displayed. This + * can occur in response to the session being paused. After this has fired the compositor may + * continue rendering, but may not render the page content. This callback can therefore be used + * in conjunction with {@link #onFirstContentfulPaint(GeckoSession)} to determine when there is + * valid content being rendered. + * + * @param session The GeckoSession that had the paint status reset event. + */ + @UiThread + default void onPaintStatusReset(@NonNull final GeckoSession session) {} + + /** + * A page has requested to change pointer icon. + * + * <p>If the application wants to control pointer icon, it should override this, then handle it. + * + * @param session The GeckoSession that initiated the callback. + * @param icon The pointer icon sent from the content. + */ + @TargetApi(Build.VERSION_CODES.N) + @UiThread + default void onPointerIconChange( + @NonNull final GeckoSession session, @NonNull final PointerIcon icon) { + final View view = session.getTextInput().getView(); + if (view != null) { + view.setPointerIcon(icon); + } + } + + /** + * This is fired when the loaded document has a valid Web App Manifest present. + * + * <p>The various colors (theme_color, background_color, etc.) present in the manifest have been + * transformed into #AARRGGBB format. + * + * @param session The GeckoSession that contains the Web App Manifest + * @param manifest A parsed and validated {@link JSONObject} containing the manifest contents. + * @see <a href="https://www.w3.org/TR/appmanifest/">Web App Manifest specification</a> + */ + @UiThread + default void onWebAppManifest( + @NonNull final GeckoSession session, @NonNull final JSONObject manifest) {} + + /** + * A script has exceeded its execution timeout value + * + * @param geckoSession GeckoSession that initiated the callback. + * @param scriptFileName Filename of the slow script + * @return A {@link GeckoResult} with a SlowScriptResponse value which indicates whether to + * allow the Slow Script to continue processing. Stop will halt the slow script. Continue + * will pause notifications for a period of time before resuming. + */ + @UiThread + default @Nullable GeckoResult<SlowScriptResponse> onSlowScript( + @NonNull final GeckoSession geckoSession, @NonNull final String scriptFileName) { + return null; + } + + /** + * The app should display its dynamic toolbar, fully expanded to the height that was previously + * specified via {@link GeckoView#setDynamicToolbarMaxHeight}. + * + * @param geckoSession GeckoSession that initiated the callback. + */ + @UiThread + default void onShowDynamicToolbar(@NonNull final GeckoSession geckoSession) {} + + /** + * This method is called when a cookie banner was detected. + * + * <p>Note: this method is called only if the cookie banner setting is such that allows to + * handle the banner. For example, if cookiebanners.service.mode=1 (Reject only) but a cookie + * banner can only be accepted on the website - the detection in that case won't be reported. + * The exception is MODE_DETECT_ONLY mode, when only the detection event is emitted. + * + * @param session GeckoSession that initiated the callback. + */ + @AnyThread + default void onCookieBannerDetected(@NonNull final GeckoSession session) {} + + /** + * This method is called when a cookie banner was handled. + * + * @param session GeckoSession that initiated the callback. + */ + @AnyThread + default void onCookieBannerHandled(@NonNull final GeckoSession session) {} + + /** + * This method is called when GeckoView is requesting a specific Nimbus feature in using message + * `GeckoView:GetNimbusFeature`. + * + * @param session GeckoSession that initiated the callback. + * @param featureId Nimbus feature id of the collected data. + * @return A {@link JSONObject} with the feature. + */ + @AnyThread + default @Nullable JSONObject onGetNimbusFeature( + @NonNull final GeckoSession session, @NonNull final String featureId) { + return null; + } + } + + public interface SelectionActionDelegate { + /** The selection is collapsed at a single position. */ + final int FLAG_IS_COLLAPSED = 1 << 0; + + /** + * The selection is inside editable content such as an input element or contentEditable node. + */ + final int FLAG_IS_EDITABLE = 1 << 1; + + /** The selection is inside a password field. */ + final int FLAG_IS_PASSWORD = 1 << 2; + + /** Hide selection actions and cause {@link #onHideAction} to be called. */ + final String ACTION_HIDE = "org.mozilla.geckoview.HIDE"; + + /** Copy onto the clipboard then delete the selected content. Selection must be editable. */ + final String ACTION_CUT = "org.mozilla.geckoview.CUT"; + + /** Copy the selected content onto the clipboard. */ + final String ACTION_COPY = "org.mozilla.geckoview.COPY"; + + /** Delete the selected content. Selection must be editable. */ + final String ACTION_DELETE = "org.mozilla.geckoview.DELETE"; + + /** Replace the selected content with the clipboard content. Selection must be editable. */ + final String ACTION_PASTE = "org.mozilla.geckoview.PASTE"; + + /** + * Replace the selected content with the clipboard content as plain text. Selection must be + * editable. + */ + final String ACTION_PASTE_AS_PLAIN_TEXT = "org.mozilla.geckoview.PASTE_AS_PLAIN_TEXT"; + + /** Select the entire content of the document or editor. */ + final String ACTION_SELECT_ALL = "org.mozilla.geckoview.SELECT_ALL"; + + /** Clear the current selection. Selection must not be editable. */ + final String ACTION_UNSELECT = "org.mozilla.geckoview.UNSELECT"; + + /** Collapse the current selection to its start position. Selection must be editable. */ + final String ACTION_COLLAPSE_TO_START = "org.mozilla.geckoview.COLLAPSE_TO_START"; + + /** Collapse the current selection to its end position. Selection must be editable. */ + final String ACTION_COLLAPSE_TO_END = "org.mozilla.geckoview.COLLAPSE_TO_END"; + + /** Represents attributes of a selection. */ + class Selection { + /** + * Flags describing the current selection, as a bitwise combination of the {@link + * #FLAG_IS_COLLAPSED FLAG_*} constants. + */ + public final @SelectionActionDelegateFlag int flags; + + /** + * Text content of the current selection. An empty string indicates the selection is collapsed + * or the selection cannot be represented as plain text. + */ + public final @NonNull String text; + + /** The bounds of the current selection in screen coordinates. */ + public final @Nullable RectF screenRect; + + /** Set of valid actions available through {@link Selection#execute(String)} */ + public final @NonNull @SelectionActionDelegateAction Collection<String> availableActions; + + private final String mActionId; + + private final WeakReference<EventDispatcher> mEventDispatcher; + + /* package */ Selection( + final GeckoBundle bundle, + final @NonNull @SelectionActionDelegateAction Set<String> actions, + final EventDispatcher eventDispatcher) { + flags = + (bundle.getBoolean("collapsed") ? SelectionActionDelegate.FLAG_IS_COLLAPSED : 0) + | (bundle.getBoolean("editable") ? SelectionActionDelegate.FLAG_IS_EDITABLE : 0) + | (bundle.getBoolean("password") ? SelectionActionDelegate.FLAG_IS_PASSWORD : 0); + text = bundle.getString("selection"); + screenRect = bundle.getRectF("screenRect"); + availableActions = actions; + mActionId = bundle.getString("actionId"); + mEventDispatcher = new WeakReference<>(eventDispatcher); + } + + /** Empty constructor for tests. */ + protected Selection() { + flags = 0; + text = ""; + screenRect = null; + availableActions = new HashSet<>(); + mActionId = null; + mEventDispatcher = null; + } + + /** + * Checks if the passed action is available + * + * @param action An {@link SelectionActionDelegate} to perform + * @return True if the action is available. + */ + @AnyThread + public boolean isActionAvailable( + @NonNull @SelectionActionDelegateAction final String action) { + return availableActions.contains(action); + } + + /** + * Execute an {@link SelectionActionDelegate} action. + * + * @throws IllegalStateException If the action was not available. + * @param action A {@link SelectionActionDelegate} action. + */ + @AnyThread + public void execute(@NonNull @SelectionActionDelegateAction final String action) { + if (!isActionAvailable(action)) { + throw new IllegalStateException("Action not available"); + } + final EventDispatcher eventDispatcher = mEventDispatcher.get(); + if (eventDispatcher == null) { + // The session is not available anymore, nothing really to do + Log.w(LOGTAG, "Calling execute on a stale Selection."); + return; + } + final GeckoBundle response = new GeckoBundle(2); + response.putString("id", action); + response.putString("actionId", mActionId); + eventDispatcher.dispatch("GeckoView:ExecuteSelectionAction", response); + } + + /** + * Hide selection actions and cause {@link #onHideAction} to be called. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void hide() { + execute(ACTION_HIDE); + } + + /** + * Copy onto the clipboard then delete the selected content. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void cut() { + execute(ACTION_CUT); + } + + /** + * Copy the selected content onto the clipboard. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void copy() { + execute(ACTION_COPY); + } + + /** + * Delete the selected content. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void delete() { + execute(ACTION_DELETE); + } + + /** + * Replace the selected content with the clipboard content. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void paste() { + execute(ACTION_PASTE); + } + + /** + * Replace the selected content with the clipboard content as plain text. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void pasteAsPlainText() { + execute(ACTION_PASTE_AS_PLAIN_TEXT); + } + + /** + * Select the entire content of the document or editor. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void selectAll() { + execute(ACTION_SELECT_ALL); + } + + /** + * Clear the current selection. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void unselect() { + execute(ACTION_UNSELECT); + } + + /** + * Collapse the current selection to its start position. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void collapseToStart() { + execute(ACTION_COLLAPSE_TO_START); + } + + /** + * Collapse the current selection to its end position. + * + * @throws IllegalStateException If the action was not available. + */ + @AnyThread + public void collapseToEnd() { + execute(ACTION_COLLAPSE_TO_END); + } + } + + /** + * Selection actions are available. Selection actions become available when the user selects + * some content in the document or editor. Inside an editor, selection actions can also become + * available when the user explicitly requests editor action UI, for example by tapping on the + * caret handle. + * + * <p>In response to this callback, applications typically display a toolbar containing the + * selection actions. To perform a certain action, check if the action is available with {@link + * Selection#isActionAvailable} then either use the relevant helper method or {@link + * Selection#execute} + * + * <p>Once an {@link #onHideAction} call (with particular reasons) or another {@link + * #onShowActionRequest} call is received, the previous Selection object is no longer usable. + * + * @param session The GeckoSession that initiated the callback. + * @param selection Current selection attributes and Callback object for performing built-in + * actions. May be used multiple times to perform multiple actions at once. + */ + @UiThread + default void onShowActionRequest( + @NonNull final GeckoSession session, @NonNull final Selection selection) {} + + /** Actions are no longer available due to the user clearing the selection. */ + final int HIDE_REASON_NO_SELECTION = 0; + + /** + * Actions are no longer available due to the user moving the selection out of view. Previous + * actions are still available after a callback with this reason. + */ + final int HIDE_REASON_INVISIBLE_SELECTION = 1; + + /** + * Actions are no longer available due to the user actively changing the selection. {@link + * #onShowActionRequest} may be called again once the user has set a selection, if the new + * selection has available actions. + */ + final int HIDE_REASON_ACTIVE_SELECTION = 2; + + /** + * Actions are no longer available due to the user actively scrolling the page. {@link + * #onShowActionRequest} may be called again once the user has stopped scrolling the page, if + * the selection is still visible. Until then, previous actions are still available after a + * callback with this reason. + */ + final int HIDE_REASON_ACTIVE_SCROLL = 3; + + /** + * Previous actions are no longer available due to the user interacting with the page. + * Applications typically hide the action toolbar in response. + * + * @param session The GeckoSession that initiated the callback. + * @param reason The reason that actions are no longer available, as one of the {@link + * #HIDE_REASON_NO_SELECTION HIDE_REASON_*} constants. + */ + @UiThread + default void onHideAction( + @NonNull final GeckoSession session, @SelectionActionDelegateHideReason final int reason) {} + + /** + * Permission for reading clipboard data. See: <a + * href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText">Clipboard.readText()</a> + */ + int PERMISSION_CLIPBOARD_READ = 1; + + /** Represents attributes of a clipboard permission. */ + public class ClipboardPermission { + /** The URI associated with this content permission. */ + public final @NonNull String uri; + + /** + * The type of this permission; one of {@link #PERMISSION_CLIPBOARD_READ + * PERMISSION_CLIPBOARD_*}. + */ + public final @ClipboardPermissionType int type; + + /** + * The last mouse or touch location in screen coordinates when the permission is requested. + */ + public final @Nullable Point screenPoint; + + /** Empty constructor for tests */ + protected ClipboardPermission() { + this.uri = ""; + this.type = PERMISSION_CLIPBOARD_READ; + this.screenPoint = null; + } + + private ClipboardPermission(final @NonNull GeckoBundle bundle) { + this.uri = bundle.getString("uri"); + this.type = PERMISSION_CLIPBOARD_READ; + this.screenPoint = bundle.getPoint("screenPoint"); + } + } + + /** + * Request clipboard permission. + * + * @param session The GeckoSession that initiated the callback. + * @param permission An {@link ClipboardPermission} describing the permission being requested. + * @return A {@link GeckoResult} with {@link AllowOrDeny}, determining the response to the + * permission request for this site. + */ + @UiThread + default @Nullable GeckoResult<AllowOrDeny> onShowClipboardPermissionRequest( + @NonNull final GeckoSession session, @NonNull ClipboardPermission permission) { + return GeckoResult.deny(); + } + + /** + * Dismiss requesting clipboard permission popup or model. + * + * @param session The GeckoSession that initiated the callback. + */ + @UiThread + default void onDismissClipboardPermissionRequest(@NonNull final GeckoSession session) {} + } + + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + SelectionActionDelegate.ACTION_HIDE, + SelectionActionDelegate.ACTION_CUT, + SelectionActionDelegate.ACTION_COPY, + SelectionActionDelegate.ACTION_DELETE, + SelectionActionDelegate.ACTION_PASTE, + SelectionActionDelegate.ACTION_PASTE_AS_PLAIN_TEXT, + SelectionActionDelegate.ACTION_SELECT_ALL, + SelectionActionDelegate.ACTION_UNSELECT, + SelectionActionDelegate.ACTION_COLLAPSE_TO_START, + SelectionActionDelegate.ACTION_COLLAPSE_TO_END + }) + public @interface SelectionActionDelegateAction {} + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + SelectionActionDelegate.FLAG_IS_COLLAPSED, + SelectionActionDelegate.FLAG_IS_EDITABLE, + SelectionActionDelegate.FLAG_IS_PASSWORD + }) + public @interface SelectionActionDelegateFlag {} + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + SelectionActionDelegate.HIDE_REASON_NO_SELECTION, + SelectionActionDelegate.HIDE_REASON_INVISIBLE_SELECTION, + SelectionActionDelegate.HIDE_REASON_ACTIVE_SELECTION, + SelectionActionDelegate.HIDE_REASON_ACTIVE_SCROLL + }) + public @interface SelectionActionDelegateHideReason {} + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + SelectionActionDelegate.PERMISSION_CLIPBOARD_READ, + }) + public @interface ClipboardPermissionType {} + + public interface NavigationDelegate { + /** + * A view has started loading content from the network. + * + * @param session The GeckoSession that initiated the callback. + * @param url The resource being loaded. + * @param perms The permissions currently associated with this url. + */ + @UiThread + default void onLocationChange( + @NonNull GeckoSession session, + @Nullable String url, + final @NonNull List<PermissionDelegate.ContentPermission> perms) {} + + /** + * The view's ability to go back has changed. + * + * @param session The GeckoSession that initiated the callback. + * @param canGoBack The new value for the ability. + */ + @UiThread + default void onCanGoBack(@NonNull final GeckoSession session, final boolean canGoBack) {} + + /** + * The view's ability to go forward has changed. + * + * @param session The GeckoSession that initiated the callback. + * @param canGoForward The new value for the ability. + */ + @UiThread + default void onCanGoForward(@NonNull final GeckoSession session, final boolean canGoForward) {} + + public static final int TARGET_WINDOW_NONE = 0; + public static final int TARGET_WINDOW_CURRENT = 1; + public static final int TARGET_WINDOW_NEW = 2; + + // Match with nsIWebNavigation.idl. + /** The load request was triggered by an HTTP redirect. */ + static final int LOAD_REQUEST_IS_REDIRECT = 0x800000; + + /** Load request details. */ + public static class LoadRequest { + /* package */ LoadRequest( + @NonNull final String uri, + @Nullable final String triggerUri, + final int geckoTarget, + final int flags, + final boolean hasUserGesture, + final boolean isDirectNavigation) { + this.uri = uri; + this.triggerUri = triggerUri; + this.target = convertGeckoTarget(geckoTarget); + this.isRedirect = (flags & LOAD_REQUEST_IS_REDIRECT) != 0; + this.hasUserGesture = hasUserGesture; + this.isDirectNavigation = isDirectNavigation; + } + + /** Empty constructor for tests. */ + protected LoadRequest() { + uri = ""; + triggerUri = null; + target = TARGET_WINDOW_NONE; + isRedirect = false; + hasUserGesture = false; + isDirectNavigation = false; + } + + // This needs to match nsIBrowserDOMWindow.idl + private @TargetWindow int convertGeckoTarget(final int geckoTarget) { + switch (geckoTarget) { + case 0: // OPEN_DEFAULTWINDOW + case 1: // OPEN_CURRENTWINDOW + return TARGET_WINDOW_CURRENT; + default: // OPEN_NEWWINDOW, OPEN_NEWTAB + return TARGET_WINDOW_NEW; + } + } + + /** The URI to be loaded. */ + public final @NonNull String uri; + + /** + * The URI of the origin page that triggered the load request. null for initial loads and + * loads originating from data: URIs. + */ + public final @Nullable String triggerUri; + + /** + * The target where the window has requested to open. One of {@link #TARGET_WINDOW_NONE + * TARGET_WINDOW_*}. + */ + public final @TargetWindow int target; + + /** + * True if and only if the request was triggered by an HTTP redirect. + * + * <p>If the user loads URI "a", which redirects to URI "b", then <code>onLoadRequest</code> + * will be called twice, first with uri "a" and <code>isRedirect = false</code>, then with uri + * "b" and <code>isRedirect = true</code>. + */ + public final boolean isRedirect; + + /** True if there was an active user gesture when the load was requested. */ + public final boolean hasUserGesture; + + /** + * This load request was initiated by a direct navigation from the application. E.g. when + * calling {@link GeckoSession#load}. + */ + public final boolean isDirectNavigation; + + @Override + public String toString() { + final StringBuilder out = new StringBuilder("LoadRequest { "); + out.append("uri: " + uri) + .append(", triggerUri: " + triggerUri) + .append(", target: " + target) + .append(", isRedirect: " + isRedirect) + .append(", hasUserGesture: " + hasUserGesture) + .append(", fromLoadUri: " + hasUserGesture) + .append(" }"); + return out.toString(); + } + } + + /** + * A request to open an URI. This is called before each top-level page load to allow custom + * behavior. For example, this can be used to override the behavior of TAGET_WINDOW_NEW + * requests, which defaults to requesting a new GeckoSession via onNewSession. + * + * @param session The GeckoSession that initiated the callback. + * @param request The {@link LoadRequest} containing the request details. + * @return A {@link GeckoResult} with a {@link AllowOrDeny} value which indicates whether or not + * the load was handled. If unhandled, Gecko will continue the load as normal. If handled (a + * {@link AllowOrDeny#DENY DENY} value), Gecko will abandon the load. A null return value is + * interpreted as {@link AllowOrDeny#ALLOW ALLOW} (unhandled). + */ + @UiThread + default @Nullable GeckoResult<AllowOrDeny> onLoadRequest( + @NonNull final GeckoSession session, @NonNull final LoadRequest request) { + return null; + } + + /** + * A request to load a URI in a non-top-level context. + * + * @param session The GeckoSession that initiated the callback. + * @param request The {@link LoadRequest} containing the request details. + * @return A {@link GeckoResult} with a {@link AllowOrDeny} value which indicates whether or not + * the load was handled. If unhandled, Gecko will continue the load as normal. If handled (a + * {@link AllowOrDeny#DENY DENY} value), Gecko will abandon the load. A null return value is + * interpreted as {@link AllowOrDeny#ALLOW ALLOW} (unhandled). + */ + @UiThread + default @Nullable GeckoResult<AllowOrDeny> onSubframeLoadRequest( + @NonNull final GeckoSession session, @NonNull final LoadRequest request) { + return null; + } + + /** + * A request has been made to open a new session. The URI is provided only for informational + * purposes. Do not call GeckoSession.load here. Additionally, the returned GeckoSession must be + * a newly-created one. + * + * @param session The GeckoSession that initiated the callback. + * @param uri The URI to be loaded. + * @return A {@link GeckoResult} which holds the returned GeckoSession. May be null, in which + * case the request for a new window by web content will fail. e.g., <code>window.open() + * </code> will return null. The implementation of onNewSession is responsible for + * maintaining a reference to the returned object, to prevent it from being garbage + * collected. + */ + @UiThread + default @Nullable GeckoResult<GeckoSession> onNewSession( + @NonNull final GeckoSession session, @NonNull final String uri) { + return null; + } + + /** + * @param session The GeckoSession that initiated the callback. + * @param uri The URI that failed to load. + * @param error A WebRequestError containing details about the error + * @return A URI to display as an error. Returning null will halt the load entirely. The + * following special methods are made available to the URI: - + * document.addCertException(isTemporary), returns Promise - + * document.getFailedCertSecurityInfo(), returns FailedCertSecurityInfo - + * document.getNetErrorInfo(), returns NetErrorInfo document.reloadWithHttpsOnlyException() + * @see <a + * href="https://searchfox.org/mozilla-central/source/dom/webidl/FailedCertSecurityInfo.webidl">FailedCertSecurityInfo + * IDL</a> + * @see <a + * href="https://searchfox.org/mozilla-central/source/dom/webidl/NetErrorInfo.webidl">NetErrorInfo + * IDL</a> + */ + @UiThread + default @Nullable GeckoResult<String> onLoadError( + @NonNull final GeckoSession session, + @Nullable final String uri, + @NonNull final WebRequestError error) { + return null; + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + NavigationDelegate.TARGET_WINDOW_NONE, + NavigationDelegate.TARGET_WINDOW_CURRENT, + NavigationDelegate.TARGET_WINDOW_NEW + }) + public @interface TargetWindow {} + + /** + * GeckoSession applications implement this interface to handle prompts triggered by content in + * the GeckoSession, such as alerts, authentication dialogs, and select list pickers. + */ + public interface PromptDelegate { + /** PromptResponse is an opaque class created upon confirming or dismissing a prompt. */ + public class PromptResponse { + private final BasePrompt mPrompt; + + /* package */ PromptResponse(@NonNull final BasePrompt prompt) { + mPrompt = prompt; + } + + /* package */ void dispatch(@NonNull final EventCallback callback) { + if (mPrompt == null) { + throw new RuntimeException("Trying to confirm/dismiss a null prompt."); + } + mPrompt.dispatch(callback); + } + } + + interface PromptInstanceDelegate { + /** + * Called when this prompt has been dismissed by the system. + * + * <p>This can happen e.g. when the page navigates away and the content of the prompt is not + * relevant anymore. + * + * <p>When this method is called, you should hide the prompt UI elements. + * + * @param prompt the prompt that should be dismissed. + */ + @UiThread + default void onPromptDismiss(final @NonNull BasePrompt prompt) {} + + /** + * Called when this prompt has been updated. + * + * <p>This is called if inner <option> elements are updated when using <select> + * element. + * + * <p>When this method is called, you should update the prompt UI elements. + * + * @param prompt the new prompt that should be updated. + */ + @UiThread + default void onPromptUpdate(final @NonNull BasePrompt prompt) {} + } + + // Prompt classes. + public class BasePrompt { + private boolean mIsCompleted; + private boolean mIsConfirmed; + private GeckoBundle mResult; + private final WeakReference<Observer> mObserver; + private PromptInstanceDelegate mDelegate; + + protected interface Observer { + @AnyThread + default void onPromptCompleted(@NonNull BasePrompt prompt) {} + } + + private void complete() { + mIsCompleted = true; + final Observer observer = mObserver.get(); + if (observer != null) { + observer.onPromptCompleted(this); + } + } + + /** The title of this prompt; may be null. */ + public final @Nullable String title; + + /* package */ String id; + + private BasePrompt( + @NonNull final String id, @Nullable final String title, final Observer observer) { + this.title = title; + this.id = id; + mIsConfirmed = false; + mIsCompleted = false; + mObserver = new WeakReference<>(observer); + } + + @UiThread + protected @NonNull PromptResponse confirm() { + if (mIsCompleted) { + throw new RuntimeException("Cannot confirm/dismiss a Prompt twice."); + } + + mIsConfirmed = true; + complete(); + return new PromptResponse(this); + } + + /** + * This dismisses the prompt without sending any meaningful information back to content. + * + * @return A {@link PromptResponse} with which you can complete the {@link GeckoResult} that + * corresponds to this prompt. + */ + @UiThread + public @NonNull PromptResponse dismiss() { + if (mIsCompleted) { + throw new RuntimeException("Cannot confirm/dismiss a Prompt twice."); + } + + complete(); + return new PromptResponse(this); + } + + /** + * Set the delegate for this prompt. + * + * @param delegate the {@link PromptInstanceDelegate} instance. + */ + @UiThread + public void setDelegate(final @Nullable PromptInstanceDelegate delegate) { + mDelegate = delegate; + } + + /** + * Get the delegate for this prompt. + * + * @return the {@link PromptInstanceDelegate} instance. + */ + @UiThread + @Nullable + public PromptInstanceDelegate getDelegate() { + return mDelegate; + } + + /* package */ GeckoBundle ensureResult() { + if (mResult == null) { + // Usually result object contains two items. + mResult = new GeckoBundle(2); + } + return mResult; + } + + /** + * This returns true if the prompt has already been confirmed or dismissed. + * + * @return A boolean which is true if the prompt has been confirmed or dismissed, and false + * otherwise. + */ + @UiThread + public boolean isComplete() { + return mIsCompleted; + } + + /* package */ void dispatch(@NonNull final EventCallback callback) { + if (!mIsCompleted) { + throw new RuntimeException("Trying to dispatch an incomplete prompt."); + } + + if (!mIsConfirmed) { + callback.sendSuccess(null); + } else { + callback.sendSuccess(mResult); + } + } + } + + /** + * BeforeUnloadPrompt represents the onbeforeunload prompt. See + * https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload + */ + class BeforeUnloadPrompt extends BasePrompt { + protected BeforeUnloadPrompt(@NonNull final String id, @NonNull final Observer observer) { + super(id, null, observer); + } + + /** + * Confirms the prompt. + * + * @param allowOrDeny whether the navigation should be allowed to continue or not. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(final @Nullable AllowOrDeny allowOrDeny) { + ensureResult().putBoolean("allow", allowOrDeny != AllowOrDeny.DENY); + return super.confirm(); + } + } + + /** + * RepostConfirmPrompt represents a prompt shown whenever the browser needs to resubmit POST + * data (e.g. due to page refresh). + */ + class RepostConfirmPrompt extends BasePrompt { + protected RepostConfirmPrompt(@NonNull final String id, @NonNull final Observer observer) { + super(id, null, observer); + } + + /** + * Confirms the prompt. + * + * @param allowOrDeny whether the browser should allow resubmitting data. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(final @Nullable AllowOrDeny allowOrDeny) { + ensureResult().putBoolean("allow", allowOrDeny != AllowOrDeny.DENY); + return super.confirm(); + } + } + + /** + * AlertPrompt contains the information necessary to represent a JavaScript alert() call from + * content; it can only be dismissed, not confirmed. + */ + public class AlertPrompt extends BasePrompt { + /** The message to be displayed with this alert; may be null. */ + public final @Nullable String message; + + protected AlertPrompt( + @NonNull final String id, + @Nullable final String title, + @Nullable final String message, + @NonNull final Observer observer) { + super(id, title, observer); + this.message = message; + } + } + + /** + * ButtonPrompt contains the information necessary to represent a JavaScript confirm() call from + * content. + */ + public class ButtonPrompt extends BasePrompt { + @Retention(RetentionPolicy.SOURCE) + @IntDef({Type.POSITIVE, Type.NEGATIVE}) + public @interface ButtonType {} + + public static class Type { + /** Index of positive response button (eg, "Yes", "OK") */ + public static final int POSITIVE = 0; + + /** Index of negative response button (eg, "No", "Cancel") */ + public static final int NEGATIVE = 2; + + protected Type() {} + } + + /** The message to be displayed with this prompt; may be null. */ + public final @Nullable String message; + + protected ButtonPrompt( + @NonNull final String id, + @Nullable final String title, + @Nullable final String message, + @NonNull final Observer observer) { + super(id, title, observer); + this.message = message; + } + + /** + * Confirms this prompt, returning the selected button to content. + * + * @param selection An int representing the selected button, must be one of {@link Type}. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@ButtonType final int selection) { + ensureResult().putInt("button", selection); + return super.confirm(); + } + } + + /** + * TextPrompt contains the information necessary to represent a Javascript prompt() call from + * content. + */ + public class TextPrompt extends BasePrompt { + /** The message to be displayed with this prompt; may be null. */ + public final @Nullable String message; + + /** The default value for the text field; may be null. */ + public final @Nullable String defaultValue; + + protected TextPrompt( + @NonNull final String id, + @Nullable final String title, + @Nullable final String message, + @Nullable final String defaultValue, + @NonNull final Observer observer) { + super(id, title, observer); + this.message = message; + this.defaultValue = defaultValue; + } + + /** + * Confirms this prompt, returning the input text to content. + * + * @param text A String containing the text input given by the user. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final String text) { + ensureResult().putString("text", text); + return super.confirm(); + } + } + + /** + * AuthPrompt contains the information necessary to represent an HTML authorization prompt + * generated by content. + */ + public class AuthPrompt extends BasePrompt { + public static class AuthOptions { + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + Flags.HOST, + Flags.PROXY, + Flags.ONLY_PASSWORD, + Flags.PREVIOUS_FAILED, + Flags.CROSS_ORIGIN_SUB_RESOURCE + }) + public @interface AuthFlag {} + + /** Auth prompt flags. */ + public static class Flags { + /** The auth prompt is for a network host. */ + public static final int HOST = 1 << 0; + + /** The auth prompt is for a proxy. */ + public static final int PROXY = 1 << 1; + + /** The auth prompt should only request a password. */ + public static final int ONLY_PASSWORD = 1 << 3; + + /** The auth prompt is the result of a previous failed login. */ + public static final int PREVIOUS_FAILED = 1 << 4; + + /** The auth prompt is for a cross-origin sub-resource. */ + public static final int CROSS_ORIGIN_SUB_RESOURCE = 1 << 5; + + protected Flags() {} + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({Level.NONE, Level.PW_ENCRYPTED, Level.SECURE}) + public @interface AuthLevel {} + + /** Auth prompt levels. */ + public static class Level { + /** The auth request is unencrypted or the encryption status is unknown. */ + public static final int NONE = 0; + + /** The auth request only encrypts password but not data. */ + public static final int PW_ENCRYPTED = 1; + + /** The auth request encrypts both password and data. */ + public static final int SECURE = 2; + + protected Level() {} + } + + /** An int bit-field of {@link Flags}. */ + public @AuthFlag final int flags; + + /** A string containing the URI for the auth request or null if unknown. */ + public @Nullable final String uri; + + /** An int, one of {@link Level}, indicating level of encryption. */ + public @AuthLevel final int level; + + /** A string containing the initial username or null if password-only. */ + public @Nullable final String username; + + /** A string containing the initial password. */ + public @Nullable final String password; + + /* package */ AuthOptions(final GeckoBundle options) { + flags = options.getInt("flags"); + uri = options.getString("uri"); + level = options.getInt("level"); + username = options.getString("username"); + password = options.getString("password"); + } + + /** Empty constructor for tests */ + protected AuthOptions() { + flags = 0; + uri = ""; + level = Level.NONE; + username = ""; + password = ""; + } + } + + /** The message to be displayed with this prompt; may be null. */ + public final @Nullable String message; + + /** The {@link AuthOptions} that describe the type of authorization prompt. */ + public final @NonNull AuthOptions authOptions; + + protected AuthPrompt( + @NonNull final String id, + @Nullable final String title, + @Nullable final String message, + @NonNull final AuthOptions authOptions, + @NonNull final Observer observer) { + super(id, title, observer); + this.message = message; + this.authOptions = authOptions; + } + + /** + * Confirms this prompt with just a password, returning the password to content. + * + * @param password A String containing the password input by the user. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final String password) { + ensureResult().putString("password", password); + return super.confirm(); + } + + /** + * Confirms this prompt with a username and password, returning both to content. + * + * @param username A String containing the username input by the user. + * @param password A String containing the password input by the user. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm( + @NonNull final String username, @NonNull final String password) { + ensureResult().putString("username", username); + ensureResult().putString("password", password); + return super.confirm(); + } + } + + /** + * ChoicePrompt contains the information necessary to display a menu or list prompt generated by + * content. + */ + public class ChoicePrompt extends BasePrompt { + public static class Choice { + /** + * A boolean indicating if the item is disabled. Item should not be selectable if this is + * true. + */ + public final boolean disabled; + + /** + * A String giving the URI of the item icon, or null if none exists (only valid for menus) + */ + public final @Nullable String icon; + + /** A String giving the ID of the item or group */ + public final @NonNull String id; + + /** A Choice array of sub-items in a group, or null if not a group */ + public final @Nullable Choice[] items; + + /** A string giving the label for displaying the item or group */ + public final @NonNull String label; + + /** A boolean indicating if the item should be pre-selected (pre-checked for menu items) */ + public final boolean selected; + + /** A boolean indicating if the item should be a menu separator (only valid for menus) */ + public final boolean separator; + + /* package */ Choice(final GeckoBundle choice) { + disabled = choice.getBoolean("disabled"); + icon = choice.getString("icon"); + id = choice.getString("id"); + label = choice.getString("label"); + selected = choice.getBoolean("selected"); + separator = choice.getBoolean("separator"); + + final GeckoBundle[] choices = choice.getBundleArray("items"); + if (choices == null) { + items = null; + } else { + items = new Choice[choices.length]; + for (int i = 0; i < choices.length; i++) { + items[i] = new Choice(choices[i]); + } + } + } + + /** Empty constructor for tests. */ + protected Choice() { + disabled = false; + icon = ""; + id = ""; + label = ""; + selected = false; + separator = false; + items = null; + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({Type.MENU, Type.SINGLE, Type.MULTIPLE}) + public @interface ChoiceType {} + + public static class Type { + /** Display choices in a menu that dismisses as soon as an item is chosen. */ + public static final int MENU = 1; + + /** Display choices in a list that allows a single selection. */ + public static final int SINGLE = 2; + + /** Display choices in a list that allows multiple selections. */ + public static final int MULTIPLE = 3; + + protected Type() {} + } + + /** The message to be displayed with this prompt; may be null. */ + public final @Nullable String message; + + /** One of {@link Type}. */ + public final @ChoiceType int type; + + /** An array of {@link Choice} representing possible choices. */ + public final @NonNull Choice[] choices; + + protected ChoicePrompt( + @NonNull final String id, + @Nullable final String title, + @Nullable final String message, + @ChoiceType final int type, + @NonNull final Choice[] choices, + @NonNull final Observer observer) { + super(id, title, observer); + this.message = message; + this.type = type; + this.choices = choices; + } + + /** + * Confirms this prompt with the string id of a single choice. + * + * @param selectedId The string ID of the selected choice. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final String selectedId) { + return confirm(new String[] {selectedId}); + } + + /** + * Confirms this prompt with the string ids of multiple choices + * + * @param selectedIds The string IDs of the selected choices. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final String[] selectedIds) { + if ((Type.MENU == type || Type.SINGLE == type) + && (selectedIds == null || selectedIds.length != 1)) { + throw new IllegalArgumentException(); + } + ensureResult().putStringArray("choices", selectedIds); + return super.confirm(); + } + + /** + * Confirms this prompt with a single choice. + * + * @param selectedChoice The selected choice. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final Choice selectedChoice) { + return confirm(selectedChoice == null ? null : selectedChoice.id); + } + + /** + * Confirms this prompt with multiple choices. + * + * @param selectedChoices The selected choices. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final Choice[] selectedChoices) { + if ((Type.MENU == type || Type.SINGLE == type) + && (selectedChoices == null || selectedChoices.length != 1)) { + throw new IllegalArgumentException(); + } + + if (selectedChoices == null) { + return confirm((String[]) null); + } + + final String[] ids = new String[selectedChoices.length]; + for (int i = 0; i < ids.length; i++) { + ids[i] = (selectedChoices[i] == null) ? null : selectedChoices[i].id; + } + + return confirm(ids); + } + } + + /** + * ColorPrompt contains the information necessary to represent a prompt for color input + * generated by content. + */ + public class ColorPrompt extends BasePrompt { + /** The default value supplied by content. */ + public final @Nullable String defaultValue; + + /** The predefined values by <datalist> element */ + public final @Nullable String[] predefinedValues; + + protected ColorPrompt( + @NonNull final String id, + @Nullable final String title, + @Nullable final String defaultValue, + @Nullable final String[] predefinedValues, + @NonNull final Observer observer) { + super(id, title, observer); + this.defaultValue = defaultValue; + this.predefinedValues = predefinedValues; + } + + /** + * Confirms the prompt and passes the color value back to content. + * + * @param color A String representing the color to be returned to content. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final String color) { + ensureResult().putString("color", color); + return super.confirm(); + } + } + + /** + * DateTimePrompt contains the information necessary to represent a prompt for date and/or time + * input generated by content. + */ + public class DateTimePrompt extends BasePrompt { + @Retention(RetentionPolicy.SOURCE) + @IntDef({Type.DATE, Type.MONTH, Type.WEEK, Type.TIME, Type.DATETIME_LOCAL}) + public @interface DatetimeType {} + + public static class Type { + /** Prompt for year, month, and day. */ + public static final int DATE = 1; + + /** Prompt for year and month. */ + public static final int MONTH = 2; + + /** Prompt for year and week. */ + public static final int WEEK = 3; + + /** Prompt for hour and minute. */ + public static final int TIME = 4; + + /** Prompt for year, month, day, hour, and minute, without timezone. */ + public static final int DATETIME_LOCAL = 5; + + protected Type() {} + } + + /** One of {@link Type} indicating the type of prompt. */ + public final @DatetimeType int type; + + /** A String representing the default value supplied by content. */ + public final @Nullable String defaultValue; + + /** A String representing the minimum value allowed by content. */ + public final @Nullable String minValue; + + /** A String representing the maximum value allowed by content. */ + public final @Nullable String maxValue; + + /** A String representing the step value allowed by content. */ + public final @Nullable String stepValue; + + /** For testing. */ + private DateTimePrompt() { + // Initialize final members + super("", null, null); + this.type = Type.DATE; + this.defaultValue = null; + this.minValue = null; + this.maxValue = null; + this.stepValue = null; + } + + /* package */ DateTimePrompt( + @NonNull final String id, + @Nullable final String title, + @DatetimeType final int type, + @Nullable final String defaultValue, + @Nullable final String minValue, + @Nullable final String maxValue, + @Nullable final String stepValue, + @NonNull final Observer observer) { + super(id, title, observer); + this.type = type; + this.defaultValue = defaultValue; + this.minValue = minValue; + this.maxValue = maxValue; + this.stepValue = stepValue; + } + + /** + * Confirms the prompt and passes the date and/or time value back to content. + * + * @param datetime A String representing the date and time to be returned to content. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final String datetime) { + ensureResult().putString("datetime", datetime); + return super.confirm(); + } + } + + /** + * FilePrompt contains the information necessary to represent a prompt for a file or files + * generated by content. + */ + public class FilePrompt extends BasePrompt { + @Retention(RetentionPolicy.SOURCE) + @IntDef({Type.SINGLE, Type.MULTIPLE}) + public @interface FileType {} + + /** Types of file prompts. */ + public static class Type { + /** Prompt for a single file. */ + public static final int SINGLE = 1; + + /** Prompt for multiple files. */ + public static final int MULTIPLE = 2; + + protected Type() {} + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({Capture.NONE, Capture.ANY, Capture.USER, Capture.ENVIRONMENT}) + public @interface CaptureType {} + + /** Possible capture attribute values. */ + public static class Capture { + // These values should match the corresponding values in nsIFilePicker.idl + /** No capture attribute has been supplied by content. */ + public static final int NONE = 0; + + /** The capture attribute was supplied with a missing or invalid value. */ + public static final int ANY = 1; + + /** The "user" capture attribute has been supplied by content. */ + public static final int USER = 2; + + /** The "environment" capture attribute has been supplied by content. */ + public static final int ENVIRONMENT = 3; + + protected Capture() {} + } + + /** One of {@link Type} indicating the prompt type. */ + public final @FileType int type; + + /** + * An array of Strings giving the MIME types specified by the "accept" attribute, if any are + * specified. + */ + public final @Nullable String[] mimeTypes; + + /** One of {@link Capture} indicating the capture attribute supplied by content. */ + public final @CaptureType int capture; + + protected FilePrompt( + @NonNull final String id, + @Nullable final String title, + @FileType final int type, + @CaptureType final int capture, + @Nullable final String[] mimeTypes, + @NonNull final Observer observer) { + super(id, title, observer); + this.type = type; + this.capture = capture; + this.mimeTypes = mimeTypes; + } + + /** + * Confirms the prompt and passes the file URI back to content. + * + * @param context An Application context for parsing URIs. + * @param uri The URI of the file chosen by the user. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm( + @NonNull final Context context, @NonNull final Uri uri) { + return confirm(context, new Uri[] {uri}); + } + + /** + * Confirms the prompt and passes the file URIs back to content. + * + * @param context An Application context for parsing URIs. + * @param uris The URIs of the files chosen by the user. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm( + @NonNull final Context context, @NonNull final Uri[] uris) { + if (Type.SINGLE == type && (uris == null || uris.length != 1)) { + throw new IllegalArgumentException(); + } + + final String[] paths = new String[uris != null ? uris.length : 0]; + for (int i = 0; i < paths.length; i++) { + paths[i] = getFile(context, uris[i]); + if (paths[i] == null) { + Log.e(LOGTAG, "Only file URIs are supported: " + uris[i]); + } + } + ensureResult().putStringArray("files", paths); + + return super.confirm(); + } + + private static String getFile(final @NonNull Context context, final @NonNull Uri uri) { + if (uri == null) { + return null; + } + if ("file".equals(uri.getScheme())) { + return uri.getPath(); + } + final ContentResolver cr = context.getContentResolver(); + final Cursor cur = + cr.query( + uri, + new String[] {"_data"}, /* selection */ + null, + /* args */ null, /* sort */ + null); + if (cur == null) { + return null; + } + try { + final int idx = cur.getColumnIndex("_data"); + if (idx < 0 || !cur.moveToFirst()) { + return null; + } + do { + try { + final String path = cur.getString(idx); + if (path != null && !path.isEmpty()) { + return path; + } + } catch (final Exception e) { + } + } while (cur.moveToNext()); + } finally { + cur.close(); + } + return null; + } + } + + /** PopupPrompt contains the information necessary to represent a popup blocking request. */ + public class PopupPrompt extends BasePrompt { + /** The target URI for the popup; may be null. */ + public final @Nullable String targetUri; + + protected PopupPrompt( + @NonNull final String id, + @Nullable final String targetUri, + @NonNull final Observer observer) { + super(id, null, observer); + this.targetUri = targetUri; + } + + /** + * Confirms the prompt and either allows or blocks the popup. + * + * @param response An {@link AllowOrDeny} specifying whether to allow or deny the popup. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@NonNull final AllowOrDeny response) { + boolean res = false; + if (AllowOrDeny.ALLOW == response) { + res = true; + } + ensureResult().putBoolean("response", res); + return super.confirm(); + } + } + + /** SharePrompt contains the information necessary to represent a (v1) WebShare request. */ + public class SharePrompt extends BasePrompt { + @Retention(RetentionPolicy.SOURCE) + @IntDef({Result.SUCCESS, Result.FAILURE, Result.ABORT}) + public @interface ShareResult {} + + /** Possible results to a {@link SharePrompt}. */ + public static class Result { + /** The user shared with another app successfully. */ + public static final int SUCCESS = 0; + + /** The user attempted to share with another app, but it failed. */ + public static final int FAILURE = 1; + + /** The user aborted the share. */ + public static final int ABORT = 2; + + protected Result() {} + } + + /** The text for the share request. */ + public final @Nullable String text; + + /** The uri for the share request. */ + public final @Nullable String uri; + + protected SharePrompt( + @NonNull final String id, + @Nullable final String title, + @Nullable final String text, + @Nullable final String uri, + @NonNull final Observer observer) { + super(id, title, observer); + this.text = text; + this.uri = uri; + } + + /** + * Confirms the prompt and either blocks or allows the share request. + * + * @param response One of {@link Result} specifying the outcome of the share attempt. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(@ShareResult final int response) { + ensureResult().putInt("response", response); + return super.confirm(); + } + + /** + * Dismisses the prompt and returns {@link Result#ABORT} to web content. + * + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse dismiss() { + ensureResult().putInt("response", Result.ABORT); + return super.dismiss(); + } + } + + /** Request containing information required to resolve Autocomplete prompt requests. */ + public class AutocompleteRequest<T extends Autocomplete.Option<?>> extends BasePrompt { + /** + * The Autocomplete options for this request. This can contain a single or multiple entries. + */ + public final @NonNull T[] options; + + protected AutocompleteRequest( + final @NonNull String id, final @NonNull T[] options, final Observer observer) { + super(id, null, observer); + this.options = options; + } + + /** + * Confirm the request by responding with a selection. See the PromptDelegate callbacks for + * specifics. + * + * @param selection The {@link Autocomplete.Option} used to confirm the request. + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse confirm(final @NonNull Autocomplete.Option<?> selection) { + ensureResult().putBundle("selection", selection.toBundle()); + return super.confirm(); + } + + /** + * Dismiss the request. See the PromptDelegate callbacks for specifics. + * + * @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult} + * associated with this prompt. + */ + @UiThread + public @NonNull PromptResponse dismiss() { + return super.dismiss(); + } + } + + // Delegate functions. + /** + * Display an alert prompt. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link AlertPrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onAlertPrompt( + @NonNull final GeckoSession session, @NonNull final AlertPrompt prompt) { + return null; + } + + /** + * Display a onbeforeunload prompt. + * + * <p>See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload + * See {@link BeforeUnloadPrompt} + * + * @param session GeckoSession that triggered the prompt + * @param prompt the {@link BeforeUnloadPrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to {@link AllowOrDeny#ALLOW} if the page is allowed + * to continue with the navigation or {@link AllowOrDeny#DENY} otherwise. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onBeforeUnloadPrompt( + @NonNull final GeckoSession session, @NonNull final BeforeUnloadPrompt prompt) { + return null; + } + + /** + * Display a POST resubmission confirmation prompt. + * + * <p>This prompt will trigger whenever refreshing or navigating to a page needs resubmitting + * POST data that has been submitted already. + * + * @param session GeckoSession that triggered the prompt + * @param prompt the {@link RepostConfirmPrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to {@link AllowOrDeny#ALLOW} if the page is allowed + * to continue with the navigation and resubmit the POST data or {@link AllowOrDeny#DENY} + * otherwise. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onRepostConfirmPrompt( + @NonNull final GeckoSession session, @NonNull final RepostConfirmPrompt prompt) { + return null; + } + + /** + * Display a button prompt. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link ButtonPrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onButtonPrompt( + @NonNull final GeckoSession session, @NonNull final ButtonPrompt prompt) { + return null; + } + + /** + * Display a text prompt. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link TextPrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onTextPrompt( + @NonNull final GeckoSession session, @NonNull final TextPrompt prompt) { + return null; + } + + /** + * Display an authorization prompt. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link AuthPrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onAuthPrompt( + @NonNull final GeckoSession session, @NonNull final AuthPrompt prompt) { + return null; + } + + /** + * Display a list/menu prompt. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link ChoicePrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onChoicePrompt( + @NonNull final GeckoSession session, @NonNull final ChoicePrompt prompt) { + return null; + } + + /** + * Display a color prompt. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link ColorPrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onColorPrompt( + @NonNull final GeckoSession session, @NonNull final ColorPrompt prompt) { + return null; + } + + /** + * Display a date/time prompt. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link DateTimePrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onDateTimePrompt( + @NonNull final GeckoSession session, @NonNull final DateTimePrompt prompt) { + return null; + } + + /** + * Display a file prompt. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link FilePrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onFilePrompt( + @NonNull final GeckoSession session, @NonNull final FilePrompt prompt) { + return null; + } + + /** + * Display a popup request prompt; this occurs when content attempts to open a new window in a + * way that doesn't appear to be the result of user input. + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link PopupPrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onPopupPrompt( + @NonNull final GeckoSession session, @NonNull final PopupPrompt prompt) { + return null; + } + + /** + * Display a share request prompt; this occurs when content attempts to use the WebShare API. + * See: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share + * + * @param session GeckoSession that triggered the prompt. + * @param prompt The {@link SharePrompt} that describes the prompt. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all + * necessary information to resolve the prompt. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onSharePrompt( + @NonNull final GeckoSession session, @NonNull final SharePrompt prompt) { + return null; + } + + /** + * Handle a login save prompt request. This is triggered by the user entering new or modified + * login credentials into a login form. + * + * @param session The {@link GeckoSession} that triggered the request. + * @param request The {@link AutocompleteRequest} containing the request details. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse}. + * <p>Confirm the request with an {@link Autocomplete.Option} to trigger a {@link + * Autocomplete.StorageDelegate#onLoginSave} request to save the given selection. The + * confirmed selection may be an entry out of the request's options, a modified option, or a + * freshly created login entry. + * <p>Dismiss the request to deny the saving request. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onLoginSave( + @NonNull final GeckoSession session, + @NonNull final AutocompleteRequest<Autocomplete.LoginSaveOption> request) { + return null; + } + + /** + * Handle a address save prompt request. This is triggered by the user entering new or modified + * address credentials into a address form. + * + * @param session The {@link GeckoSession} that triggered the request. + * @param request The {@link AutocompleteRequest} containing the request details. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse}. + * <p>Confirm the request with an {@link Autocomplete.Option} to trigger a {@link + * Autocomplete.StorageDelegate#onAddressSave} request to save the given selection. The + * confirmed selection may be an entry out of the request's options, a modified option, or a + * freshly created address entry. + * <p>Dismiss the request to deny the saving request. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onAddressSave( + @NonNull final GeckoSession session, + @NonNull final AutocompleteRequest<Autocomplete.AddressSaveOption> request) { + return null; + } + + /** + * Handle a credit card save prompt request. This is triggered by the user entering new or + * modified credit card credentials into a form. + * + * @param session The {@link GeckoSession} that triggered the request. + * @param request The {@link AutocompleteRequest} containing the request details. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse}. + * <p>Confirm the request with an {@link Autocomplete.Option} to trigger a {@link + * Autocomplete.StorageDelegate#onCreditCardSave} request to save the given selection. The + * confirmed selection may be an entry out of the request's options, a modified option, or a + * freshly created credit card entry. + * <p>Dismiss the request to deny the saving request. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onCreditCardSave( + @NonNull final GeckoSession session, + @NonNull final AutocompleteRequest<Autocomplete.CreditCardSaveOption> request) { + return null; + } + + /** + * Handle a login selection prompt request. This is triggered by the user focusing on a login + * username field. + * + * @param session The {@link GeckoSession} that triggered the request. + * @param request The {@link AutocompleteRequest} containing the request details. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} + * <p>Confirm the request with an {@link Autocomplete.Option} to let GeckoView fill out the + * login forms with the given selection details. The confirmed selection may be an entry out + * of the request's options, a modified option, or a freshly created login entry. + * <p>Dismiss the request to deny autocompletion for the detected form. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onLoginSelect( + @NonNull final GeckoSession session, + @NonNull final AutocompleteRequest<Autocomplete.LoginSelectOption> request) { + return null; + } + + /** + * Handle a credit card selection prompt request. This is triggered by the user focusing on a + * credit card input field. + * + * @param session The {@link GeckoSession} that triggered the request. + * @param request The {@link AutocompleteRequest} containing the request details. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} + * <p>Confirm the request with an {@link Autocomplete.Option} to let GeckoView fill out the + * credit card forms with the given selection details. The confirmed selection may be an + * entry out of the request's options, a modified option, or a freshly created credit card + * entry. + * <p>Dismiss the request to deny autocompletion for the detected form. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onCreditCardSelect( + @NonNull final GeckoSession session, + @NonNull final AutocompleteRequest<Autocomplete.CreditCardSelectOption> request) { + return null; + } + + /** + * Handle a address selection prompt request. This is triggered by the user focusing on a + * address field. + * + * @param session The {@link GeckoSession} that triggered the request. + * @param request The {@link AutocompleteRequest} containing the request details. + * @return A {@link GeckoResult} resolving to a {@link PromptResponse} + * <p>Confirm the request with an {@link Autocomplete.Option} to let GeckoView fill out the + * address forms with the given selection details. The confirmed selection may be an entry + * out of the request's options, a modified option, or a freshly created address entry. + * <p>Dismiss the request to deny autocompletion for the detected form. + */ + @UiThread + default @Nullable GeckoResult<PromptResponse> onAddressSelect( + @NonNull final GeckoSession session, + @NonNull final AutocompleteRequest<Autocomplete.AddressSelectOption> request) { + return null; + } + } + + /** GeckoSession applications implement this interface to handle content scroll events. */ + public interface ScrollDelegate { + /** + * The scroll position of the content has changed. + * + * @param session GeckoSession that initiated the callback. + * @param scrollX The new horizontal scroll position in pixels. + * @param scrollY The new vertical scroll position in pixels. + */ + @UiThread + default void onScrollChanged( + @NonNull final GeckoSession session, final int scrollX, final int scrollY) {} + } + + /** + * Get the PanZoomController instance for this session. + * + * @return PanZoomController instance. + */ + @UiThread + public @NonNull PanZoomController getPanZoomController() { + ThreadUtils.assertOnUiThread(); + + return mPanZoomController; + } + + /** + * Get the OverscrollEdgeEffect instance for this session. + * + * @return OverscrollEdgeEffect instance. + */ + @UiThread + public @NonNull OverscrollEdgeEffect getOverscrollEdgeEffect() { + ThreadUtils.assertOnUiThread(); + + if (mOverscroll == null) { + mOverscroll = new OverscrollEdgeEffect(); + } + return mOverscroll; + } + + /** + * Get the CompositorController instance for this session. + * + * @return CompositorController instance. + */ + @UiThread + public @NonNull CompositorController getCompositorController() { + ThreadUtils.assertOnUiThread(); + + if (mController == null) { + mController = new CompositorController(this); + if (mCompositorReady) { + mController.onCompositorReady(); + } + } + return mController; + } + + /** + * Get a matrix for transforming from client coordinates to surface coordinates. + * + * @param matrix Matrix to be replaced by the transformation matrix. + * @see #getClientToScreenMatrix(Matrix) + * @see #getPageToSurfaceMatrix(Matrix) + */ + @UiThread + public void getClientToSurfaceMatrix(@NonNull final Matrix matrix) { + ThreadUtils.assertOnUiThread(); + + matrix.setScale(mViewportZoom, mViewportZoom); + if (mClientTop != mTop) { + matrix.postTranslate(0, mClientTop - mTop); + } + } + + /** + * Get a matrix for transforming from client coordinates to screen coordinates. The client + * coordinates are in CSS pixels and are relative to the viewport origin; their relation to screen + * coordinates does not depend on the current scroll position. + * + * @param matrix Matrix to be replaced by the transformation matrix. + * @see #getClientToSurfaceMatrix(Matrix) + * @see #getPageToScreenMatrix(Matrix) + */ + @UiThread + public void getClientToScreenMatrix(@NonNull final Matrix matrix) { + ThreadUtils.assertOnUiThread(); + + getClientToSurfaceMatrix(matrix); + matrix.postTranslate(mLeft, mTop); + } + + /** + * Get a matrix for transforming from page coordinates to screen coordinates. The page coordinates + * are in CSS pixels and are relative to the page origin; their relation to screen coordinates + * depends on the current scroll position of the outermost frame. + * + * @param matrix Matrix to be replaced by the transformation matrix. + * @see #getPageToSurfaceMatrix(Matrix) + * @see #getClientToScreenMatrix(Matrix) + */ + @UiThread + public void getPageToScreenMatrix(@NonNull final Matrix matrix) { + ThreadUtils.assertOnUiThread(); + + getPageToSurfaceMatrix(matrix); + matrix.postTranslate(mLeft, mTop); + } + + /** + * Get a matrix for transforming from page coordinates to surface coordinates. + * + * @param matrix Matrix to be replaced by the transformation matrix. + * @see #getPageToScreenMatrix(Matrix) + * @see #getClientToSurfaceMatrix(Matrix) + */ + @UiThread + public void getPageToSurfaceMatrix(@NonNull final Matrix matrix) { + ThreadUtils.assertOnUiThread(); + + getClientToSurfaceMatrix(matrix); + matrix.postTranslate(-mViewportLeft, -mViewportTop); + } + + /** + * Get a matrix for transforming from layout device client coordinates to screen coordinates. + * + * @param matrix Matrix to be replaced by the transformation matrix. + * @see #getClientToScreenMatrix(Matrix) + * @see #getPageToSurfaceMatrix(Matrix) + */ + @UiThread + /* package */ void getClientToScreenOffsetMatrix(@NonNull final Matrix matrix) { + ThreadUtils.assertOnUiThread(); + + matrix.postTranslate(mLeft, mTop); + } + + /** + * Get a matrix for transforming from screen coordinates to Android's current window coordinates. + * + * @param matrix Matrix to be replaced by the transformation matrix. + * @see + * https://developer.android.com/guide/topics/large-screens/multi-window-support#window_metrics + */ + @UiThread + /* package */ void getScreenToWindowManagerOffsetMatrix(@NonNull final Matrix matrix) { + ThreadUtils.assertOnUiThread(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + final WindowManager wm = + (WindowManager) + GeckoAppShell.getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + final Rect currentWindowRect = wm.getCurrentWindowMetrics().getBounds(); + matrix.postTranslate(-currentWindowRect.left, -currentWindowRect.top); + return; + } + + // TODO(m_kato): Bug 1678531 + // How to get window coordinate on Android 7-10 that supports split window? + } + + /** + * Get the bounds of the client area in client coordinates. The returned top-left coordinates are + * always (0, 0). Use the matrix from {@link #getClientToSurfaceMatrix(Matrix)} or {@link + * #getClientToScreenMatrix(Matrix)} to map these bounds to surface or screen coordinates, + * respectively. + * + * @param rect RectF to be replaced by the client bounds in client coordinates. + * @see #getSurfaceBounds(Rect) + */ + @UiThread + public void getClientBounds(@NonNull final RectF rect) { + ThreadUtils.assertOnUiThread(); + + rect.set(0.0f, 0.0f, (float) mWidth / mViewportZoom, (float) mClientHeight / mViewportZoom); + } + + /** + * Get the bounds of the client area in surface coordinates. This is equivalent to mapping the + * bounds returned by #getClientBounds(RectF) with the matrix returned by + * #getClientToSurfaceMatrix(Matrix). + * + * @param rect Rect to be replaced by the client bounds in surface coordinates. + */ + @UiThread + public void getSurfaceBounds(@NonNull final Rect rect) { + ThreadUtils.assertOnUiThread(); + + rect.set(0, mClientTop - mTop, mWidth, mHeight); + } + + /** + * GeckoSession applications implement this interface to handle requests for permissions from + * content, such as geolocation and notifications. For each permission, usually two requests are + * generated: one request for the Android app permission through requestAppPermissions, which is + * typically handled by a system permission dialog; and another request for the content permission + * (e.g. through requestContentPermission), which is typically handled by an app-specific + * permission dialog. + * + * <p>When denying an Android app permission, the response is not stored by GeckoView. It is the + * responsibility of the consumer to store the response state and therefore prevent further + * requests from being presented to the user. + */ + public interface PermissionDelegate { + /** + * Permission for using the geolocation API. See: + * https://developer.mozilla.org/en-US/docs/Web/API/Geolocation + */ + int PERMISSION_GEOLOCATION = 0; + + /** + * Permission for using the notifications API. See: + * https://developer.mozilla.org/en-US/docs/Web/API/notification + */ + int PERMISSION_DESKTOP_NOTIFICATION = 1; + + /** + * Permission for using the storage API. See: + * https://developer.mozilla.org/en-US/docs/Web/API/Storage_API + */ + int PERMISSION_PERSISTENT_STORAGE = 2; + + /** Permission for using the WebXR API. See: https://www.w3.org/TR/webxr */ + int PERMISSION_XR = 3; + + /** Permission for allowing autoplay of inaudible (silent) video. */ + int PERMISSION_AUTOPLAY_INAUDIBLE = 4; + + /** Permission for allowing autoplay of audible video. */ + int PERMISSION_AUTOPLAY_AUDIBLE = 5; + + /** Permission for accessing system media keys used to decode DRM media. */ + int PERMISSION_MEDIA_KEY_SYSTEM_ACCESS = 6; + + /** + * Permission for trackers to operate on the page -- disables all tracking protection features + * for a given site. + */ + int PERMISSION_TRACKING = 7; + + /** + * Permission for third party frames to access first party cookies and storage. May be granted + * heuristically in some cases. + */ + int PERMISSION_STORAGE_ACCESS = 8; + + /** + * Represents a content permission -- including the type of permission, the present value of the + * permission, the URL the permission pertains to, and other information. + */ + class ContentPermission { + @Retention(RetentionPolicy.SOURCE) + @IntDef({VALUE_PROMPT, VALUE_DENY, VALUE_ALLOW}) + public @interface Value {} + + /** The corresponding permission is currently set to default/prompt behavior. */ + public static final int VALUE_PROMPT = 3; + + /** The corresponding permission is currently set to deny. */ + public static final int VALUE_DENY = 2; + + /** The corresponding permission is currently set to allow. */ + public static final int VALUE_ALLOW = 1; + + /** The URI associated with this content permission. */ + public final @NonNull String uri; + + /** + * The third party origin associated with the request; currently only used for storage access + * permission. + */ + public final @Nullable String thirdPartyOrigin; + + /** + * A boolean indicating whether this content permission is associated with private browsing. + */ + public final boolean privateMode; + + /** The type of this permission; one of {@link #PERMISSION_GEOLOCATION PERMISSION_*}. */ + public final int permission; + + /** The value of the permission; one of {@link #VALUE_PROMPT VALUE_}. */ + public final @Value int value; + + /** + * The context ID associated with the permission if any. + * + * @see GeckoSessionSettings.Builder#contextId + */ + public final @Nullable String contextId; + + private final String mPrincipal; + + protected ContentPermission() { + this.uri = ""; + this.thirdPartyOrigin = null; + this.privateMode = false; + this.permission = PERMISSION_GEOLOCATION; + this.value = VALUE_ALLOW; + this.mPrincipal = ""; + this.contextId = null; + } + + private ContentPermission(final @NonNull GeckoBundle bundle) { + this.uri = bundle.getString("uri"); + this.mPrincipal = bundle.getString("principal"); + this.privateMode = bundle.getBoolean("privateMode"); + + final String permission = bundle.getString("perm"); + this.permission = convertType(permission); + if (permission.startsWith("3rdPartyStorage^")) { + // Storage access permissions are stored with the key "3rdPartyStorage^https://foo.com" + // where the third party origin is "https://foo.com". + this.thirdPartyOrigin = permission.substring(16); + } else { + this.thirdPartyOrigin = bundle.getString("thirdPartyOrigin"); + } + + this.value = bundle.getInt("value"); + this.contextId = + StorageController.retrieveUnsafeSessionContextId(bundle.getString("contextId")); + } + + /** + * Converts a JSONObject to a ContentPermission -- should only be used on the output of {@link + * #toJson()}. + * + * @param perm A JSONObject representing a ContentPermission, output by {@link #toJson()}. + * @return The corresponding ContentPermission. + */ + @AnyThread + public static @Nullable ContentPermission fromJson(final @NonNull JSONObject perm) { + ContentPermission res = null; + try { + res = new ContentPermission(GeckoBundle.fromJSONObject(perm)); + } catch (final JSONException e) { + Log.w(LOGTAG, "Failed to create ContentPermission; invalid JSONObject.", e); + } + return res; + } + + /** + * Converts a ContentPermission to a JSONObject that can be converted back to a + * ContentPermission by {@link #fromJson(JSONObject)}. + * + * @return A JSONObject representing this ContentPermission. Modifying any of the fields may + * result in undefined behavior when converted back to a ContentPermission and used. + * @throws JSONException if the conversion fails for any reason. + */ + @AnyThread + public @NonNull JSONObject toJson() throws JSONException { + return toGeckoBundle().toJSONObject(); + } + + private static int convertType(final @NonNull String type) { + if ("geolocation".equals(type)) { + return PERMISSION_GEOLOCATION; + } else if ("desktop-notification".equals(type)) { + return PERMISSION_DESKTOP_NOTIFICATION; + } else if ("persistent-storage".equals(type)) { + return PERMISSION_PERSISTENT_STORAGE; + } else if ("xr".equals(type)) { + return PERMISSION_XR; + } else if ("autoplay-media-inaudible".equals(type)) { + return PERMISSION_AUTOPLAY_INAUDIBLE; + } else if ("autoplay-media-audible".equals(type)) { + return PERMISSION_AUTOPLAY_AUDIBLE; + } else if ("media-key-system-access".equals(type)) { + return PERMISSION_MEDIA_KEY_SYSTEM_ACCESS; + } else if ("trackingprotection".equals(type) || "trackingprotection-pb".equals(type)) { + return PERMISSION_TRACKING; + } else if ("storage-access".equals(type) || type.startsWith("3rdPartyStorage^")) { + return PERMISSION_STORAGE_ACCESS; + } else { + return -1; + } + } + + // This also gets used in StorageController, so it's package rather than private. + /* package */ static String convertType(final int type, final boolean privateMode) { + switch (type) { + case PERMISSION_GEOLOCATION: + return "geolocation"; + case PERMISSION_DESKTOP_NOTIFICATION: + return "desktop-notification"; + case PERMISSION_PERSISTENT_STORAGE: + return "persistent-storage"; + case PERMISSION_XR: + return "xr"; + case PERMISSION_AUTOPLAY_INAUDIBLE: + return "autoplay-media-inaudible"; + case PERMISSION_AUTOPLAY_AUDIBLE: + return "autoplay-media-audible"; + case PERMISSION_MEDIA_KEY_SYSTEM_ACCESS: + return "media-key-system-access"; + case PERMISSION_TRACKING: + return privateMode ? "trackingprotection-pb" : "trackingprotection"; + case PERMISSION_STORAGE_ACCESS: + return "storage-access"; + default: + return ""; + } + } + + /* package */ static @NonNull ArrayList<ContentPermission> fromBundleArray( + final @NonNull GeckoBundle[] bundleArray) { + final ArrayList<ContentPermission> res = new ArrayList<ContentPermission>(); + if (bundleArray == null) { + return res; + } + + for (final GeckoBundle bundle : bundleArray) { + final ContentPermission temp = new ContentPermission(bundle); + if (temp.permission == -1 || temp.value < 1 || temp.value > 3) { + continue; + } + res.add(temp); + } + return res; + } + + /* package */ @NonNull + GeckoBundle toGeckoBundle() { + final GeckoBundle res = new GeckoBundle(7); + res.putString("uri", uri); + res.putString("thirdPartyOrigin", thirdPartyOrigin); + res.putString("principal", mPrincipal); + res.putBoolean("privateMode", privateMode); + res.putString("perm", convertType(permission, privateMode)); + res.putInt("value", value); + res.putString("contextId", contextId); + return res; + } + } + + /** Callback interface for notifying the result of a permission request. */ + interface Callback { + /** + * Called by the implementation after permissions are granted; the implementation must call + * either grant() or reject() for every request. + */ + @UiThread + default void grant() {} + + /** + * Called by the implementation when permissions are not granted; the implementation must call + * either grant() or reject() for every request. + */ + @UiThread + default void reject() {} + } + + /** + * Request Android app permissions. + * + * @param session GeckoSession instance requesting the permissions. + * @param permissions List of permissions to request; possible values are, + * android.Manifest.permission.ACCESS_COARSE_LOCATION + * android.Manifest.permission.ACCESS_FINE_LOCATION android.Manifest.permission.CAMERA + * android.Manifest.permission.RECORD_AUDIO + * @param callback Callback interface. + */ + @UiThread + default void onAndroidPermissionsRequest( + @NonNull final GeckoSession session, + @Nullable final String[] permissions, + @NonNull final Callback callback) { + callback.reject(); + } + + /** + * Request content permission. + * + * <p>Note, that in the case of PERMISSION_PERSISTENT_STORAGE, once permission has been granted + * for a site, it cannot be revoked. If the permission has previously been granted, it is the + * responsibility of the consuming app to remember the permission and prevent the prompt from + * being redisplayed to the user. + * + * @param session GeckoSession instance requesting the permission. + * @param perm An {@link ContentPermission} describing the permission being requested and its + * current status. + * @return A {@link GeckoResult} resolving to one of {@link ContentPermission#VALUE_PROMPT + * VALUE_*}, determining the response to the permission request and updating the permissions + * for this site. + */ + @UiThread + default @Nullable GeckoResult<Integer> onContentPermissionRequest( + @NonNull final GeckoSession session, @NonNull ContentPermission perm) { + return GeckoResult.fromValue(ContentPermission.VALUE_PROMPT); + } + + class MediaSource { + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + SOURCE_CAMERA, SOURCE_SCREEN, + SOURCE_MICROPHONE, SOURCE_AUDIOCAPTURE, + SOURCE_OTHER + }) + public @interface Source {} + + /** Constant to indicate that camera will be recorded. */ + public static final int SOURCE_CAMERA = 0; + + /** Constant to indicate that screen will be recorded. */ + public static final int SOURCE_SCREEN = 1; + + /** Constant to indicate that microphone will be recorded. */ + public static final int SOURCE_MICROPHONE = 2; + + /** Constant to indicate that device audio playback will be recorded. */ + public static final int SOURCE_AUDIOCAPTURE = 3; + + /** Constant to indicate a media source that does not fall under the other categories. */ + public static final int SOURCE_OTHER = 4; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_VIDEO, TYPE_AUDIO}) + public @interface Type {} + + /** The media type is video. */ + public static final int TYPE_VIDEO = 0; + + /** The media type is audio. */ + public static final int TYPE_AUDIO = 1; + + /** A string giving a unique source identifier. */ + public final @NonNull String id; + + /** + * A string giving the name of the video source from the system (for example, "Camera 0, + * Facing back, Orientation 90"). May be empty. + */ + public final @Nullable String name; + + /** + * An int indicating the media source type. Possible values for a video source are: + * SOURCE_CAMERA, SOURCE_SCREEN, and SOURCE_OTHER. Possible values for an audio source are: + * SOURCE_MICROPHONE, SOURCE_AUDIOCAPTURE, and SOURCE_OTHER. + */ + public final @Source int source; + + /** An int giving the type of media, must be either TYPE_VIDEO or TYPE_AUDIO. */ + public final @Type int type; + + private static @Source int getSourceFromString(final String src) { + // The strings here should match those in MediaSourceEnum in MediaStreamTrack.webidl + if ("camera".equals(src)) { + return SOURCE_CAMERA; + } else if ("screen".equals(src) || "window".equals(src) || "browser".equals(src)) { + return SOURCE_SCREEN; + } else if ("microphone".equals(src)) { + return SOURCE_MICROPHONE; + } else if ("audioCapture".equals(src)) { + return SOURCE_AUDIOCAPTURE; + } else if ("other".equals(src) || "application".equals(src)) { + return SOURCE_OTHER; + } else { + throw new IllegalArgumentException( + "String: " + src + " is not a valid media source string"); + } + } + + private static @Type int getTypeFromString(final String type) { + // The strings here should match the possible types in MediaDevice::MediaDevice in + // MediaManager.cpp + if ("videoinput".equals(type)) { + return TYPE_VIDEO; + } else if ("audioinput".equals(type)) { + return TYPE_AUDIO; + } else { + throw new IllegalArgumentException( + "String: " + type + " is not a valid media type string"); + } + } + + /* package */ MediaSource(final GeckoBundle media) { + id = media.getString("id"); + name = media.getString("name"); + source = getSourceFromString(media.getString("mediaSource")); + type = getTypeFromString(media.getString("type")); + } + + /** Empty constructor for tests. */ + protected MediaSource() { + id = null; + name = null; + source = SOURCE_CAMERA; + type = TYPE_VIDEO; + } + } + + /** + * Callback interface for notifying the result of a media permission request, including which + * media source(s) to use. + */ + interface MediaCallback { + /** + * Called by the implementation after permissions are granted; the implementation must call + * one of grant() or reject() for every request. + * + * @param video "id" value from the bundle for the video source to use, or null when video is + * not requested. + * @param audio "id" value from the bundle for the audio source to use, or null when audio is + * not requested. + */ + @UiThread + default void grant(final @Nullable String video, final @Nullable String audio) {} + + /** + * Called by the implementation after permissions are granted; the implementation must call + * one of grant() or reject() for every request. + * + * @param video MediaSource for the video source to use (must be an original MediaSource + * object that was passed to the implementation); or null when video is not requested. + * @param audio MediaSource for the audio source to use (must be an original MediaSource + * object that was passed to the implementation); or null when audio is not requested. + */ + @UiThread + default void grant(final @Nullable MediaSource video, final @Nullable MediaSource audio) {} + + /** + * Called by the implementation when permissions are not granted; the implementation must call + * one of grant() or reject() for every request. + */ + @UiThread + default void reject() {} + } + + /** + * Request content media permissions, including request for which video and/or audio source to + * use. + * + * <p>Media permissions will still be requested if the associated device permissions have been + * denied if there are video or audio sources in that category that can still be accessed. It is + * the responsibility of consumers to ensure that media permission requests are not displayed in + * this case. + * + * @param session GeckoSession instance requesting the permission. + * @param uri The URI of the content requesting the permission. + * @param video List of video sources, or null if not requesting video. + * @param audio List of audio sources, or null if not requesting audio. + * @param callback Callback interface. + */ + @UiThread + default void onMediaPermissionRequest( + @NonNull final GeckoSession session, + @NonNull final String uri, + @Nullable final MediaSource[] video, + @Nullable final MediaSource[] audio, + @NonNull final MediaCallback callback) { + callback.reject(); + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PermissionDelegate.PERMISSION_GEOLOCATION, + PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION, + PermissionDelegate.PERMISSION_PERSISTENT_STORAGE, + PermissionDelegate.PERMISSION_XR, + PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE, + PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE, + PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, + PermissionDelegate.PERMISSION_TRACKING, + PermissionDelegate.PERMISSION_STORAGE_ACCESS + }) + public @interface Permission {} + + /** + * Interface that SessionTextInput uses for performing operations such as opening and closing the + * software keyboard. If the delegate is not set, these operations are forwarded to the system + * {@link android.view.inputmethod.InputMethodManager} automatically. + */ + public interface TextInputDelegate { + /** Restarting input due to an input field gaining focus. */ + int RESTART_REASON_FOCUS = 0; + + /** Restarting input due to an input field losing focus. */ + int RESTART_REASON_BLUR = 1; + + /** + * Restarting input due to the content of the input field changing. For example, the input field + * type may have changed, or the current composition may have been committed outside of the + * input method. + */ + int RESTART_REASON_CONTENT_CHANGE = 2; + + /** + * Reset the input method, and discard any existing states such as the current composition or + * current autocompletion. Because the current focused editor may have changed, as part of the + * reset, a custom input method would normally call {@link + * SessionTextInput#onCreateInputConnection} to update its knowledge of the focused editor. Note + * that {@code restartInput} should be used to detect changes in focus, rather than {@link + * #showSoftInput} or {@link #hideSoftInput}, because focus changes are not always accompanied + * by requests to show or hide the soft input. This method is always called, even in viewless + * mode. + * + * @param session Session instance. + * @param reason Reason for the reset. + */ + @UiThread + default void restartInput( + @NonNull final GeckoSession session, @RestartReason final int reason) {} + + /** + * Display the soft input. May be called consecutively, even if the soft input is already shown. + * This method is always called, even in viewless mode. + * + * @param session Session instance. + * @see #hideSoftInput + */ + @UiThread + default void showSoftInput(@NonNull final GeckoSession session) {} + + /** + * Hide the soft input. May be called consecutively, even if the soft input is already hidden. + * This method is always called, even in viewless mode. + * + * @param session Session instance. + * @see #showSoftInput + */ + @UiThread + default void hideSoftInput(@NonNull final GeckoSession session) {} + + /** + * Update the soft input on the current selection. This method is <i>not</i> called in viewless + * mode. + * + * @param session Session instance. + * @param selStart Start offset of the selection. + * @param selEnd End offset of the selection. + * @param compositionStart Composition start offset, or -1 if there is no composition. + * @param compositionEnd Composition end offset, or -1 if there is no composition. + */ + @UiThread + default void updateSelection( + @NonNull final GeckoSession session, + final int selStart, + final int selEnd, + final int compositionStart, + final int compositionEnd) {} + + /** + * Update the soft input on the current extracted text, as requested through {@link + * android.view.inputmethod.InputConnection#getExtractedText}. Consequently, this method is + * <i>not</i> called in viewless mode. + * + * @param session Session instance. + * @param request The extract text request. + * @param text The extracted text. + */ + @UiThread + default void updateExtractedText( + @NonNull final GeckoSession session, + @NonNull final ExtractedTextRequest request, + @NonNull final ExtractedText text) {} + + /** + * Update the cursor-anchor information as requested through {@link + * android.view.inputmethod.InputConnection#requestCursorUpdates}. Consequently, this method is + * <i>not</i> called in viewless mode. + * + * @param session Session instance. + * @param info Cursor-anchor information. + */ + @UiThread + default void updateCursorAnchorInfo( + @NonNull final GeckoSession session, @NonNull final CursorAnchorInfo info) {} + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TextInputDelegate.RESTART_REASON_FOCUS, + TextInputDelegate.RESTART_REASON_BLUR, + TextInputDelegate.RESTART_REASON_CONTENT_CHANGE + }) + public @interface RestartReason {} + + /* package */ void onSurfaceChanged(final @NonNull SurfaceInfo surfaceInfo) { + ThreadUtils.assertOnUiThread(); + + mWidth = surfaceInfo.mWidth; + mHeight = surfaceInfo.mHeight; + mNewSurfaceProvider = surfaceInfo.mNewSurfaceProvider; + + if (mCompositorReady) { + mCompositor.syncResumeResizeCompositor( + surfaceInfo.mLeft, + surfaceInfo.mTop, + surfaceInfo.mWidth, + surfaceInfo.mHeight, + surfaceInfo.mSurface, + surfaceInfo.mSurfaceControl); + onWindowBoundsChanged(); + return; + } + + // We have a valid surface but we're not attached or the compositor + // is not ready; save the surface for later when we're ready. + mSurfaceInfo = surfaceInfo; + + // Adjust bounds as the last step. + onWindowBoundsChanged(); + } + + /* package */ void onSurfaceDestroyed() { + ThreadUtils.assertOnUiThread(); + + mNewSurfaceProvider = null; + + if (mCompositorReady) { + mCompositor.syncPauseCompositor(); + return; + } + + // While the surface was valid, we never became attached or the + // compositor never became ready; clear the saved surface. + mSurfaceInfo = null; + } + + /* package */ void onScreenOriginChanged(final int left, final int top) { + ThreadUtils.assertOnUiThread(); + + if (mLeft == left && mTop == top) { + return; + } + + mLeft = left; + mTop = top; + onWindowBoundsChanged(); + } + + /* package */ void setDynamicToolbarMaxHeight(final int height) { + if (mDynamicToolbarMaxHeight == height) { + return; + } + + if (mHeight != 0 && height != 0 && mHeight < height) { + Log.w( + LOGTAG, + new AssertionError( + "The maximum height of the dynamic toolbar (" + + height + + ") should be smaller than GeckoView height (" + + mHeight + + ")")); + } + + mDynamicToolbarMaxHeight = height; + + if (mAttachedCompositor) { + mCompositor.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight); + } + } + + /* package */ void setFixedBottomOffset(final int offset) { + if (mFixedBottomOffset == offset) { + return; + } + + mFixedBottomOffset = offset; + + if (mCompositorReady) { + mCompositor.setFixedBottomOffset(mFixedBottomOffset); + } + } + + /* package */ void onCompositorAttached() { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + mAttachedCompositor = true; + mCompositor.attachNPZC(mPanZoomController.mNative); + + if (mSurfaceInfo != null) { + // If we have a valid surface, create the compositor now that we're attached. + // Leave mSurface alone because we'll need it later for onCompositorReady. + onSurfaceChanged(mSurfaceInfo); + } + + mCompositor.sendToolbarAnimatorMessage(IS_COMPOSITOR_CONTROLLER_OPEN); + mCompositor.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight); + } + + /* package */ void onCompositorDetached() { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + if (mController != null) { + mController.onCompositorDetached(); + } + + mAttachedCompositor = false; + mCompositorReady = false; + } + + /* package */ void handleCompositorMessage(final int message) { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + switch (message) { + case COMPOSITOR_CONTROLLER_OPEN: + { + if (isCompositorReady()) { + return; + } + + // Delay calling onCompositorReady to avoid deadlock due + // to synchronous call to the compositor. + ThreadUtils.postToUiThread(this::onCompositorReady); + break; + } + + case FIRST_PAINT: + { + if (mController != null) { + mController.onFirstPaint(); + } + final ContentDelegate delegate = mContentHandler.getDelegate(); + if (delegate != null) { + delegate.onFirstComposite(this); + } + break; + } + + case LAYERS_UPDATED: + { + if (mController != null) { + mController.notifyDrawCallbacks(); + } + break; + } + + default: + { + Log.w(LOGTAG, "Unexpected message: " + message); + break; + } + } + } + + /* package */ boolean isCompositorReady() { + return mCompositorReady; + } + + /* package */ void onCompositorReady() { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + if (!mAttachedCompositor) { + return; + } + + mCompositorReady = true; + + if (mController != null) { + mController.onCompositorReady(); + } + + if (mSurfaceInfo != null) { + // If we have a valid surface, resume the + // compositor now that the compositor is ready. + onSurfaceChanged(mSurfaceInfo); + mSurfaceInfo = null; + } + + if (mFixedBottomOffset != 0) { + mCompositor.setFixedBottomOffset(mFixedBottomOffset); + } + } + + /* package */ void updateOverscrollVelocity(final float x, final float y) { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + if (mOverscroll == null) { + return; + } + + // Multiply the velocity by 1000 to match what was done in JPZ. + mOverscroll.setVelocity(x * 1000.0f, OverscrollEdgeEffect.AXIS_X); + mOverscroll.setVelocity(y * 1000.0f, OverscrollEdgeEffect.AXIS_Y); + } + + /* package */ void updateOverscrollOffset(final float x, final float y) { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + if (mOverscroll == null) { + return; + } + + mOverscroll.setDistance(x, OverscrollEdgeEffect.AXIS_X); + mOverscroll.setDistance(y, OverscrollEdgeEffect.AXIS_Y); + } + + /* package */ void onMetricsChanged(final float scrollX, final float scrollY, final float zoom) { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + mViewportLeft = scrollX; + mViewportTop = scrollY; + mViewportZoom = zoom; + } + + /* protected */ void onWindowBoundsChanged() { + if (DEBUG) { + ThreadUtils.assertOnUiThread(); + } + + if (mHeight != 0 && mDynamicToolbarMaxHeight != 0 && mHeight < mDynamicToolbarMaxHeight) { + Log.w( + LOGTAG, + new AssertionError( + "The maximum height of the dynamic toolbar (" + + mDynamicToolbarMaxHeight + + ") should be smaller than GeckoView height (" + + mHeight + + ")")); + } + + final int toolbarHeight = 0; + + mClientTop = mTop + toolbarHeight; + // If the view is not tall enough to even fix the toolbar we just + // default the client height to 0 + mClientHeight = Math.max(mHeight - toolbarHeight, 0); + + if (mAttachedCompositor) { + mCompositor.onBoundsChanged(mLeft, mClientTop, mWidth, mClientHeight); + } + + if (mOverscroll != null) { + mOverscroll.setSize(mWidth, mClientHeight); + } + } + + /* pacakge */ void onSafeAreaInsetsChanged( + final int top, final int right, final int bottom, final int left) { + ThreadUtils.assertOnUiThread(); + + if (mAttachedCompositor) { + mCompositor.onSafeAreaInsetsChanged(top, right, bottom, left); + } + } + + /* package */ void setPointerIcon( + final int defaultCursor, final @Nullable Bitmap customCursor, final float x, final float y) { + ThreadUtils.assertOnUiThread(); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return; + } + + final PointerIcon icon; + if (customCursor != null) { + try { + icon = PointerIcon.create(customCursor, x, y); + } catch (final IllegalArgumentException e) { + // x/y hotspot might be invalid + return; + } + } else { + final Context context = GeckoAppShell.getApplicationContext(); + icon = PointerIcon.getSystemIcon(context, defaultCursor); + } + + final ContentDelegate delegate = getContentDelegate(); + if (delegate != null) { + delegate.onPointerIconChange(this, icon); + } + } + + /** GeckoSession applications implement this interface to handle media events. */ + public interface MediaDelegate { + + class RecordingDevice { + + /* + * Default status flags for this RecordingDevice. + */ + public static class Status { + public static final long RECORDING = 0; + public static final long INACTIVE = 1 << 0; + + // Do not instantiate this class. + protected Status() {} + } + + /* + * Default device types for this RecordingDevice. + */ + public static class Type { + public static final long CAMERA = 0; + public static final long MICROPHONE = 1 << 0; + + // Do not instantiate this class. + protected Type() {} + } + + @Retention(RetentionPolicy.SOURCE) + @LongDef( + flag = true, + value = {Status.RECORDING, Status.INACTIVE}) + public @interface RecordingStatus {} + + @Retention(RetentionPolicy.SOURCE) + @LongDef( + flag = true, + value = {Type.CAMERA, Type.MICROPHONE}) + public @interface DeviceType {} + + /** + * A long giving the current recording status, must be either Status.RECORDING, Status.PAUSED + * or Status.INACTIVE. + */ + public final @RecordingStatus long status; + + /** + * A long giving the type of the recording device, must be either Type.CAMERA or + * Type.MICROPHONE. + */ + public final @DeviceType long type; + + private static @DeviceType long getTypeFromString(final String type) { + if ("microphone".equals(type)) { + return Type.MICROPHONE; + } else if ("camera".equals(type)) { + return Type.CAMERA; + } else { + throw new IllegalArgumentException( + "String: " + type + " is not a valid recording device string"); + } + } + + private static @RecordingStatus long getStatusFromString(final String type) { + if ("recording".equals(type)) { + return Status.RECORDING; + } else { + return Status.INACTIVE; + } + } + + /* package */ RecordingDevice(final GeckoBundle media) { + status = getStatusFromString(media.getString("status")); + type = getTypeFromString(media.getString("type")); + } + + /** Empty constructor for tests. */ + protected RecordingDevice() { + status = Status.INACTIVE; + type = Type.CAMERA; + } + } + + /** + * A recording device has changed state. Any change to the recording state of the devices + * microphone or camera will call this delegate method. The argument provides details of the + * active recording devices. + * + * @param session The session that the event has originated from. + * @param devices The list of active devices and their recording state. + */ + @UiThread + default void onRecordingStatusChanged( + @NonNull final GeckoSession session, @NonNull final RecordingDevice[] devices) {} + } + + /** An interface for recording new history visits and fetching the visited status for links. */ + public interface HistoryDelegate { + /** A representation of an entry in browser history. */ + public interface HistoryItem { + /** + * Get the URI of this history element. + * + * @return A String representing the URI of this history element. + */ + @AnyThread + default @NonNull String getUri() { + throw new UnsupportedOperationException("HistoryItem.getUri() called on invalid object."); + } + + /** + * Get the title of this history element. + * + * @return A String representing the title of this history element. + */ + @AnyThread + default @NonNull String getTitle() { + throw new UnsupportedOperationException( + "HistoryItem.getString() called on invalid object."); + } + } + + /** + * A representation of browser history, accessible as a `List`. The list itself and its entries + * are immutable; any attempt to mutate will result in an `UnsupportedOperationException`. + */ + public interface HistoryList extends List<HistoryItem> { + /** + * Get the current index in browser history. + * + * @return An int representing the current index in browser history. + */ + @AnyThread + default int getCurrentIndex() { + throw new UnsupportedOperationException( + "HistoryList.getCurrentIndex() called on invalid object."); + } + } + + // These flags are similar to those in `IHistory::LoadFlags`, but we use + // different values to decouple GeckoView from Gecko changes. These + // should be kept in sync with `GeckoViewHistory::GeckoViewVisitFlags`. + + /** The URL was visited a top-level window. */ + final int VISIT_TOP_LEVEL = 1 << 0; + + /** The URL is the target of a temporary redirect. */ + final int VISIT_REDIRECT_TEMPORARY = 1 << 1; + + /** The URL is the target of a permanent redirect. */ + final int VISIT_REDIRECT_PERMANENT = 1 << 2; + + /** The URL is temporarily redirected to another URL. */ + final int VISIT_REDIRECT_SOURCE = 1 << 3; + + /** The URL is permanently redirected to another URL. */ + final int VISIT_REDIRECT_SOURCE_PERMANENT = 1 << 4; + + /** The URL failed to load due to a client or server error. */ + final int VISIT_UNRECOVERABLE_ERROR = 1 << 5; + + /** + * Records a visit to a page. + * + * @param session The session where the URL was visited. + * @param url The visited URL. + * @param lastVisitedURL The last visited URL in this session, to detect redirects and reloads. + * @param flags Additional flags for this visit, including redirect and error statuses. This is + * a bitmask of one or more {@link #VISIT_TOP_LEVEL VISIT_*} flags, OR-ed together. + * @return A {@link GeckoResult} completed with a boolean indicating whether to highlight links + * for the new URL as visited ({@code true}) or unvisited ({@code false}). + */ + @UiThread + default @Nullable GeckoResult<Boolean> onVisited( + @NonNull final GeckoSession session, + @NonNull final String url, + @Nullable final String lastVisitedURL, + @VisitFlags final int flags) { + return null; + } + + /** + * Returns the visited statuses for links on a page. This is used to highlight links as visited + * or unvisited, for example. + * + * @param session The session requesting the visited statuses. + * @param urls A list of URLs to check. + * @return A {@link GeckoResult} completed with a list of booleans corresponding to the URLs in + * {@code urls}, and indicating whether to highlight links for each URL as visited ({@code + * true}) or unvisited ({@code false}). + */ + @UiThread + default @Nullable GeckoResult<boolean[]> getVisited( + @NonNull final GeckoSession session, @NonNull final String[] urls) { + return null; + } + + @UiThread + @SuppressWarnings("checkstyle:javadocmethod") + default void onHistoryStateChange( + @NonNull final GeckoSession session, @NonNull final HistoryList historyList) {} + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + HistoryDelegate.VISIT_TOP_LEVEL, + HistoryDelegate.VISIT_REDIRECT_TEMPORARY, + HistoryDelegate.VISIT_REDIRECT_PERMANENT, + HistoryDelegate.VISIT_REDIRECT_SOURCE, + HistoryDelegate.VISIT_REDIRECT_SOURCE_PERMANENT, + HistoryDelegate.VISIT_UNRECOVERABLE_ERROR + }) + public @interface VisitFlags {} + + private Autofill.Support getAutofillSupport() { + return mAutofillSupport; + } + + /** + * Sets the autofill delegate for this session. + * + * @param delegate An instance of {@link Autofill.Delegate}. + */ + @UiThread + public void setAutofillDelegate(final @Nullable Autofill.Delegate delegate) { + getAutofillSupport().setDelegate(delegate); + } + + /** + * @return The current {@link Autofill.Delegate} for this session, if any. + */ + @UiThread + public @Nullable Autofill.Delegate getAutofillDelegate() { + return getAutofillSupport().getDelegate(); + } + + /** + * Provides an autofill structure similar to {@link + * View#onProvideAutofillVirtualStructure(ViewStructure, int)} , but does not rely on {@link + * ViewStructure} to build the tree. This is useful for apps that want to provide autofill + * functionality without using the Android autofill system or requiring API 26. + * + * @return The elements available for autofill. + */ + @UiThread + public @NonNull Autofill.Session getAutofillSession() { + return getAutofillSupport().getAutofillSession(); + } + + /** + * Saves a PDF of the currently displayed page. + * + * @return A GeckoResult with an InputStream containing the PDF. The result could + * CompleteExceptionally with a {@link GeckoPrintException}s, if there are any issues while + * generating the PDF. + */ + @AnyThread + public @NonNull GeckoResult<InputStream> saveAsPdf() { + return saveAsPdfByBrowsingContext(null); + } + + /** + * Saves a PDF of the specified browsing context. Use null if the browsing context is unknown or + * to print the main page. + * + * @param browsingContextId the browsing context id of the item to print + * @return A GeckoResult with an InputStream containing the PDF. + */ + @AnyThread + private @NonNull GeckoResult<InputStream> saveAsPdfByBrowsingContext( + final @Nullable Long browsingContextId) { + final GeckoResult<InputStream> geckoResult = new GeckoResult<>(); + final GeckoSession self = this; + this.isPdfJs() + .then( + new GeckoResult.OnValueListener<Boolean, Void>() { + @Override + public GeckoResult<Void> onValue(final Boolean isPdfJs) { + if (!isPdfJs) { + if (browsingContextId == null) { + self.mWindow.printToPdf(geckoResult); + } else { + self.mWindow.printToPdf(geckoResult, browsingContextId); + } + } else { + geckoResult.completeFrom( + self.getPdfFileSaver().save().map(result -> result.body)); + } + return null; + } + }); + + return geckoResult; + } + + /** Prints the currently displayed page. */ + @AnyThread + public void printPageContent() { + final PrintDelegate delegate = getPrintDelegate(); + if (delegate != null) { + delegate.onPrint(this); + } else { + Log.w(LOGTAG, "Print delegate required for printing."); + } + } + + private static String rgbaToArgb(final String color) { + // We expect #rrggbbaa + if (color.length() != 9 || !color.startsWith("#")) { + throw new IllegalArgumentException("Invalid color format"); + } + + return "#" + color.substring(7) + color.substring(1, 7); + } + + private static void fixupManifestColor(final JSONObject manifest, final String name) + throws JSONException { + if (manifest.isNull(name)) { + return; + } + + manifest.put(name, rgbaToArgb(manifest.getString(name))); + } + + private static JSONObject fixupWebAppManifest(final JSONObject manifest) { + // Colors are #rrggbbaa, but we want them to be #aarrggbb, since that's what + // android.graphics.Color expects. + try { + fixupManifestColor(manifest, "theme_color"); + fixupManifestColor(manifest, "background_color"); + } catch (final JSONException e) { + Log.w(LOGTAG, "Failed to fixup web app manifest", e); + } + + return manifest; + } + + private static boolean maybeCheckDataUriLength(final @NonNull Loader request) { + if (!request.mIsDataUri) { + return true; + } + + return request.mUri.length() <= DATA_URI_MAX_LENGTH; + } + + /** + * Used for printing page content. + * + * <p>The provided implementation is in {@link GeckoView}. It uses a PDF of the content and the + * Android print API to print the page. + */ + @AnyThread + public interface PrintDelegate { + /** + * Print the current page content. + * + * @param session to print + */ + default void onPrint(@NonNull final GeckoSession session) {} + + /** + * Print any provided PDF InputStream. + * + * @param pdfInputStream an InputStream containing a PDF + */ + default void onPrint(@NonNull final InputStream pdfInputStream) {} + + /** + * Print any provided PDF InputStream. + * + * @param pdfInputStream an InputStream containing a PDF + * @return A GeckoResult if the print dialog has closed + */ + default @Nullable GeckoResult<Boolean> onPrintWithStatus( + @NonNull final InputStream pdfInputStream) { + return null; + } + } + + /** + * Gets the print delegate for this session. + * + * @return The current {@link PrintDelegate} for this session, if any. + */ + @AnyThread + public @Nullable PrintDelegate getPrintDelegate() { + return mPrintHandler.getDelegate(); + } + + /** + * Sets the print delegate for this session. + * + * @param delegate An instance of {@link PrintDelegate}. + */ + @AnyThread + public void setPrintDelegate(final @Nullable PrintDelegate delegate) { + mPrintHandler.setDelegate(delegate, this); + } + + /** Thrown when failure occurs when printing from a website. */ + @WrapForJNI + public static class GeckoPrintException extends Exception { + /** The print service was not available. */ + public static final int ERROR_PRINT_SETTINGS_SERVICE_NOT_AVAILABLE = -1; + + /** The print service was not created due to an initialization error. */ + public static final int ERROR_UNABLE_TO_CREATE_PRINT_SETTINGS = -2; + + /** An error happened while trying to find the canonical browing context */ + public static final int ERROR_UNABLE_TO_RETRIEVE_CANONICAL_BROWSING_CONTEXT = -3; + + /** An error happened while trying to find the activity context delegate */ + public static final int ERROR_NO_ACTIVITY_CONTEXT_DELEGATE = -4; + + /** An error happened while trying to find the activity context */ + public static final int ERROR_NO_ACTIVITY_CONTEXT = -5; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + value = { + ERROR_PRINT_SETTINGS_SERVICE_NOT_AVAILABLE, + ERROR_UNABLE_TO_CREATE_PRINT_SETTINGS, + ERROR_UNABLE_TO_RETRIEVE_CANONICAL_BROWSING_CONTEXT, + ERROR_NO_ACTIVITY_CONTEXT_DELEGATE, + ERROR_NO_ACTIVITY_CONTEXT + }) + public @interface Codes {} + + /** One of {@link Codes} that provides more information about this exception. */ + public final @Codes int code; + + @Override + public String toString() { + return "GeckoPrintException: " + code; + } + + /* package */ GeckoPrintException(final @Codes int code) { + this.code = code; + } + + /** For testing. */ + protected GeckoPrintException() { + code = ERROR_PRINT_SETTINGS_SERVICE_NOT_AVAILABLE; + } + } +} |