1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * vim: ts=4 sw=4 expandtab:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 package org.mozilla.geckoview;
8 
9 import java.io.ByteArrayInputStream;
10 import java.lang.annotation.Retention;
11 import java.lang.annotation.RetentionPolicy;
12 import java.lang.ref.WeakReference;
13 import java.nio.ByteBuffer;
14 import java.security.cert.CertificateException;
15 import java.security.cert.CertificateFactory;
16 import java.security.cert.X509Certificate;
17 import java.util.AbstractSequentialList;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.HashSet;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.ListIterator;
25 import java.util.Map;
26 import java.util.NoSuchElementException;
27 import java.util.Set;
28 import java.util.UUID;
29 
30 import org.json.JSONException;
31 import org.json.JSONObject;
32 import org.mozilla.gecko.annotation.WrapForJNI;
33 import org.mozilla.gecko.EventDispatcher;
34 import org.mozilla.gecko.GeckoThread;
35 import org.mozilla.gecko.IGeckoEditableParent;
36 import org.mozilla.gecko.mozglue.JNIObject;
37 import org.mozilla.gecko.NativeQueue;
38 import org.mozilla.gecko.util.BundleEventListener;
39 import org.mozilla.gecko.util.EventCallback;
40 import org.mozilla.gecko.util.GeckoBundle;
41 import org.mozilla.gecko.util.IntentUtils;
42 import org.mozilla.gecko.util.ThreadUtils;
43 
44 import android.content.ContentResolver;
45 import android.content.Context;
46 import android.database.Cursor;
47 import android.graphics.Matrix;
48 import android.graphics.Rect;
49 import android.graphics.RectF;
50 import android.net.Uri;
51 import android.os.Binder;
52 import android.os.IBinder;
53 import android.os.IInterface;
54 import android.os.Parcel;
55 import android.os.Parcelable;
56 import android.os.SystemClock;
57 import android.support.annotation.AnyThread;
58 import android.support.annotation.IntDef;
59 import android.support.annotation.LongDef;
60 import android.support.annotation.Nullable;
61 import android.support.annotation.NonNull;
62 import android.support.annotation.StringDef;
63 import android.support.annotation.UiThread;
64 import android.text.TextUtils;
65 import android.util.Base64;
66 import android.util.Log;
67 import android.util.LongSparseArray;
68 import android.util.SparseArray;
69 import android.view.Surface;
70 import android.view.inputmethod.CursorAnchorInfo;
71 import android.view.inputmethod.ExtractedText;
72 import android.view.inputmethod.ExtractedTextRequest;
73 import android.view.View;
74 import android.view.ViewStructure;
75 
76 public class GeckoSession implements Parcelable {
77     private static final String LOGTAG = "GeckoSession";
78     private static final boolean DEBUG = false;
79 
80     // Type of changes given to onWindowChanged.
81     // Window has been cleared due to the session being closed.
82     private static final int WINDOW_CLOSE = 0;
83     // Window has been set due to the session being opened.
84     private static final int WINDOW_OPEN = 1; // Window has been opened.
85     // Window has been cleared due to the session being transferred to another session.
86     private static final int WINDOW_TRANSFER_OUT = 2; // Window has been transfer.
87     // Window has been set due to another session being transferred to this one.
88     private static final int WINDOW_TRANSFER_IN = 3;
89 
90     private enum State implements NativeQueue.State {
91         INITIAL(0),
92         READY(1);
93 
94         private final int mRank;
95 
State(final int rank)96         private State(final int rank) {
97             mRank = rank;
98         }
99 
100         @Override
is(final NativeQueue.State other)101         public boolean is(final NativeQueue.State other) {
102             return this == other;
103         }
104 
105         @Override
isAtLeast(final NativeQueue.State other)106         public boolean isAtLeast(final NativeQueue.State other) {
107             return (other instanceof State) &&
108                    mRank >= ((State) other).mRank;
109         }
110     }
111 
112     private final NativeQueue mNativeQueue =
113         new NativeQueue(State.INITIAL, State.READY);
114 
115     private final EventDispatcher mEventDispatcher =
116         new EventDispatcher(mNativeQueue);
117 
118     private final SessionTextInput mTextInput = new SessionTextInput(this, mNativeQueue);
119     private SessionAccessibility mAccessibility;
120     private SessionFinder mFinder;
121 
122     private String mId = UUID.randomUUID().toString().replace("-", "");
getId()123     /* package */ String getId() {
124         return mId;
125     }
126 
127     private boolean mShouldPinOnScreen;
128 
129     // All fields are accessed on UI thread only.
130     private PanZoomController mPanZoomController = new PanZoomController(this);
131     private OverscrollEdgeEffect mOverscroll;
132     private CompositorController mController;
133     private Autofill.Support mAutofillSupport;
134 
135     private boolean mAttachedCompositor;
136     private boolean mCompositorReady;
137     private Surface mSurface;
138 
139     // All fields of coordinates are in screen units.
140     private int mLeft;
141     private int mTop; // Top of the surface (including toolbar);
142     private int mClientTop; // Top of the client area (i.e. excluding toolbar);
143     private int mOffsetX;
144     private int mOffsetY;
145     private int mWidth;
146     private int mHeight; // Height of the surface (including toolbar);
147     private int mClientHeight; // Height of the client area (i.e. excluding toolbar);
148     private int mFixedBottomOffset = 0; // The margin for fixed elements attached to the bottom of the viewport.
149     private int mDynamicToolbarMaxHeight = 0; // The maximum height of the dynamic toolbar
150     private float mViewportLeft;
151     private float mViewportTop;
152     private float mViewportZoom = 1.0f;
153 
154     //
155     // NOTE: These values are also defined in
156     // gfx/layers/ipc/UiCompositorControllerMessageTypes.h and must be kept in sync. Any
157     // new AnimatorMessageType added here must also be added there.
158     //
159     // Sent from compositor after first paint
160     /* package */ final static int FIRST_PAINT                      = 0;
161     // Sent from compositor when a layer has been updated
162     /* package */ final static int LAYERS_UPDATED                   = 1;
163     // Special message sent from UiCompositorControllerChild once it is open
164     /* package */ final static int COMPOSITOR_CONTROLLER_OPEN       = 2;
165     // Special message sent from controller to query if the compositor controller is open.
166     /* package */ final static int IS_COMPOSITOR_CONTROLLER_OPEN    = 3;
167 
168     /* protected */ class Compositor extends JNIObject {
isReady()169         public boolean isReady() {
170             return GeckoSession.this.isCompositorReady();
171         }
172 
173         @WrapForJNI(calledFrom = "ui")
onCompositorAttached()174         private void onCompositorAttached() {
175             GeckoSession.this.onCompositorAttached();
176         }
177 
178         @WrapForJNI(calledFrom = "ui")
onCompositorDetached()179         private void onCompositorDetached() {
180             // Clear out any pending calls on the UI thread.
181             GeckoSession.this.onCompositorDetached();
182         }
183 
184         @WrapForJNI(dispatchTo = "gecko")
disposeNative()185         @Override protected native void disposeNative();
186 
187         @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
attachNPZC(PanZoomController.NativeProvider npzc)188         public native void attachNPZC(PanZoomController.NativeProvider npzc);
189 
190         @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
onBoundsChanged(int left, int top, int width, int height)191         public native void onBoundsChanged(int left, int top, int width, int height);
192 
193         @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
setDynamicToolbarMaxHeight(int height)194         public native void setDynamicToolbarMaxHeight(int height);
195 
196         // Gecko thread pauses compositor; blocks UI thread.
197         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
syncPauseCompositor()198         public native void syncPauseCompositor();
199 
200         // UI thread resumes compositor and notifies Gecko thread; does not block UI thread.
201         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
syncResumeResizeCompositor(int x, int y, int width, int height, Object surface)202         public native void syncResumeResizeCompositor(int x, int y, int width, int height, Object surface);
203 
204         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
setMaxToolbarHeight(int height)205         public native void setMaxToolbarHeight(int height);
206 
207         @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
setFixedBottomOffset(int offset)208         public native void setFixedBottomOffset(int offset);
209 
210         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
sendToolbarAnimatorMessage(int message)211         public native void sendToolbarAnimatorMessage(int message);
212 
213         @WrapForJNI(calledFrom = "ui")
recvToolbarAnimatorMessage(final int message)214         private void recvToolbarAnimatorMessage(final int message) {
215             GeckoSession.this.handleCompositorMessage(message);
216         }
217 
218         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
setDefaultClearColor(int color)219         public native void setDefaultClearColor(int color);
220 
221         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
requestScreenPixels(final GeckoResult<ByteBuffer> result, final int x, final int y, final int srcWidth, final int srcHeight, final int outWidth, final int outHeight)222         /* package */ native void requestScreenPixels(final GeckoResult<ByteBuffer> result,
223                                                       final int x, final int y,
224                                                       final int srcWidth, final int srcHeight,
225                                                       final int outWidth, final int outHeight);
226 
227         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
enableLayerUpdateNotifications(boolean enable)228         public native void enableLayerUpdateNotifications(boolean enable);
229 
230         // The compositor invokes this function just before compositing a frame where the
231         // document is different from the document composited on the last frame. In these
232         // cases, the viewport information we have in Java is no longer valid and needs to
233         // be replaced with the new viewport information provided.
234         @WrapForJNI(calledFrom = "ui")
updateRootFrameMetrics(final float scrollX, final float scrollY, final float zoom)235         private void updateRootFrameMetrics(final float scrollX, final float scrollY,
236                                             final float zoom) {
237             GeckoSession.this.onMetricsChanged(scrollX, scrollY, zoom);
238         }
239 
240         @WrapForJNI(calledFrom = "ui")
updateOverscrollVelocity(final float x, final float y)241         private void updateOverscrollVelocity(final float x, final float y) {
242             GeckoSession.this.updateOverscrollVelocity(x, y);
243         }
244 
245         @WrapForJNI(calledFrom = "ui")
updateOverscrollOffset(final float x, final float y)246         private void updateOverscrollOffset(final float x, final float y) {
247             GeckoSession.this.updateOverscrollOffset(x, y);
248         }
249 
250         @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
onSafeAreaInsetsChanged(int top, int right, int bottom, int left)251         public native void onSafeAreaInsetsChanged(int top, int right, int bottom, int left);
252 
253         @Override
finalize()254         protected void finalize() throws Throwable {
255             disposeNative();
256         }
257     }
258 
259     /* package */ final Compositor mCompositor = new Compositor();
260 
261     @WrapForJNI(stubName = "GetCompositor", calledFrom = "ui")
getCompositorFromNative()262     private Object getCompositorFromNative() {
263         // Only used by native code.
264         return mCompositorReady ? mCompositor : null;
265     }
266 
267     private final GeckoSessionHandler<HistoryDelegate> mHistoryHandler =
268         new GeckoSessionHandler<HistoryDelegate>(
269             "GeckoViewHistory", this,
270             new String[]{
271                 "GeckoView:OnVisited",
272                 "GeckoView:GetVisited",
273                 "GeckoView:StateUpdated",
274             }
275         ) {
276             @Override
277             public void handleMessage(final HistoryDelegate delegate,
278                                       final String event,
279                                       final GeckoBundle message,
280                                       final EventCallback callback) {
281                 if ("GeckoView:OnVisited".equals(event)) {
282                     final GeckoResult<Boolean> result =
283                         delegate.onVisited(GeckoSession.this, message.getString("url"),
284                                            message.getString("lastVisitedURL"),
285                                            message.getInt("flags"));
286 
287                     if (result == null) {
288                         callback.sendSuccess(false);
289                         return;
290                     }
291 
292                     result.accept(
293                         visited -> callback.sendSuccess(visited.booleanValue()),
294                         exception -> callback.sendSuccess(false));
295                 } else if ("GeckoView:GetVisited".equals(event)) {
296                     final String[] urls = message.getStringArray("urls");
297 
298                     final GeckoResult<boolean[]> result =
299                         delegate.getVisited(GeckoSession.this, urls);
300 
301                     if (result == null) {
302                         callback.sendSuccess(null);
303                         return;
304                     }
305 
306                     result.accept(
307                         visited -> callback.sendSuccess(visited),
308                         exception -> callback.sendError("Failed to fetch visited statuses for URIs"));
309                 } else if ("GeckoView:StateUpdated".equals(event)) {
310 
311                     final GeckoBundle update = message.getBundle("data");
312 
313                     if (update == null) {
314                         return;
315                     }
316                     final int previousHistorySize = mStateCache.size();
317                     mStateCache.updateSessionState(update);
318 
319                     ProgressDelegate progressDelegate = getProgressDelegate();
320                     if (progressDelegate != null) {
321                         progressDelegate.onSessionStateChange(GeckoSession.this, new SessionState(mStateCache));
322                     }
323 
324                     if (update.getBundle("historychange") != null) {
325                         final SessionState state = new SessionState(mStateCache);
326 
327                         delegate.onHistoryStateChange(GeckoSession.this, state);
328 
329                         // If the previous history was larger than one entry and the new size is one, it means the
330                         // History has been purged and the navigation delegate needs to be update.
331                         if ((previousHistorySize > 1) && (state.size() == 1) && mNavigationHandler.getDelegate() != null) {
332                             mNavigationHandler.getDelegate().onCanGoForward(GeckoSession.this, false);
333                             mNavigationHandler.getDelegate().onCanGoBack(GeckoSession.this, false);
334                         }
335                     }
336                 }
337             }
338         };
339 
340     private final WebExtension.SessionController mWebExtensionController;
341 
342     private final GeckoSessionHandler<ContentDelegate> mContentHandler =
343         new GeckoSessionHandler<ContentDelegate>(
344             "GeckoViewContent", this,
345             new String[]{
346                 "GeckoView:ContentCrash",
347                 "GeckoView:ContentKill",
348                 "GeckoView:ContextMenu",
349                 "GeckoView:DOMMetaViewportFit",
350                 "GeckoView:DOMTitleChanged",
351                 "GeckoView:DOMWindowClose",
352                 "GeckoView:ExternalResponse",
353                 "GeckoView:FocusRequest",
354                 "GeckoView:FullScreenEnter",
355                 "GeckoView:FullScreenExit",
356                 "GeckoView:WebAppManifest",
357                 "GeckoView:FirstContentfulPaint",
358             }
359         ) {
360             @Override
361             public void handleMessage(final ContentDelegate delegate,
362                                       final String event,
363                                       final GeckoBundle message,
364                                       final EventCallback callback) {
365                 if ("GeckoView:ContentCrash".equals(event)) {
366                     close();
367                     delegate.onCrash(GeckoSession.this);
368                 } else if ("GeckoView:ContentKill".equals(event)) {
369                     close();
370                     delegate.onKill(GeckoSession.this);
371                 } else if ("GeckoView:ContextMenu".equals(event)) {
372                     final ContentDelegate.ContextElement elem =
373                         new ContentDelegate.ContextElement(
374                             message.getString("baseUri"),
375                             message.getString("uri"),
376                             message.getString("title"),
377                             message.getString("alt"),
378                             message.getString("elementType"),
379                             message.getString("elementSrc"));
380 
381                     delegate.onContextMenu(GeckoSession.this,
382                                            message.getInt("screenX"),
383                                            message.getInt("screenY"),
384                                            elem);
385 
386                 } else if ("GeckoView:DOMMetaViewportFit".equals(event)) {
387                     delegate.onMetaViewportFitChange(GeckoSession.this,
388                                                      message.getString("viewportfit"));
389                 } else if ("GeckoView:DOMTitleChanged".equals(event)) {
390                     delegate.onTitleChange(GeckoSession.this,
391                                            message.getString("title"));
392                 } else if ("GeckoView:FocusRequest".equals(event)) {
393                     delegate.onFocusRequest(GeckoSession.this);
394                 } else if ("GeckoView:DOMWindowClose".equals(event)) {
395                     delegate.onCloseRequest(GeckoSession.this);
396                 } else if ("GeckoView:FullScreenEnter".equals(event)) {
397                     delegate.onFullScreen(GeckoSession.this, true);
398                 } else if ("GeckoView:FullScreenExit".equals(event)) {
399                     delegate.onFullScreen(GeckoSession.this, false);
400                 } else if ("GeckoView:ExternalResponse".equals(event)) {
401                     delegate.onExternalResponse(GeckoSession.this, new WebResponseInfo(message));
402                 } else if ("GeckoView:WebAppManifest".equals(event)) {
403                     final GeckoBundle manifest = message.getBundle("manifest");
404                     if (manifest == null) {
405                         return;
406                     }
407 
408                     try {
409                         delegate.onWebAppManifest(GeckoSession.this, fixupWebAppManifest(manifest.toJSONObject()));
410                     } catch (JSONException e) {
411                         Log.e(LOGTAG, "Failed to convert web app manifest to JSON", e);
412                     }
413                 } else if ("GeckoView:FirstContentfulPaint".equals(event)) {
414                     delegate.onFirstContentfulPaint(GeckoSession.this);
415                 }
416             }
417         };
418 
419     private final GeckoSessionHandler<NavigationDelegate> mNavigationHandler =
420         new GeckoSessionHandler<NavigationDelegate>(
421             "GeckoViewNavigation", this,
422             new String[]{
423                 "GeckoView:LocationChange",
424                 "GeckoView:OnNewSession"
425             },
426             new String[] {
427                 "GeckoView:OnLoadError",
428                 "GeckoView:OnLoadRequest",
429             }
430         ) {
431             // This needs to match nsIBrowserDOMWindow.idl
432             private int convertGeckoTarget(final int geckoTarget) {
433                 switch (geckoTarget) {
434                     case 0: // OPEN_DEFAULTWINDOW
435                     case 1: // OPEN_CURRENTWINDOW
436                         return NavigationDelegate.TARGET_WINDOW_CURRENT;
437                     default: // OPEN_NEWWINDOW, OPEN_NEWTAB, OPEN_SWITCHTAB
438                         return NavigationDelegate.TARGET_WINDOW_NEW;
439                 }
440             }
441 
442             @Override
443             public void handleDefaultMessage(final String event,
444                                              final GeckoBundle message,
445                                              final EventCallback callback) {
446 
447                 if ("GeckoView:OnLoadRequest".equals(event)) {
448                     callback.sendSuccess(false);
449                 } else if ("GeckoView:OnLoadError".equals(event)) {
450                     callback.sendSuccess(null);
451                 } else {
452                     super.handleDefaultMessage(event, message, callback);
453                 }
454             }
455 
456             @Override
457             public void handleMessage(final NavigationDelegate delegate,
458                                       final String event,
459                                       final GeckoBundle message,
460                                       final EventCallback callback) {
461                 Log.d(LOGTAG, "handleMessage " + event + " uri=" + message.getString("uri"));
462                 if ("GeckoView:LocationChange".equals(event)) {
463                     if (message.getBoolean("isTopLevel")) {
464                         delegate.onLocationChange(GeckoSession.this,
465                                                   message.getString("uri"));
466                     }
467                     delegate.onCanGoBack(GeckoSession.this,
468                                          message.getBoolean("canGoBack"));
469                     delegate.onCanGoForward(GeckoSession.this,
470                                             message.getBoolean("canGoForward"));
471                 } else if ("GeckoView:OnLoadRequest".equals(event)) {
472                     final NavigationDelegate.LoadRequest request =
473                         new NavigationDelegate.LoadRequest(
474                               message.getString("uri"),
475                               message.getString("triggerUri"),
476                               message.getInt("where"),
477                               message.getInt("flags"),
478                               message.getBoolean("hasUserGesture"),
479                               /* isDirectNavigation */ false);
480 
481                     if (!IntentUtils.isUriSafeForScheme(request.uri)) {
482                         callback.sendError("Blocked unsafe intent URI");
483 
484                         delegate.onLoadError(GeckoSession.this, request.uri,
485                                              new WebRequestError(WebRequestError.ERROR_MALFORMED_URI,
486                                                                  WebRequestError.ERROR_CATEGORY_URI,
487                                                                  null));
488 
489                         return;
490                     }
491 
492                     final GeckoResult<AllowOrDeny> result =
493                         delegate.onLoadRequest(GeckoSession.this, request);
494 
495                     if (result == null) {
496                         callback.sendSuccess(null);
497                         return;
498                     }
499 
500                     result.accept(value -> {
501                         ThreadUtils.assertOnUiThread();
502                         if (value == AllowOrDeny.ALLOW) {
503                             callback.sendSuccess(false);
504                         } else  if (value == AllowOrDeny.DENY) {
505                             callback.sendSuccess(true);
506                         } else {
507                             callback.sendError("Invalid response");
508                         }
509                     }, exception -> callback.sendError(exception.getMessage()));
510                 } else if ("GeckoView:OnLoadError".equals(event)) {
511                     final String uri = message.getString("uri");
512                     final long errorCode = message.getLong("error");
513                     final int errorModule = message.getInt("errorModule");
514                     final int errorClass = message.getInt("errorClass");
515 
516                     final WebRequestError err = WebRequestError.fromGeckoError(errorCode, errorModule, errorClass, null);
517 
518                     final GeckoResult<String> result = delegate.onLoadError(GeckoSession.this, uri, err);
519                     if (result == null) {
520                         callback.sendError("abort");
521                         return;
522                     }
523 
524                     result.accept(url -> {
525                         if (url == null) {
526                             callback.sendError("abort");
527                         } else {
528                             callback.sendSuccess(url);
529                         }
530                     }, exception -> callback.sendError(exception.getMessage()));
531                 } else if ("GeckoView:OnNewSession".equals(event)) {
532                     final String uri = message.getString("uri");
533                     final GeckoResult<GeckoSession> result = delegate.onNewSession(GeckoSession.this, uri);
534                     if (result == null) {
535                         callback.sendSuccess(null);
536                         return;
537                     }
538 
539                     result.accept(session -> {
540                         ThreadUtils.assertOnUiThread();
541                         if (session == null) {
542                             callback.sendSuccess(null);
543                             return;
544                         }
545 
546                         if (session.isOpen()) {
547                             throw new IllegalArgumentException("Must use an unopened GeckoSession instance");
548                         }
549 
550                         if (GeckoSession.this.mWindow == null) {
551                             callback.sendError("Session is not attached to a window");
552                         } else {
553                             session.open(GeckoSession.this.mWindow.runtime);
554                             callback.sendSuccess(session.getId());
555                         }
556                     }, exception -> callback.sendError(exception.getMessage()));
557                 }
558             }
559         };
560 
561     private final GeckoSessionHandler<ContentDelegate> mProcessHangHandler =
562         new GeckoSessionHandler<ContentDelegate>(
563                 "GeckoViewProcessHangMonitor", this,
564                 new String[]{"GeckoView:HangReport"}) {
565 
566 
567             @Override
568             protected void handleMessage(final ContentDelegate delegate,
569                                          final String event,
570                                          final GeckoBundle message,
571                                          final EventCallback eventCallback) {
572                 Log.d(LOGTAG, "handleMessage " + event + " uri=" + message.getString("uri"));
573 
574                 GeckoResult<SlowScriptResponse> result = delegate.onSlowScript(GeckoSession.this,
575                         message.getString("scriptFileName"));
576                 if (result != null) {
577                     final int mReportId = message.getInt("hangId");
578                     result.accept(stopOrContinue -> {
579                         if (stopOrContinue != null) {
580                             final GeckoBundle bundle = new GeckoBundle();
581                             bundle.putInt("hangId", mReportId);
582                             switch (stopOrContinue) {
583                                 case STOP:
584                                     mEventDispatcher.dispatch("GeckoView:HangReportStop", bundle);
585                                     break;
586                                 case CONTINUE:
587                                     mEventDispatcher.dispatch("GeckoView:HangReportWait", bundle);
588                                     break;
589                             }
590                         }
591                     });
592                 } else {
593                     // default to stopping the script
594                     final GeckoBundle bundle = new GeckoBundle();
595                     bundle.putInt("hangId", message.getInt("hangId"));
596                     mEventDispatcher.dispatch("GeckoView:HangReportStop", bundle);
597                 }
598             }
599         };
600 
601     private final GeckoSessionHandler<ProgressDelegate> mProgressHandler =
602         new GeckoSessionHandler<ProgressDelegate>(
603             "GeckoViewProgress", this,
604             new String[]{
605                 "GeckoView:PageStart",
606                 "GeckoView:PageStop",
607                 "GeckoView:ProgressChanged",
608                 "GeckoView:SecurityChanged",
609                 "GeckoView:StateUpdated",
610             }
611         ) {
612             @Override
613             public void handleMessage(final ProgressDelegate delegate,
614                                       final String event,
615                                       final GeckoBundle message,
616                                       final EventCallback callback) {
617                 Log.d(LOGTAG, "handleMessage " + event + " uri=" + message.getString("uri"));
618                 if ("GeckoView:PageStart".equals(event)) {
619                     delegate.onPageStart(GeckoSession.this,
620                                          message.getString("uri"));
621                 } else if ("GeckoView:PageStop".equals(event)) {
622                     delegate.onPageStop(GeckoSession.this,
623                                         message.getBoolean("success"));
624                 } else if ("GeckoView:ProgressChanged".equals(event)) {
625                     delegate.onProgressChange(GeckoSession.this,
626                                               message.getInt("progress"));
627                 } else if ("GeckoView:SecurityChanged".equals(event)) {
628                     final GeckoBundle identity = message.getBundle("identity");
629                     delegate.onSecurityChange(GeckoSession.this, new ProgressDelegate.SecurityInformation(identity));
630                 } else if ("GeckoView:StateUpdated".equals(event)) {
631                     final GeckoBundle update = message.getBundle("data");
632                     if (update != null) {
633                         if (getHistoryDelegate() == null) {
634                             mStateCache.updateSessionState(update);
635                             delegate.onSessionStateChange(GeckoSession.this, new SessionState(mStateCache));
636                         }
637                     }
638                 }
639             }
640         };
641 
642     private final GeckoSessionHandler<ScrollDelegate> mScrollHandler =
643         new GeckoSessionHandler<ScrollDelegate>(
644             "GeckoViewScroll", this,
645             new String[]{ "GeckoView:ScrollChanged" }
646         ) {
647             @Override
648             public void handleMessage(final ScrollDelegate delegate,
649                                       final String event,
650                                       final GeckoBundle message,
651                                       final EventCallback callback) {
652 
653                 if ("GeckoView:ScrollChanged".equals(event)) {
654                     delegate.onScrollChanged(GeckoSession.this,
655                                              message.getInt("scrollX"),
656                                              message.getInt("scrollY"));
657                 }
658             }
659         };
660 
661     private final GeckoSessionHandler<ContentBlocking.Delegate> mContentBlockingHandler =
662         new GeckoSessionHandler<ContentBlocking.Delegate>(
663             "GeckoViewContentBlocking", this,
664             new String[]{ "GeckoView:ContentBlockingEvent" }
665         ) {
666             @Override
667             public void handleMessage(final ContentBlocking.Delegate delegate,
668                                       final String event,
669                                       final GeckoBundle message,
670                                       final EventCallback callback) {
671 
672                 if ("GeckoView:ContentBlockingEvent".equals(event)) {
673                     final ContentBlocking.BlockEvent be =
674                         ContentBlocking.BlockEvent.fromBundle(message);
675                     if (be.isBlocking()) {
676                         delegate.onContentBlocked(GeckoSession.this, be);
677                     } else {
678                         delegate.onContentLoaded(GeckoSession.this, be);
679                     }
680                 }
681             }
682         };
683 
684     private final GeckoSessionHandler<PermissionDelegate> mPermissionHandler =
685         new GeckoSessionHandler<PermissionDelegate>(
686             "GeckoViewPermission", this,
687             new String[] {
688                 "GeckoView:AndroidPermission",
689                 "GeckoView:ContentPermission",
690                 "GeckoView:MediaPermission"
691             }
692         ) {
693             @Override
694             public void handleMessage(final PermissionDelegate delegate,
695                                       final String event,
696                                       final GeckoBundle message,
697                                       final EventCallback callback) {
698                 if (delegate == null) {
699                     callback.sendSuccess(/* granted */ false);
700                     return;
701                 }
702                 if ("GeckoView:AndroidPermission".equals(event)) {
703                     delegate.onAndroidPermissionsRequest(
704                             GeckoSession.this, message.getStringArray("perms"),
705                             new PermissionCallback("android", callback));
706                 } else if ("GeckoView:ContentPermission".equals(event)) {
707                     final String typeString = message.getString("perm");
708                     final int type;
709                     if ("geolocation".equals(typeString)) {
710                         type = PermissionDelegate.PERMISSION_GEOLOCATION;
711                     } else if ("desktop-notification".equals(typeString)) {
712                         type = PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION;
713                     } else if ("persistent-storage".equals(typeString)) {
714                         type = PermissionDelegate.PERMISSION_PERSISTENT_STORAGE;
715                     } else if ("xr".equals(typeString)) {
716                         type = PermissionDelegate.PERMISSION_XR;
717                     } else if ("midi".equals(typeString)) {
718                         // We can get this from WPT and presumably other content, but Gecko
719                         // doesn't support Web MIDI.
720                         callback.sendError("Unsupported");
721                         return;
722                     } else if ("autoplay-media-inaudible".equals(typeString)) {
723                         type = PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE;
724                     } else if ("autoplay-media-audible".equals(typeString)) {
725                         type = PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE;
726                     } else if ("media-key-system-access".equals(typeString)) {
727                         type = PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS;
728                     } else {
729                         throw new IllegalArgumentException("Unknown permission request: " + typeString);
730                     }
731                     delegate.onContentPermissionRequest(
732                             GeckoSession.this, message.getString("uri"),
733                             type, new PermissionCallback(typeString, callback));
734                 } else if ("GeckoView:MediaPermission".equals(event)) {
735                     GeckoBundle[] videoBundles = message.getBundleArray("video");
736                     GeckoBundle[] audioBundles = message.getBundleArray("audio");
737                     PermissionDelegate.MediaSource[] videos = null;
738                     PermissionDelegate.MediaSource[] audios = null;
739 
740                     if (videoBundles != null) {
741                         videos = new PermissionDelegate.MediaSource[videoBundles.length];
742                         for (int i = 0; i < videoBundles.length; i++) {
743                             videos[i] = new PermissionDelegate.MediaSource(videoBundles[i]);
744                         }
745                     }
746 
747                     if (audioBundles != null) {
748                         audios = new PermissionDelegate.MediaSource[audioBundles.length];
749                         for (int i = 0; i < audioBundles.length; i++) {
750                             audios[i] = new PermissionDelegate.MediaSource(audioBundles[i]);
751                         }
752                     }
753 
754                     delegate.onMediaPermissionRequest(
755                             GeckoSession.this, message.getString("uri"),
756                             videos, audios, new PermissionCallback("media", callback));
757                 }
758             }
759         };
760 
761     private final GeckoSessionHandler<SelectionActionDelegate> mSelectionActionDelegate =
762         new GeckoSessionHandler<SelectionActionDelegate>(
763             "GeckoViewSelectionAction", this,
764             new String[] {
765                 "GeckoView:HideSelectionAction",
766                 "GeckoView:ShowSelectionAction",
767             }
768         ) {
769             @Override
770             public void handleMessage(final SelectionActionDelegate delegate,
771                                       final String event,
772                                       final GeckoBundle message,
773                                       final EventCallback callback) {
774                 if ("GeckoView:ShowSelectionAction".equals(event)) {
775                     final @SelectionActionDelegateAction HashSet<String> actionsSet =
776                             new HashSet<>(Arrays.asList(message.getStringArray("actions")));
777                     final SelectionActionDelegate.Selection selection =
778                             new SelectionActionDelegate.Selection(message, actionsSet, callback);
779 
780                     delegate.onShowActionRequest(GeckoSession.this, selection);
781 
782                 } else if ("GeckoView:HideSelectionAction".equals(event)) {
783                     final String reasonString = message.getString("reason");
784                     final int reason;
785                     if ("invisibleselection".equals(reasonString)) {
786                         reason = SelectionActionDelegate.HIDE_REASON_INVISIBLE_SELECTION;
787                     } else if ("presscaret".equals(reasonString)) {
788                         reason = SelectionActionDelegate.HIDE_REASON_ACTIVE_SELECTION;
789                     } else if ("scroll".equals(reasonString)) {
790                         reason = SelectionActionDelegate.HIDE_REASON_ACTIVE_SCROLL;
791                     } else if ("visibilitychange".equals(reasonString)) {
792                         reason = SelectionActionDelegate.HIDE_REASON_NO_SELECTION;
793                     } else {
794                         throw new IllegalArgumentException();
795                     }
796 
797                     delegate.onHideAction(GeckoSession.this, reason);
798                 }
799             }
800         };
801 
802     private LongSparseArray<MediaElement> mMediaElements = new LongSparseArray<>();
getMediaElements()803     /* package */ LongSparseArray<MediaElement> getMediaElements() {
804         return mMediaElements;
805     }
806     private final GeckoSessionHandler<MediaDelegate> mMediaHandler =
807             new GeckoSessionHandler<MediaDelegate>(
808                     "GeckoViewMedia", this,
809                     new String[]{
810                         "GeckoView:MediaAdd",
811                         "GeckoView:MediaRemove",
812                         "GeckoView:MediaRemoveAll",
813                         "GeckoView:MediaReadyStateChanged",
814                         "GeckoView:MediaTimeChanged",
815                         "GeckoView:MediaPlaybackStateChanged",
816                         "GeckoView:MediaMetadataChanged",
817                         "GeckoView:MediaProgress",
818                         "GeckoView:MediaVolumeChanged",
819                         "GeckoView:MediaRateChanged",
820                         "GeckoView:MediaFullscreenChanged",
821                         "GeckoView:MediaError",
822                         "GeckoView:MediaRecordingStatusChanged",
823                     }
824             ) {
825         @Override
826         public void handleMessage(final MediaDelegate delegate,
827                                   final String event,
828                                   final GeckoBundle message,
829                                   final EventCallback callback) {
830             if ("GeckoView:MediaAdd".equals(event)) {
831                 final MediaElement element = new MediaElement(message.getLong("id"), GeckoSession.this);
832                 delegate.onMediaAdd(GeckoSession.this, element);
833                 return;
834             } else if ("GeckoView:MediaRemoveAll".equals(event)) {
835                 for (int i = 0; i < mMediaElements.size(); i++) {
836                     final long key = mMediaElements.keyAt(i);
837                     delegate.onMediaRemove(GeckoSession.this, mMediaElements.get(key));
838                 }
839                 mMediaElements.clear();
840                 return;
841             } else if ("GeckoView:MediaRecordingStatusChanged".equals(event)) {
842                 final GeckoBundle[] deviceBundles = message.getBundleArray("devices");
843                 final MediaDelegate.RecordingDevice[] devices = new MediaDelegate.RecordingDevice[deviceBundles.length];
844                 for (int i = 0; i < deviceBundles.length; i++) {
845                     devices[i] = new MediaDelegate.RecordingDevice(deviceBundles[i]);
846                 }
847                 delegate.onRecordingStatusChanged(GeckoSession.this, devices);
848                 return;
849             }
850 
851             final long id = message.getLong("id", 0);
852             final MediaElement element = mMediaElements.get(id);
853             if (element == null) {
854                 Log.w(LOGTAG, "MediaElement not found for '" + id + "'");
855                 return;
856             }
857 
858             if ("GeckoView:MediaTimeChanged".equals(event)) {
859                 element.notifyTimeChange(message.getDouble("time"));
860             } else if ("GeckoView:MediaProgress".equals(event)) {
861                 element.notifyLoadProgress(message);
862             } else if ("GeckoView:MediaMetadataChanged".equals(event)) {
863                 element.notifyMetadataChange(message);
864             } else if ("GeckoView:MediaReadyStateChanged".equals(event)) {
865                 element.notifyReadyStateChange(message.getInt("readyState"));
866             } else if ("GeckoView:MediaPlaybackStateChanged".equals(event)) {
867                 element.notifyPlaybackStateChange(message.getString("playbackState"));
868             } else if ("GeckoView:MediaVolumeChanged".equals(event)) {
869                 element.notifyVolumeChange(message.getDouble("volume"), message.getBoolean("muted"));
870             } else if ("GeckoView:MediaRateChanged".equals(event)) {
871                 element.notifyPlaybackRateChange(message.getDouble("rate"));
872             } else if ("GeckoView:MediaFullscreenChanged".equals(event)) {
873                 element.notifyFullscreenChange(message.getBoolean("fullscreen"));
874             } else if ("GeckoView:MediaRemove".equals(event)) {
875                 delegate.onMediaRemove(GeckoSession.this, element);
876                 mMediaElements.remove(element.getVideoId());
877             } else if ("GeckoView:MediaError".equals(event)) {
878                 element.notifyError(message.getInt("code"));
879             } else {
880                 throw new UnsupportedOperationException(event + " media message not implemented");
881             }
882         }
883     };
884 
885 
886     /* package */ int handlersCount;
887 
888     private final GeckoSessionHandler<?>[] mSessionHandlers = new GeckoSessionHandler<?>[] {
889         mContentHandler, mHistoryHandler, mMediaHandler, mNavigationHandler,
890         mPermissionHandler, mProcessHangHandler, mProgressHandler, mScrollHandler,
891         mSelectionActionDelegate, mContentBlockingHandler
892     };
893 
894     private static class PermissionCallback implements
895         PermissionDelegate.Callback, PermissionDelegate.MediaCallback {
896 
897         private final String mType;
898         private EventCallback mCallback;
899 
PermissionCallback(final String type, final EventCallback callback)900         public PermissionCallback(final String type, final EventCallback callback) {
901             mType = type;
902             mCallback = callback;
903         }
904 
submit(final Object response)905         private void submit(final Object response) {
906             if (mCallback != null) {
907                 mCallback.sendSuccess(response);
908                 mCallback = null;
909             }
910         }
911 
912         @Override // PermissionDelegate.Callback
grant()913         public void grant() {
914             if ("media".equals(mType)) {
915                 throw new UnsupportedOperationException();
916             }
917             submit(/* response */ true);
918         }
919 
920         @Override // PermissionDelegate.Callback, PermissionDelegate.MediaCallback
reject()921         public void reject() {
922             submit(/* response */ false);
923         }
924 
925         @Override // PermissionDelegate.MediaCallback
grant(final String video, final String audio)926         public void grant(final String video, final String audio) {
927             if (!"media".equals(mType)) {
928                 throw new UnsupportedOperationException();
929             }
930             final GeckoBundle response = new GeckoBundle(2);
931             response.putString("video", video);
932             response.putString("audio", audio);
933             submit(response);
934         }
935 
936         @Override // PermissionDelegate.MediaCallback
grant(final PermissionDelegate.MediaSource video, final PermissionDelegate.MediaSource audio)937         public void grant(final PermissionDelegate.MediaSource video, final PermissionDelegate.MediaSource audio) {
938             grant(video != null ? video.id : null,
939                   audio != null ? audio.id : null);
940         }
941     }
942 
943     /**
944      * Get the current user agent string for this GeckoSession.
945      *
946      * @return a {@link GeckoResult} containing the UserAgent string
947      */
948     @AnyThread
getUserAgent()949     public @NonNull GeckoResult<String> getUserAgent() {
950         final CallbackResult<String> result = new CallbackResult<String>() {
951             @Override
952             public void sendSuccess(final Object value) {
953                 complete((String) value);
954             }
955         };
956         mEventDispatcher.dispatch("GeckoView:GetUserAgent", null, result);
957         return result;
958     }
959 
960     /**
961      * Get the default user agent for this GeckoView build.
962      *
963      * This method does not account for any override that might have been
964      * applied to the user agent string.
965      *
966      * @return the default user agent string
967      */
968     @AnyThread
getDefaultUserAgent()969     public static @NonNull String getDefaultUserAgent() {
970         return BuildConfig.USER_AGENT_GECKOVIEW_MOBILE;
971     }
972 
973     /**
974      * Get the current permission delegate for this GeckoSession.
975      * @return PermissionDelegate instance or null if using default delegate.
976      */
977     @UiThread
getPermissionDelegate()978     public @Nullable PermissionDelegate getPermissionDelegate() {
979         ThreadUtils.assertOnUiThread();
980         return mPermissionHandler.getDelegate();
981     }
982 
983     /**
984      * Set the current permission delegate for this GeckoSession.
985      * @param delegate PermissionDelegate instance or null to use the default delegate.
986      */
987     @UiThread
setPermissionDelegate(final @Nullable PermissionDelegate delegate)988     public void setPermissionDelegate(final @Nullable PermissionDelegate delegate) {
989         ThreadUtils.assertOnUiThread();
990         mPermissionHandler.setDelegate(delegate, this);
991     }
992 
993     private PromptDelegate mPromptDelegate;
994 
995     private final Listener mListener = new Listener();
996 
997     /* package */ static final class Window extends JNIObject implements IInterface {
998         public final GeckoRuntime runtime;
999         private WeakReference<GeckoSession> mOwner;
1000         private NativeQueue mNativeQueue;
1001         private Binder mBinder;
1002 
Window(final @NonNull GeckoRuntime runtime, final @NonNull GeckoSession owner, final @NonNull NativeQueue nativeQueue)1003         public Window(final @NonNull GeckoRuntime runtime,
1004                       final @NonNull GeckoSession owner,
1005                       final @NonNull NativeQueue nativeQueue) {
1006             this.runtime = runtime;
1007             mOwner = new WeakReference<>(owner);
1008             mNativeQueue = nativeQueue;
1009         }
1010 
1011         @Override // IInterface
asBinder()1012         public Binder asBinder() {
1013             if (mBinder == null) {
1014                 mBinder = new Binder();
1015                 mBinder.attachInterface(this, Window.class.getName());
1016             }
1017             return mBinder;
1018         }
1019 
1020         // Create a new Gecko window and assign an initial set of Java session objects to it.
1021         @WrapForJNI(dispatchTo = "proxy")
open(Window instance, NativeQueue queue, Compositor compositor, EventDispatcher dispatcher, SessionAccessibility.NativeProvider sessionAccessibility, GeckoBundle initData, String id, String chromeUri, int screenId, boolean privateMode, boolean isRemote)1022         public static native void open(Window instance, NativeQueue queue,
1023                                        Compositor compositor, EventDispatcher dispatcher,
1024                                        SessionAccessibility.NativeProvider sessionAccessibility,
1025                                        GeckoBundle initData, String id, String chromeUri,
1026                                        int screenId, boolean privateMode, boolean isRemote);
1027 
1028         @Override // JNIObject
disposeNative()1029         public void disposeNative() {
1030             if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
1031                 nativeDisposeNative();
1032             } else {
1033                 GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
1034                         this, "nativeDisposeNative");
1035             }
1036         }
1037 
1038         @WrapForJNI(dispatchTo = "proxy", stubName = "DisposeNative")
nativeDisposeNative()1039         private native void nativeDisposeNative();
1040 
1041         // Force the underlying Gecko window to close and release assigned Java objects.
close()1042         public void close() {
1043             // Reset our queue, so we don't end up with queued calls on a disposed object.
1044             synchronized (this) {
1045                 if (mNativeQueue == null) {
1046                     // Already closed elsewhere.
1047                     return;
1048                 }
1049                 mNativeQueue.reset(State.INITIAL);
1050                 mNativeQueue = null;
1051                 mOwner = null;
1052             }
1053 
1054             // Detach ourselves from the binder as well, to prevent this window from being
1055             // read from any parcels.
1056             asBinder().attachInterface(null, Window.class.getName());
1057 
1058             if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
1059                 nativeClose();
1060             } else {
1061                 GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
1062                         this, "nativeClose");
1063             }
1064         }
1065 
1066         @WrapForJNI(dispatchTo = "proxy", stubName = "Close")
nativeClose()1067         private native void nativeClose();
1068 
1069         // Assign a new set of Java session objects to the underlying Gecko window.
1070         // This replaces previously assigned objects from open() or transfer() calls.
transfer(final GeckoSession owner, final NativeQueue queue, final Compositor compositor, final EventDispatcher dispatcher, final SessionAccessibility.NativeProvider sessionAccessibility, final GeckoBundle initData)1071         public synchronized void transfer(final GeckoSession owner,
1072                                           final NativeQueue queue,
1073                                           final Compositor compositor,
1074                                           final EventDispatcher dispatcher,
1075                                           final SessionAccessibility.NativeProvider sessionAccessibility,
1076                                           final GeckoBundle initData) {
1077             if (mNativeQueue == null) {
1078                 // Already closed.
1079                 return;
1080             }
1081 
1082             final GeckoSession oldOwner = mOwner.get();
1083             if (oldOwner != null && owner != oldOwner) {
1084                 oldOwner.abandonWindow();
1085             }
1086 
1087             mOwner = new WeakReference<>(owner);
1088 
1089             if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
1090                 nativeTransfer(queue, compositor, dispatcher, sessionAccessibility, initData);
1091             } else {
1092                 GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
1093                         this, "nativeTransfer",
1094                         NativeQueue.class, queue,
1095                         Compositor.class, compositor,
1096                         EventDispatcher.class, dispatcher,
1097                         SessionAccessibility.NativeProvider.class, sessionAccessibility,
1098                         GeckoBundle.class, initData);
1099             }
1100 
1101             if (mNativeQueue != queue) {
1102                 // Reset the old queue to prevent old events from affecting this window.
1103                 // Gecko will call onReady later with the new queue if needed.
1104                 mNativeQueue.reset(State.INITIAL);
1105                 mNativeQueue = queue;
1106             }
1107         }
1108 
1109         @WrapForJNI(dispatchTo = "proxy", stubName = "Transfer")
nativeTransfer(NativeQueue queue, Compositor compositor, EventDispatcher dispatcher, SessionAccessibility.NativeProvider sessionAccessibility, GeckoBundle initData)1110         private native void nativeTransfer(NativeQueue queue, Compositor compositor,
1111                                            EventDispatcher dispatcher,
1112                                            SessionAccessibility.NativeProvider sessionAccessibility,
1113                                            GeckoBundle initData);
1114 
1115         @WrapForJNI(dispatchTo = "proxy")
attachEditable(IGeckoEditableParent parent)1116         public native void attachEditable(IGeckoEditableParent parent);
1117 
1118         @WrapForJNI(dispatchTo = "proxy")
attachAccessibility(SessionAccessibility.NativeProvider sessionAccessibility)1119         public native void attachAccessibility(SessionAccessibility.NativeProvider sessionAccessibility);
1120 
1121         @WrapForJNI(calledFrom = "gecko")
onReady(final @Nullable NativeQueue queue)1122         private synchronized void onReady(final @Nullable NativeQueue queue) {
1123             // onReady is called the first time the Gecko window is ready, with a null queue
1124             // argument. In this case, we simply set the current queue to ready state.
1125             //
1126             // After the initial call, onReady is called again every time Window.transfer()
1127             // is called, with a non-null queue argument. In this case, we only set the
1128             // current queue to ready state _if_ the current queue matches the given queue,
1129             // because if the queues don't match, we know there is another onReady call coming.
1130 
1131             if ((queue == null && mNativeQueue == null) ||
1132                 (queue != null && mNativeQueue != queue)) {
1133                 return;
1134             }
1135 
1136             if (mNativeQueue.checkAndSetState(State.INITIAL, State.READY) &&
1137                     queue == null) {
1138                 Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
1139                       " - chrome startup finished");
1140             }
1141         }
1142 
1143         @Override
finalize()1144         protected void finalize() throws Throwable {
1145             close();
1146             disposeNative();
1147         }
1148 
1149         @WrapForJNI(calledFrom = "gecko")
onLoadRequest(final @NonNull String uri, final int windowType, final int flags, final @Nullable String triggeringUri, final boolean hasUserGesture, final boolean isTopLevel)1150         private GeckoResult<Boolean> onLoadRequest(final @NonNull String uri, final int windowType,
1151                                                    final int flags, final @Nullable String triggeringUri,
1152                                                    final boolean hasUserGesture, final boolean isTopLevel) {
1153             final GeckoSession session = (mOwner == null) ? null : mOwner.get();
1154             if (session == null) {
1155                 // Don't handle any load request if we can't get the session for some reason.
1156                 return GeckoResult.fromValue(false);
1157             }
1158             GeckoResult<Boolean> res = new GeckoResult<>();
1159 
1160             ThreadUtils.postToUiThread(new Runnable() {
1161                 @Override
1162                 public void run() {
1163                     final NavigationDelegate delegate = session.getNavigationDelegate();
1164 
1165                     if (delegate == null) {
1166                         res.complete(false);
1167                         return;
1168                     }
1169 
1170                     final String trigger = TextUtils.isEmpty(triggeringUri) ? null : triggeringUri;
1171                     final NavigationDelegate.LoadRequest req = new NavigationDelegate.LoadRequest(uri,
1172                             trigger, windowType, flags, hasUserGesture, false /* isDirectNavigation */);
1173                     final GeckoResult<AllowOrDeny> reqResponse = isTopLevel ?
1174                             delegate.onLoadRequest(session, req) :
1175                             delegate.onSubframeLoadRequest(session, req);
1176 
1177                     if (reqResponse == null) {
1178                         res.complete(false);
1179                         return;
1180                     }
1181 
1182                     reqResponse.accept(value -> {
1183                         if (value == AllowOrDeny.DENY) {
1184                             res.complete(true);
1185                         } else {
1186                             res.complete(false);
1187                         }
1188                     }, ex -> {
1189                         // This is incredibly ugly and unreadable because checkstyle sucks.
1190                             res.complete(false);
1191                         });
1192                 }
1193             });
1194 
1195             return res;
1196         }
1197     }
1198 
1199     private class Listener implements BundleEventListener {
registerListeners()1200         /* package */ void registerListeners() {
1201             getEventDispatcher().registerUiThreadListener(this,
1202                 "GeckoView:PinOnScreen",
1203                 "GeckoView:Prompt",
1204                 null);
1205         }
1206 
1207         @Override
handleMessage(final String event, final GeckoBundle message, final EventCallback callback)1208         public void handleMessage(final String event, final GeckoBundle message,
1209                                   final EventCallback callback) {
1210             if (DEBUG) {
1211                 Log.d(LOGTAG, "handleMessage: event = " + event);
1212             }
1213 
1214             if ("GeckoView:PinOnScreen".equals(event)) {
1215                 GeckoSession.this.setShouldPinOnScreen(message.getBoolean("pinned"));
1216             } else if ("GeckoView:Prompt".equals(event)) {
1217                 handlePromptEvent(GeckoSession.this, message, callback);
1218             }
1219         }
1220     }
1221 
1222     protected @Nullable Window mWindow;
1223     private GeckoSessionSettings mSettings;
1224 
GeckoSession()1225     public GeckoSession() {
1226         this(null);
1227     }
1228 
GeckoSession(final @Nullable GeckoSessionSettings settings)1229     public GeckoSession(final @Nullable GeckoSessionSettings settings) {
1230         mSettings = new GeckoSessionSettings(settings, this);
1231         mListener.registerListeners();
1232 
1233         mWebExtensionController = new WebExtension.SessionController(this);
1234 
1235         mAutofillSupport = new Autofill.Support(this);
1236         mAutofillSupport.registerListeners();
1237 
1238         if (BuildConfig.DEBUG && handlersCount != mSessionHandlers.length) {
1239             throw new AssertionError("Add new handler to handlers list");
1240         }
1241     }
1242 
getRuntime()1243     /* package */ @Nullable GeckoRuntime getRuntime() {
1244         if (mWindow == null) {
1245             return null;
1246         }
1247         return mWindow.runtime;
1248     }
1249 
abandonWindow()1250     /* package */ synchronized void abandonWindow() {
1251         if (mWindow == null) {
1252             return;
1253         }
1254 
1255         onWindowChanged(WINDOW_TRANSFER_OUT, /* inProgress */ true);
1256         mWindow = null;
1257         onWindowChanged(WINDOW_TRANSFER_OUT, /* inProgress */ false);
1258     }
1259 
transferFrom(final Window window, final GeckoSessionSettings settings, final String id)1260     private void transferFrom(final Window window,
1261                               final GeckoSessionSettings settings,
1262                               final String id) {
1263         if (isOpen()) {
1264             // We will leak the existing Window if we transfer in another one.
1265             throw new IllegalStateException("Session is open");
1266         }
1267 
1268         if (window != null) {
1269             onWindowChanged(WINDOW_TRANSFER_IN, /* inProgress */ true);
1270         }
1271 
1272         mWindow = window;
1273         mSettings = new GeckoSessionSettings(settings, this);
1274         mId = id;
1275 
1276         if (mWindow != null) {
1277             mWindow.transfer(this, mNativeQueue, mCompositor,
1278                     mEventDispatcher, mAccessibility != null ? mAccessibility.nativeProvider : null,
1279                     createInitData());
1280             onWindowChanged(WINDOW_TRANSFER_IN, /* inProgress */ false);
1281             mWebExtensionController.setRuntime(mWindow.runtime);
1282         }
1283     }
1284 
transferFrom(final GeckoSession session)1285     /* package */ void transferFrom(final GeckoSession session) {
1286         transferFrom(session.mWindow, session.mSettings, session.mId);
1287         session.mWindow = null;
1288     }
1289 
1290     @Override // Parcelable
1291     @AnyThread
describeContents()1292     public int describeContents() {
1293         return 0;
1294     }
1295 
1296     @Override // Parcelable
1297     @AnyThread
writeToParcel(final Parcel out, final int flags)1298     public void writeToParcel(final Parcel out, final int flags) {
1299         out.writeStrongInterface(mWindow);
1300         out.writeParcelable(mSettings, flags);
1301         out.writeString(mId);
1302     }
1303 
1304     // AIDL code may call readFromParcel even though it's not part of Parcelable.
1305     @AnyThread
readFromParcel(final @NonNull Parcel source)1306     public void readFromParcel(final @NonNull Parcel source) {
1307         final IBinder binder = source.readStrongBinder();
1308         final IInterface ifce = (binder != null) ?
1309                 binder.queryLocalInterface(Window.class.getName()) : null;
1310         final Window window = (ifce instanceof Window) ? (Window) ifce : null;
1311         final GeckoSessionSettings settings =
1312                 source.readParcelable(getClass().getClassLoader());
1313         final String id = source.readString();
1314         transferFrom(window, settings, id);
1315     }
1316 
1317     public static final Creator<GeckoSession> CREATOR = new Creator<GeckoSession>() {
1318         @Override
1319         @AnyThread
1320         public GeckoSession createFromParcel(final Parcel in) {
1321             final GeckoSession session = new GeckoSession();
1322             session.readFromParcel(in);
1323             return session;
1324         }
1325 
1326         @Override
1327         @AnyThread
1328         public GeckoSession[] newArray(final int size) {
1329             return new GeckoSession[size];
1330         }
1331     };
1332 
1333     @Override
1334     @AnyThread
hashCode()1335     public int hashCode() {
1336         return mId.hashCode();
1337     }
1338 
1339     @Override
1340     @AnyThread
equals(final Object obj)1341     public boolean equals(final Object obj) {
1342         return obj instanceof GeckoSession && mId.equals(((GeckoSession) obj).mId);
1343     }
1344 
1345     /**
1346      * Return whether this session is open.
1347      *
1348      * @return True if session is open.
1349      * @see #open
1350      * @see #close
1351      */
1352     @AnyThread
isOpen()1353     public boolean isOpen() {
1354         return mWindow != null;
1355     }
1356 
isReady()1357     /* package */ boolean isReady() {
1358         return mNativeQueue.isReady();
1359     }
1360 
createInitData()1361     private GeckoBundle createInitData() {
1362         final GeckoBundle initData = new GeckoBundle(2);
1363         initData.putBundle("settings", mSettings.toBundle());
1364 
1365         final GeckoBundle modules = new GeckoBundle(mSessionHandlers.length);
1366         for (final GeckoSessionHandler<?> handler : mSessionHandlers) {
1367             modules.putBoolean(handler.getName(), handler.isEnabled());
1368         }
1369         initData.putBundle("modules", modules);
1370         return initData;
1371     }
1372 
1373     /**
1374      * Opens the session.
1375      *
1376      * Call this when you are ready to use a GeckoSession instance.
1377      *
1378      * The session is in a 'closed' state when first created. Opening it creates
1379      * the underlying Gecko objects necessary to load a page, etc. Most GeckoSession
1380      * methods only take affect on an open session, and are queued until the session
1381      * is opened here. Opening a session is an asynchronous operation.
1382      *
1383      * @param runtime The Gecko runtime to attach this session to.
1384      * @see #close
1385      * @see #isOpen
1386      */
1387     @UiThread
open(final @NonNull GeckoRuntime runtime)1388     public void open(final @NonNull GeckoRuntime runtime) {
1389         ThreadUtils.assertOnUiThread();
1390 
1391         if (isOpen()) {
1392             // We will leak the existing Window if we open another one.
1393             throw new IllegalStateException("Session is open");
1394         }
1395 
1396         final String chromeUri = mSettings.getChromeUri();
1397         final int screenId = mSettings.getScreenId();
1398         final boolean isPrivate = mSettings.getUsePrivateMode();
1399         final boolean isRemote = runtime.getSettings().getUseMultiprocess();
1400 
1401         mWindow = new Window(runtime, this, mNativeQueue);
1402         mWebExtensionController.setRuntime(runtime);
1403 
1404         onWindowChanged(WINDOW_OPEN, /* inProgress */ true);
1405 
1406         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
1407             Window.open(mWindow, mNativeQueue, mCompositor, mEventDispatcher,
1408                         mAccessibility != null ? mAccessibility.nativeProvider : null,
1409                         createInitData(), mId, chromeUri, screenId, isPrivate, isRemote);
1410         } else {
1411             GeckoThread.queueNativeCallUntil(
1412                 GeckoThread.State.PROFILE_READY,
1413                 Window.class, "open",
1414                 Window.class, mWindow,
1415                 NativeQueue.class, mNativeQueue,
1416                 Compositor.class, mCompositor,
1417                 EventDispatcher.class, mEventDispatcher,
1418                 SessionAccessibility.NativeProvider.class,
1419                 mAccessibility != null ? mAccessibility.nativeProvider : null,
1420                 GeckoBundle.class, createInitData(),
1421                 String.class, mId,
1422                 String.class, chromeUri,
1423                 screenId, isPrivate, isRemote);
1424         }
1425 
1426         onWindowChanged(WINDOW_OPEN, /* inProgress */ false);
1427     }
1428 
1429     /**
1430      * Closes the session.
1431      *
1432      * This frees the underlying Gecko objects and unloads the current page. The session may be
1433      * reopened later, but page state is not restored. Call this when you are finished using
1434      * a GeckoSession instance.
1435      *
1436      * @see #open
1437      * @see #isOpen
1438      */
1439     @UiThread
close()1440     public void close() {
1441         ThreadUtils.assertOnUiThread();
1442 
1443         if (!isOpen()) {
1444             Log.w(LOGTAG, "Attempted to close a GeckoSession that was already closed.");
1445             return;
1446         }
1447 
1448         onWindowChanged(WINDOW_CLOSE, /* inProgress */ true);
1449 
1450         mWindow.close();
1451         mWindow.disposeNative();
1452         mWindow = null;
1453 
1454         onWindowChanged(WINDOW_CLOSE, /* inProgress */ false);
1455     }
1456 
onWindowChanged(final int change, final boolean inProgress)1457     private void onWindowChanged(final int change, final boolean inProgress) {
1458         if ((change == WINDOW_OPEN || change == WINDOW_TRANSFER_IN) && !inProgress) {
1459             mTextInput.onWindowChanged(mWindow);
1460         }
1461         if ((change == WINDOW_CLOSE || change == WINDOW_TRANSFER_OUT) && !inProgress) {
1462             getAutofillSupport().clear();
1463         }
1464     }
1465 
1466     /**
1467      * Get the SessionTextInput instance for this session. May be called on any thread.
1468      *
1469      * @return SessionTextInput instance.
1470      */
1471     @AnyThread
getTextInput()1472     public @NonNull SessionTextInput getTextInput() {
1473         // May be called on any thread.
1474         return mTextInput;
1475     }
1476 
1477     /**
1478       * Get the SessionAccessibility instance for this session.
1479       *
1480       * @return SessionAccessibility instance.
1481       */
1482     @UiThread
getAccessibility()1483     public @NonNull SessionAccessibility getAccessibility() {
1484         ThreadUtils.assertOnUiThread();
1485         if (mAccessibility != null) {
1486             return mAccessibility;
1487         }
1488 
1489         mAccessibility = new SessionAccessibility(this);
1490         if (mWindow != null) {
1491             if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
1492                 mWindow.attachAccessibility(mAccessibility.nativeProvider);
1493             } else {
1494                 GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
1495                         mWindow, "attachAccessibility",
1496                         SessionAccessibility.NativeProvider.class, mAccessibility.nativeProvider);
1497             }
1498         }
1499         return mAccessibility;
1500     }
1501 
1502     @Retention(RetentionPolicy.SOURCE)
1503     @IntDef(flag = true,
1504             value = { LOAD_FLAGS_NONE, LOAD_FLAGS_BYPASS_CACHE, LOAD_FLAGS_BYPASS_PROXY,
1505                       LOAD_FLAGS_EXTERNAL, LOAD_FLAGS_ALLOW_POPUPS, LOAD_FLAGS_FORCE_ALLOW_DATA_URI, LOAD_FLAGS_REPLACE_HISTORY })
1506     /* package */ @interface LoadFlags {}
1507 
1508     // These flags follow similarly named ones in Gecko's nsIWebNavigation.idl
1509     // https://searchfox.org/mozilla-central/source/docshell/base/nsIWebNavigation.idl
1510     //
1511     // We do not use the same values directly in order to insulate ourselves from
1512     // changes in Gecko. Instead, the flags are converted in GeckoViewNavigation.jsm.
1513 
1514     /**
1515      * Default load flag, no special considerations.
1516      */
1517     public static final int LOAD_FLAGS_NONE = 0;
1518 
1519     /**
1520      * Bypass the cache.
1521      */
1522     public static final int LOAD_FLAGS_BYPASS_CACHE = 1 << 0;
1523 
1524     /**
1525      * Bypass the proxy, if one has been configured.
1526      */
1527     public static final int LOAD_FLAGS_BYPASS_PROXY = 1 << 1;
1528 
1529     /**
1530      * The load is coming from an external app. Perform additional checks.
1531      */
1532     public static final int LOAD_FLAGS_EXTERNAL = 1 << 2;
1533 
1534     /**
1535      * Popup blocking will be disabled for this load
1536      */
1537     public static final int LOAD_FLAGS_ALLOW_POPUPS = 1 << 3;
1538 
1539     /**
1540      * Bypass the URI classifier (content blocking and Safe Browsing).
1541      */
1542     public static final int LOAD_FLAGS_BYPASS_CLASSIFIER = 1 << 4;
1543 
1544     /**
1545      * Allows a top-level data: navigation to occur. E.g. view-image
1546      * is an explicit user action which should be allowed.
1547      */
1548     public static final int LOAD_FLAGS_FORCE_ALLOW_DATA_URI = 1 << 5;
1549 
1550     /**
1551      * This flag specifies that any existing history entry should be replaced.
1552      */
1553     public static final int LOAD_FLAGS_REPLACE_HISTORY = 1 << 6;
1554 
1555     /**
1556      * Formats the map of additional request headers into an ArrayList
1557      * @param additionalHeaders Request headers to be formatted
1558      * @return Correctly formatted request headers as a ArrayList
1559      */
additionalHeadersToStringArray(final @NonNull Map<String, String> additionalHeaders)1560     private ArrayList<String> additionalHeadersToStringArray(final @NonNull Map<String, String> additionalHeaders) {
1561         ArrayList<String> headers = new ArrayList<String>();
1562         for (String key : additionalHeaders.keySet()) {
1563             // skip null key if one exists
1564             if (key == null)
1565                 continue;
1566 
1567             headers.add( String.format("%s:%s", key, additionalHeaders.get(key)) );
1568         }
1569         return headers;
1570     }
1571 
1572     /**
1573      * Load the given URI.
1574      * @param uri The URI of the resource to load.
1575      */
1576     @AnyThread
loadUri(final @NonNull String uri)1577     public void loadUri(final @NonNull String uri) {
1578         loadUri(uri, (GeckoSession)null, LOAD_FLAGS_NONE, (Map<String, String>) null);
1579     }
1580 
1581     /**
1582      * Load the given URI with specified HTTP request headers.
1583      * @param uri The URI of the resource to load.
1584      * @param additionalHeaders any additional request headers used with the load
1585      */
1586     @AnyThread
loadUri(final @NonNull String uri, final @Nullable Map<String, String> additionalHeaders)1587     public void loadUri(final @NonNull String uri, final @Nullable Map<String, String> additionalHeaders) {
1588         loadUri(uri, (GeckoSession)null, LOAD_FLAGS_NONE, additionalHeaders);
1589     }
1590 
1591     /**
1592      * Load the given URI with the specified referrer and load type.
1593      *
1594      * @param uri the URI to load
1595      * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1596      */
1597     @AnyThread
loadUri(final @NonNull String uri, final @LoadFlags int flags)1598     public void loadUri(final @NonNull String uri, final @LoadFlags int flags) {
1599         loadUri(uri, (GeckoSession)null, flags, (Map<String, String>) null);
1600     }
1601 
1602     /**
1603      * Load the given URI with the specified referrer and load type.
1604      *
1605      * @param uri the URI to load
1606      * @param referrer the referrer, may be null
1607      * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1608      */
1609     @AnyThread
loadUri(final @NonNull String uri, final @Nullable String referrer, final @LoadFlags int flags)1610     public void loadUri(final @NonNull String uri, final @Nullable String referrer,
1611                         final @LoadFlags int flags) {
1612         loadUri(uri, referrer, flags, (Map<String, String>) null);
1613     }
1614 
1615     /**
1616      * Load the given URI with the specified referrer, load type and HTTP request headers.
1617      *
1618      * @param uri the URI to load
1619      * @param referrer the referrer, may be null
1620      * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1621      * @param additionalHeaders any additional request headers used with the load
1622      */
1623     @AnyThread
loadUri(final @NonNull String uri, final @Nullable String referrer, final @LoadFlags int flags, final @Nullable Map<String, String> additionalHeaders)1624     public void loadUri(final @NonNull String uri, final @Nullable String referrer,
1625                         final @LoadFlags int flags, final @Nullable Map<String, String> additionalHeaders) {
1626         final GeckoBundle msg = new GeckoBundle();
1627         msg.putString("uri", uri);
1628         msg.putInt("flags", flags);
1629 
1630         if (referrer != null) {
1631             msg.putString("referrerUri", referrer);
1632         }
1633 
1634         if (additionalHeaders != null) {
1635             msg.putStringArray("headers", additionalHeadersToStringArray(additionalHeaders));
1636         }
1637         mEventDispatcher.dispatch("GeckoView:LoadUri", msg);
1638     }
1639 
1640     /**
1641      * Load the given URI with the specified referrer and load type. This method will also do any
1642      * applicable checks to ensure that the specified URI is both safe and allowable
1643      * according to the referring GeckoSession.
1644      *
1645      * @param uri the URI to load
1646      * @param referrer the referring GeckoSession, may be null
1647      * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1648      */
1649     @AnyThread
loadUri(final @NonNull String uri, final @Nullable GeckoSession referrer, final @LoadFlags int flags)1650     public void loadUri(final @NonNull String uri, final @Nullable GeckoSession referrer,
1651                         final @LoadFlags int flags) {
1652         loadUri(uri, referrer, flags, (Map<String, String>) null);
1653     }
1654 
1655     /**
1656      * Load the given URI with the specified referrer, load type and HTTP request headers. This
1657      * method will also do any applicable checks to ensure that the specified URI is both safe and
1658      * allowable according to the referring GeckoSession.
1659      *
1660      * @param uri the URI to load
1661      * @param referrer the referring GeckoSession, may be null
1662      * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1663      * @param additionalHeaders any additional request headers used with the load
1664      */
1665     @AnyThread
loadUri(final @NonNull String uri, final @Nullable GeckoSession referrer, final @LoadFlags int flags, final @Nullable Map<String, String> additionalHeaders)1666     public void loadUri(final @NonNull String uri, final @Nullable GeckoSession referrer,
1667                         final @LoadFlags int flags, final @Nullable Map<String, String> additionalHeaders) {
1668         // For performance reasons we short-circuit the delegate here
1669         // instead of making Gecko call it for direct loadUri calls.
1670         final NavigationDelegate.LoadRequest request =
1671                 new NavigationDelegate.LoadRequest(
1672                         uri,
1673                         null, /* triggerUri */
1674                         1, /* geckoTarget: OPEN_CURRENTWINDOW */
1675                         0, /* flags */
1676                         false, /* hasUserGesture */
1677                         true /* isDirectNavigation */);
1678 
1679         shouldLoadUri(request).getOrAccept(allowOrDeny -> {
1680             if (allowOrDeny == AllowOrDeny.DENY) {
1681                 return;
1682             }
1683 
1684             final GeckoBundle msg = new GeckoBundle();
1685             msg.putString("uri", uri);
1686             msg.putInt("flags", flags);
1687 
1688             if (referrer != null) {
1689                 msg.putString("referrerSessionId", referrer.mId);
1690             }
1691 
1692             if (additionalHeaders != null) {
1693                 msg.putStringArray("headers", additionalHeadersToStringArray(additionalHeaders));
1694             }
1695 
1696             mEventDispatcher.dispatch("GeckoView:LoadUri", msg);
1697         });
1698     }
1699 
shouldLoadUri(final NavigationDelegate.LoadRequest request)1700     private GeckoResult<AllowOrDeny> shouldLoadUri(final NavigationDelegate.LoadRequest request) {
1701         final NavigationDelegate delegate = mNavigationHandler.getDelegate();
1702         if (delegate == null) {
1703             return GeckoResult.fromValue(AllowOrDeny.ALLOW);
1704         }
1705 
1706         final GeckoResult<AllowOrDeny> result = new GeckoResult<>();
1707 
1708         ThreadUtils.runOnUiThread(() -> {
1709             final GeckoResult<AllowOrDeny> delegateResult =
1710                     delegate.onLoadRequest(this, request);
1711 
1712             if (delegateResult == null) {
1713                 result.complete(AllowOrDeny.ALLOW);
1714             } else {
1715                 delegateResult.getOrAccept(
1716                     allowOrDeny -> result.complete(allowOrDeny),
1717                     error -> result.completeExceptionally(error)
1718                 );
1719             }
1720         });
1721 
1722         return result;
1723     }
1724 
1725     /**
1726      * Load the given URI.
1727      * @param uri The URI of the resource to load.
1728      */
1729     @AnyThread
loadUri(final @NonNull Uri uri)1730     public void loadUri(final @NonNull Uri uri) {
1731         loadUri(uri.toString(), (GeckoSession)null, LOAD_FLAGS_NONE, (Map<String, String>) null);
1732     }
1733 
1734     /**
1735      * Load the given URI with specified HTTP request headers.
1736      * @param uri The URI of the resource to load.
1737      * @param additionalHeaders any additional request headers used with the load
1738      */
1739     @AnyThread
loadUri(final @NonNull Uri uri, final @Nullable Map<String, String> additionalHeaders)1740     public void loadUri(final @NonNull Uri uri, final @Nullable Map<String, String> additionalHeaders) {
1741         loadUri(uri.toString(), (GeckoSession)null, LOAD_FLAGS_NONE, additionalHeaders);
1742     }
1743 
1744     /**
1745      * Load the given URI with the specified referrer and load type.
1746      * @param uri the URI to load
1747      * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1748      */
1749     @AnyThread
loadUri(final @NonNull Uri uri, final @LoadFlags int flags)1750     public void loadUri(final @NonNull Uri uri, final @LoadFlags int flags) {
1751         loadUri(uri.toString(), (GeckoSession)null, flags, (Map<String, String>) null);
1752     }
1753 
1754     /**
1755      * Load the given URI with the specified referrer and load type.
1756      * @param uri the URI to load
1757      * @param referrer the Uri to use as the referrer
1758      * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1759      */
1760     @AnyThread
loadUri(final @NonNull Uri uri, final @Nullable Uri referrer, final @LoadFlags int flags)1761     public void loadUri(final @NonNull Uri uri, final @Nullable Uri referrer,
1762                         final @LoadFlags int flags) {
1763         loadUri(uri.toString(), referrer != null ? referrer.toString() : null, flags, (Map<String, String>) null);
1764     }
1765 
1766     /**
1767      * Load the given URI with the specified referrer, load type and HTTP request headers.
1768      * @param uri the URI to load
1769      * @param referrer the Uri to use as the referrer
1770      * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1771      * @param additionalHeaders any additional request headers used with the load
1772      */
1773     @AnyThread
loadUri(final @NonNull Uri uri, final @Nullable Uri referrer, final @LoadFlags int flags, final @Nullable Map<String, String> additionalHeaders)1774     public void loadUri(final @NonNull Uri uri, final @Nullable Uri referrer,
1775                         final @LoadFlags int flags, final @Nullable Map<String, String> additionalHeaders) {
1776         loadUri(uri.toString(), referrer != null ? referrer.toString() : null, flags, additionalHeaders);
1777     }
1778 
1779     /**
1780      * Load the specified String data. Internally this is converted to a data URI.
1781      *
1782      * @param data a String representing the data
1783      * @param mimeType the mime type of the data, e.g. "text/plain". Maybe be null, in
1784      *                 which case the type is guessed.
1785      *
1786      */
1787     @AnyThread
loadString(@onNull final String data, @Nullable final String mimeType)1788     public void loadString(@NonNull final String data, @Nullable final String mimeType) {
1789         if (data == null) {
1790             throw new IllegalArgumentException("data cannot be null");
1791         }
1792 
1793         loadUri(createDataUri(data, mimeType), (GeckoSession)null, LOAD_FLAGS_NONE);
1794     }
1795 
1796     /**
1797      * Load the specified bytes. Internally this is converted to a data URI.
1798      *
1799      * @param bytes    the data to load
1800      * @param mimeType the mime type of the data, e.g. video/mp4. May be null, in which
1801      *                 case the type is guessed.
1802      */
1803     @AnyThread
loadData(@onNull final byte[] bytes, @Nullable final String mimeType)1804     public void loadData(@NonNull final byte[] bytes, @Nullable final String mimeType) {
1805         if (bytes == null) {
1806             throw new IllegalArgumentException("data cannot be null");
1807         }
1808 
1809         loadUri(createDataUri(bytes, mimeType), (GeckoSession)null, LOAD_FLAGS_FORCE_ALLOW_DATA_URI);
1810     }
1811 
1812     /**
1813      * Creates a data URI of of the form "data:&lt;mime type&gt;,&lt;base64-encoded data&gt;"
1814      * @param bytes the bytes that should be contained in the URL
1815      * @param mimeType optional mime type, e.g. text/plain
1816      * @return a URI String
1817      */
1818     @AnyThread
createDataUri(@onNull final byte[] bytes, @Nullable final String mimeType)1819     public static @NonNull String createDataUri(@NonNull final byte[] bytes,
1820                                                 @Nullable final String mimeType) {
1821         return String.format("data:%s;base64,%s", mimeType != null ? mimeType : "",
1822                              Base64.encodeToString(bytes, Base64.NO_WRAP));
1823     }
1824 
1825     /**
1826      * Creates a data URI of of the form "data:&lt;mime type&gt;,&lt;base64-encoded data&gt;"
1827      * @param data the String data that should be contained in the URL
1828      * @param mimeType optional mime type, e.g. text/plain
1829      * @return a URI String
1830      */
1831     @AnyThread
createDataUri(@onNull final String data, @Nullable final String mimeType)1832     public static @NonNull String createDataUri(@NonNull final String data,
1833                                                 @Nullable final String mimeType) {
1834         return String.format("data:%s,%s", mimeType != null ? mimeType : "", data);
1835     }
1836 
1837     /**
1838     * Reload the current URI.
1839     */
1840     @AnyThread
reload()1841     public void reload() {
1842         reload(LOAD_FLAGS_NONE);
1843     }
1844 
1845     /**
1846     * Reload the current URI.
1847     *
1848     * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*}
1849     */
1850     @AnyThread
reload(final @LoadFlags int flags)1851     public void reload(final @LoadFlags int flags) {
1852         final GeckoBundle msg = new GeckoBundle();
1853         msg.putInt("flags", flags);
1854         mEventDispatcher.dispatch("GeckoView:Reload", msg);
1855     }
1856 
1857     /**
1858     * Stop loading.
1859     */
1860     @AnyThread
stop()1861     public void stop() {
1862         mEventDispatcher.dispatch("GeckoView:Stop", null);
1863     }
1864 
1865     /**
1866     * Go back in history.
1867     */
1868     @AnyThread
goBack()1869     public void goBack() {
1870         mEventDispatcher.dispatch("GeckoView:GoBack", null);
1871     }
1872 
1873     /**
1874     * Go forward in history.
1875     */
1876     @AnyThread
goForward()1877     public void goForward() {
1878         mEventDispatcher.dispatch("GeckoView:GoForward", null);
1879     }
1880 
1881     /**
1882      * Navigate to an index in browser history; the index of the currently
1883      * viewed page can be retrieved from an up-to-date HistoryList by
1884      * calling {@link HistoryDelegate.HistoryList#getCurrentIndex()}.
1885      *
1886      * @param index The index of the location in browser history you want
1887      *              to navigate to.
1888      */
1889     @AnyThread
gotoHistoryIndex(final int index)1890     public void gotoHistoryIndex(final int index) {
1891         final GeckoBundle msg = new GeckoBundle(1);
1892         msg.putInt("index", index);
1893         mEventDispatcher.dispatch("GeckoView:GotoHistoryIndex", msg);
1894     }
1895 
1896     /**
1897      * Returns a WebExtensionController for this GeckoSession. Delegates attached
1898      * to this controller will receive events specific to this session.
1899      *
1900      * @return an instance of {@link WebExtension.SessionController}.
1901      */
1902     @UiThread
getWebExtensionController()1903     public @NonNull WebExtension.SessionController getWebExtensionController() {
1904         return mWebExtensionController;
1905     }
1906 
1907     /**
1908      * Purge history for the session.
1909      * The session history is used for back and forward history.
1910      * Purging the session history means {@link NavigationDelegate#onCanGoBack(GeckoSession, boolean)}
1911      * and {@link NavigationDelegate#onCanGoForward(GeckoSession, boolean)} will be false.
1912      */
1913     @AnyThread
purgeHistory()1914     public void purgeHistory() {
1915         mEventDispatcher.dispatch("GeckoView:PurgeHistory", null);
1916     }
1917 
1918     @Retention(RetentionPolicy.SOURCE)
1919     @IntDef(flag = true,
1920             value = {FINDER_FIND_BACKWARDS, FINDER_FIND_LINKS_ONLY,
1921                     FINDER_FIND_MATCH_CASE, FINDER_FIND_WHOLE_WORD})
1922     /* package */ @interface FinderFindFlags {}
1923 
1924     /** Go backwards when finding the next match. */
1925     public static final int FINDER_FIND_BACKWARDS = 1;
1926     /** Perform case-sensitive match; default is to perform a case-insensitive match. */
1927     public static final int FINDER_FIND_MATCH_CASE = 1 << 1;
1928     /** Must match entire words; default is to allow matching partial words. */
1929     public static final int FINDER_FIND_WHOLE_WORD = 1 << 2;
1930     /** Limit matches to links on the page. */
1931     public static final int FINDER_FIND_LINKS_ONLY = 1 << 3;
1932 
1933     @Retention(RetentionPolicy.SOURCE)
1934     @IntDef(flag = true,
1935             value = {FINDER_DISPLAY_HIGHLIGHT_ALL, FINDER_DISPLAY_DIM_PAGE,
1936                     FINDER_DISPLAY_DRAW_LINK_OUTLINE})
1937     /* package */ @interface FinderDisplayFlags {}
1938 
1939     /** Highlight all find-in-page matches. */
1940     public static final int FINDER_DISPLAY_HIGHLIGHT_ALL = 1;
1941     /** Dim the rest of the page when showing a find-in-page match. */
1942     public static final int FINDER_DISPLAY_DIM_PAGE = 1 << 1;
1943     /** Draw outlines around matching links. */
1944     public static final int FINDER_DISPLAY_DRAW_LINK_OUTLINE = 1 << 2;
1945 
1946     /**
1947      * Represent the result of a find-in-page operation.
1948      */
1949     @AnyThread
1950     public static class FinderResult {
1951         /** Whether a match was found. */
1952         public final boolean found;
1953         /** Whether the search wrapped around the top or bottom of the page. */
1954         public final boolean wrapped;
1955         /** Ordinal number of the current match starting from 1, or 0 if no match. */
1956         public final int current;
1957         /** Total number of matches found so far, or -1 if unknown. */
1958         public final int total;
1959         /** Search string. */
1960         @NonNull public final String searchString;
1961         /** Flags used for the search; either 0 or a combination of {@link #FINDER_FIND_BACKWARDS
1962          * FINDER_FIND_*} flags. */
1963         @FinderFindFlags public final int flags;
1964         /** URI of the link, if the current match is a link, or null otherwise. */
1965         @Nullable public final String linkUri;
1966         /** Bounds of the current match in client coordinates, or null if unknown. */
1967         @Nullable public final RectF clientRect;
1968 
FinderResult(@onNull final GeckoBundle bundle)1969         /* package */ FinderResult(@NonNull final GeckoBundle bundle) {
1970             found = bundle.getBoolean("found");
1971             wrapped = bundle.getBoolean("wrapped");
1972             current = bundle.getInt("current", 0);
1973             total = bundle.getInt("total", -1);
1974             searchString = bundle.getString("searchString");
1975             flags = SessionFinder.getFlagsFromBundle(bundle.getBundle("flags"));
1976             linkUri = bundle.getString("linkURL");
1977 
1978             final GeckoBundle rectBundle = bundle.getBundle("clientRect");
1979             if (rectBundle == null) {
1980                 clientRect = null;
1981             } else {
1982                 clientRect = new RectF((float) rectBundle.getDouble("left"),
1983                                        (float) rectBundle.getDouble("top"),
1984                                        (float) rectBundle.getDouble("right"),
1985                                        (float) rectBundle.getDouble("bottom"));
1986             }
1987         }
1988 
1989         /**
1990          * Empty constructor for tests
1991          */
FinderResult()1992         protected FinderResult() {
1993             found = false;
1994             wrapped = false;
1995             current = 0;
1996             total = 0;
1997             flags = 0;
1998             searchString = "";
1999             linkUri = "";
2000             clientRect = null;
2001         }
2002     }
2003 
2004     /**
2005      * Get the SessionFinder instance for this session, to perform find-in-page operations.
2006      *
2007      * @return SessionFinder instance.
2008      */
2009     @AnyThread
getFinder()2010     public @NonNull SessionFinder getFinder() {
2011         if (mFinder == null) {
2012             mFinder = new SessionFinder(getEventDispatcher());
2013         }
2014         return mFinder;
2015     }
2016 
2017     /**
2018      * Set this GeckoSession as active or inactive, which represents if the session is currently
2019      * visible or not. Setting a GeckoSession to inactive will significantly reduce its memory
2020      * footprint, but should only be done if the GeckoSession is not currently visible. Note that
2021      * a session can be active (i.e. visible) but not focused. When a session is set inactive,
2022      * it will flush the session state and trigger a `ProgressDelegate.onSessionStateChange`
2023      * callback.
2024      *
2025      * @param active A boolean determining whether the GeckoSession is active.
2026      *
2027      * @see #setFocused
2028      */
2029     @AnyThread
setActive(final boolean active)2030     public void setActive(final boolean active) {
2031         final GeckoBundle msg = new GeckoBundle(1);
2032         msg.putBoolean("active", active);
2033         mEventDispatcher.dispatch("GeckoView:SetActive", msg);
2034 
2035         if (!active) {
2036             mEventDispatcher.dispatch("GeckoView:FlushSessionState", null);
2037         }
2038 
2039         ThreadUtils.runOnUiThread(
2040             () -> getAutofillSupport().onActiveChanged(active)
2041         );
2042     }
2043 
2044     /**
2045      * Move focus to this session or away from this session. Only one session has focus at
2046      * a given time. Note that a session can be unfocused but still active (i.e. visible).
2047      *
2048      * @param focused True if the session should gain focus or
2049      *                false if the session should lose focus.
2050      *
2051      * @see #setActive
2052      */
2053     @AnyThread
setFocused(final boolean focused)2054     public void setFocused(final boolean focused) {
2055         final GeckoBundle msg = new GeckoBundle(1);
2056         msg.putBoolean("focused", focused);
2057         mEventDispatcher.dispatch("GeckoView:SetFocused", msg);
2058     }
2059 
2060     /**
2061      * Class representing a saved session state.
2062      */
2063     @AnyThread
2064     public static class SessionState extends AbstractSequentialList<HistoryDelegate.HistoryItem>
2065                                      implements HistoryDelegate.HistoryList, Parcelable {
2066         private GeckoBundle mState;
2067 
2068         private class SessionStateItem implements HistoryDelegate.HistoryItem {
2069             private final GeckoBundle mItem;
2070 
SessionStateItem(final @NonNull GeckoBundle item)2071             private SessionStateItem(final @NonNull GeckoBundle item) {
2072                 mItem = item;
2073             }
2074 
2075             @Override /* HistoryItem */
getUri()2076             public String getUri() {
2077                 return mItem.getString("url");
2078             }
2079 
2080             @Override /* HistoryItem */
getTitle()2081             public String getTitle() {
2082                 return mItem.getString("title");
2083             }
2084         }
2085 
2086         private class SessionStateIterator implements ListIterator<HistoryDelegate.HistoryItem> {
2087             private final SessionState mState;
2088             private int mIndex;
2089 
SessionStateIterator(final @NonNull SessionState state)2090             private SessionStateIterator(final @NonNull SessionState state) {
2091                 this(state, 0);
2092             }
2093 
SessionStateIterator(final @NonNull SessionState state, final int index)2094             private SessionStateIterator(final @NonNull SessionState state, final int index) {
2095                 mIndex = index;
2096                 mState = state;
2097             }
2098 
2099             @Override /* ListIterator */
add(final HistoryDelegate.HistoryItem item)2100             public void add(final HistoryDelegate.HistoryItem item) {
2101                 throw new UnsupportedOperationException();
2102             }
2103 
2104             @Override /* ListIterator */
hasNext()2105             public boolean hasNext() {
2106                 final GeckoBundle[] entries = mState.getHistoryEntries();
2107 
2108                 if (entries == null) {
2109                     Log.w(LOGTAG, "No history entries found.");
2110                     return false;
2111                 }
2112 
2113                 if (mIndex >= mState.getHistoryEntries().length) {
2114                     return false;
2115                 }
2116                 return true;
2117             }
2118 
2119             @Override /* ListIterator */
hasPrevious()2120             public boolean hasPrevious() {
2121                 if (mIndex <= 0) {
2122                     return false;
2123                 }
2124                 return true;
2125             }
2126 
2127             @Override /* ListIterator */
next()2128             public HistoryDelegate.HistoryItem next() {
2129                 if (hasNext()) {
2130                     mIndex++;
2131                     return new SessionStateItem(mState.getHistoryEntries()[mIndex - 1]);
2132                 } else {
2133                     throw new NoSuchElementException();
2134                 }
2135             }
2136 
2137             @Override /* ListIterator */
nextIndex()2138             public int nextIndex() {
2139                 return mIndex;
2140             }
2141 
2142             @Override /* ListIterator */
previous()2143             public HistoryDelegate.HistoryItem previous() {
2144                 if (hasPrevious()) {
2145                     mIndex--;
2146                     return new SessionStateItem(mState.getHistoryEntries()[mIndex]);
2147                 } else {
2148                     throw new NoSuchElementException();
2149                 }
2150             }
2151 
2152             @Override /* ListIterator */
previousIndex()2153             public int previousIndex() {
2154                 return mIndex - 1;
2155             }
2156 
2157             @Override /* ListIterator */
remove()2158             public void remove() {
2159                 throw new UnsupportedOperationException();
2160             }
2161 
2162             @Override /* ListIterator */
set(final @NonNull HistoryDelegate.HistoryItem item)2163             public void set(final @NonNull HistoryDelegate.HistoryItem item) {
2164                 throw new UnsupportedOperationException();
2165             }
2166         }
2167 
SessionState()2168         private SessionState() {
2169             mState = new GeckoBundle(3);
2170         }
2171 
SessionState(final @NonNull GeckoBundle state)2172         private SessionState(final @NonNull GeckoBundle state) {
2173             mState = new GeckoBundle(state);
2174         }
2175 
SessionState(final @NonNull SessionState state)2176         public SessionState(final @NonNull SessionState state) {
2177             mState = new GeckoBundle(state.mState);
2178         }
2179 
updateSessionState(final @NonNull GeckoBundle updateData)2180         /* package */ void updateSessionState(final @NonNull GeckoBundle updateData) {
2181             if (updateData == null) {
2182                 Log.w(LOGTAG, "Session state update has no data field.");
2183                 return;
2184             }
2185 
2186             final GeckoBundle history = updateData.getBundle("historychange");
2187             final GeckoBundle scroll = updateData.getBundle("scroll");
2188             final GeckoBundle formdata = updateData.getBundle("formdata");
2189 
2190             if (history != null) {
2191                 mState.putBundle("history", history);
2192             }
2193 
2194             if (scroll != null) {
2195                 mState.putBundle("scrolldata", scroll);
2196             }
2197 
2198             if (formdata != null) {
2199                 mState.putBundle("formdata", formdata);
2200             }
2201 
2202             return;
2203         }
2204 
2205         @Override
hashCode()2206         public int hashCode() {
2207             return mState.hashCode();
2208         }
2209 
2210         @Override
equals(final Object other)2211         public boolean equals(final Object other) {
2212             if (other == null || !(other instanceof SessionState)) {
2213                 return false;
2214             }
2215 
2216             final SessionState otherState = (SessionState)other;
2217 
2218             return this.mState.equals(otherState.mState);
2219         }
2220 
2221         /**
2222          * Creates a new SessionState instance from a value previously returned by
2223          * {@link #toString()}.
2224          *
2225          * @param value The serialized SessionState in String form.
2226          * @return A new SessionState instance.
2227          * @throws JSONException if the value is not a valid json
2228          */
fromString(final @NonNull String value)2229         public static @NonNull SessionState fromString(final @NonNull String value) throws JSONException {
2230             return new SessionState(GeckoBundle.fromJSONObject(new JSONObject(value)));
2231         }
2232 
2233         @Override
toString()2234         public String toString() {
2235             if (mState == null) {
2236                 Log.w(LOGTAG, "Can't convert SessionState with null state to string");
2237                 return null;
2238             }
2239 
2240             String res;
2241             try {
2242                 res = mState.toJSONObject().toString();
2243             } catch (JSONException e) {
2244                 Log.e(LOGTAG, "Could not convert session state to string.");
2245                 res = null;
2246             }
2247 
2248             return res;
2249         }
2250 
2251         @Override // Parcelable
describeContents()2252         public int describeContents() {
2253             return 0;
2254         }
2255 
2256         @Override // Parcelable
writeToParcel(final Parcel dest, final int flags)2257         public void writeToParcel(final Parcel dest, final int flags) {
2258             dest.writeString(toString());
2259         }
2260 
2261         // AIDL code may call readFromParcel even though it's not part of Parcelable.
readFromParcel(final @NonNull Parcel source)2262         public void readFromParcel(final @NonNull Parcel source) {
2263             if (source.readString() == null) {
2264                 Log.w(LOGTAG, "Can't reproduce session state from Parcel");
2265             }
2266 
2267             try {
2268                 mState = GeckoBundle.fromJSONObject(new JSONObject(source.readString()));
2269             } catch (JSONException e) {
2270                 Log.e(LOGTAG, "Could not convert string to session state.");
2271                 mState = null;
2272             }
2273         }
2274 
2275         public static final Parcelable.Creator<SessionState> CREATOR =
2276                 new Parcelable.Creator<SessionState>() {
2277             @Override
2278             public SessionState createFromParcel(final Parcel source) {
2279                 if (source.readString() == null) {
2280                     Log.w(LOGTAG, "Can't create session state from Parcel");
2281                 }
2282 
2283                 GeckoBundle res;
2284                 try {
2285                     res = GeckoBundle.fromJSONObject(new JSONObject(source.readString()));
2286                 } catch (JSONException e) {
2287                     Log.e(LOGTAG, "Could not convert parcel to session state.");
2288                     res = null;
2289                 }
2290 
2291                 return new SessionState(res);
2292             }
2293 
2294             @Override
2295             public SessionState[] newArray(final int size) {
2296                 return new SessionState[size];
2297             }
2298         };
2299 
2300         @Override /* AbstractSequentialList */
get(final int index)2301         public @NonNull HistoryDelegate.HistoryItem get(final int index) {
2302             final GeckoBundle[] entries = getHistoryEntries();
2303 
2304             if (entries == null || index < 0 || index >= entries.length) {
2305                 throw new NoSuchElementException();
2306             }
2307 
2308             return new SessionStateItem(entries[index]);
2309         }
2310 
2311         @Override /* AbstractSequentialList */
iterator()2312         public @NonNull Iterator<HistoryDelegate.HistoryItem> iterator() {
2313             return listIterator(0);
2314         }
2315 
2316         @Override /* AbstractSequentialList */
listIterator(final int index)2317         public @NonNull ListIterator<HistoryDelegate.HistoryItem> listIterator(final int index) {
2318             return new SessionStateIterator(this, index);
2319         }
2320 
2321         @Override /* AbstractSequentialList */
size()2322         public int size() {
2323             final GeckoBundle[] entries = getHistoryEntries();
2324 
2325             if (entries == null) {
2326                 Log.w(LOGTAG, "No history entries found.");
2327                 return 0;
2328             }
2329 
2330             return entries.length;
2331         }
2332 
2333         @Override /* HistoryList */
getCurrentIndex()2334         public int getCurrentIndex() {
2335             final GeckoBundle history = getHistory();
2336 
2337             if (history == null) {
2338                 throw new IllegalStateException("No history state exists.");
2339             }
2340 
2341             return history.getInt("index") + history.getInt("fromIdx");
2342         }
2343 
2344         // Some helpers for common code.
getHistory()2345         private GeckoBundle getHistory() {
2346             if (mState == null) {
2347                 return null;
2348             }
2349 
2350             return mState.getBundle("history");
2351         }
2352 
getHistoryEntries()2353         private GeckoBundle[] getHistoryEntries() {
2354             final GeckoBundle history = getHistory();
2355 
2356             if (history == null) {
2357                 return null;
2358             }
2359 
2360             return history.getBundleArray("entries");
2361         }
2362     }
2363 
2364     private SessionState mStateCache = new SessionState();
2365 
2366     /**
2367      * Restore a saved state to this GeckoSession; only data that is saved (history, scroll
2368      * position, zoom, and form data) will be restored. These will overwrite the corresponding
2369      * state of this GeckoSession.
2370      *
2371      * @param state A saved session state; this should originate from onSessionStateChange().
2372      */
2373     @AnyThread
restoreState(final @NonNull SessionState state)2374     public void restoreState(final @NonNull SessionState state) {
2375         mEventDispatcher.dispatch("GeckoView:RestoreState", state.mState);
2376     }
2377 
2378     // This is the GeckoDisplay acquired via acquireDisplay(), if any.
2379     private GeckoDisplay mDisplay;
getDisplay()2380     /* package */ GeckoDisplay getDisplay() {
2381         return mDisplay;
2382     }
2383 
2384     /**
2385      * Acquire the GeckoDisplay instance for providing the session with a drawing Surface.
2386      * Be sure to call {@link GeckoDisplay#surfaceChanged(Surface, int, int)} on the
2387      * acquired display if there is already a valid Surface.
2388      *
2389      * @return GeckoDisplay instance.
2390      * @see #releaseDisplay(GeckoDisplay)
2391      */
2392     @UiThread
acquireDisplay()2393     public @NonNull GeckoDisplay acquireDisplay() {
2394         ThreadUtils.assertOnUiThread();
2395 
2396         if (mDisplay != null) {
2397             throw new IllegalStateException("Display already acquired");
2398         }
2399 
2400         mDisplay = new GeckoDisplay(this);
2401         return mDisplay;
2402     }
2403 
2404     /**
2405      * Release an acquired GeckoDisplay instance. Be sure to call {@link
2406      * GeckoDisplay#surfaceDestroyed()} before releasing the display if it still has a
2407      * valid Surface.
2408      *
2409      * @param display Acquired GeckoDisplay instance.
2410      * @see #acquireDisplay()
2411      */
2412     @UiThread
releaseDisplay(final @NonNull GeckoDisplay display)2413     public void releaseDisplay(final @NonNull GeckoDisplay display) {
2414         ThreadUtils.assertOnUiThread();
2415 
2416         if (display != mDisplay) {
2417             throw new IllegalArgumentException("Display not attached");
2418         }
2419 
2420         mDisplay = null;
2421     }
2422 
2423     @AnyThread
getSettings()2424     public @NonNull GeckoSessionSettings getSettings() {
2425         return mSettings;
2426     }
2427 
2428     /**
2429     * Exits fullscreen mode
2430     */
2431     @AnyThread
exitFullScreen()2432     public void exitFullScreen() {
2433         mEventDispatcher.dispatch("GeckoViewContent:ExitFullScreen", null);
2434     }
2435 
2436     /**
2437     * Set the content callback handler.
2438     * This will replace the current handler.
2439     * @param delegate An implementation of ContentDelegate.
2440     */
2441     @UiThread
setContentDelegate(final @Nullable ContentDelegate delegate)2442     public void setContentDelegate(final @Nullable ContentDelegate delegate) {
2443         ThreadUtils.assertOnUiThread();
2444         mContentHandler.setDelegate(delegate, this);
2445         mProcessHangHandler.setDelegate(delegate, this);
2446     }
2447 
2448     /**
2449     * Get the content callback handler.
2450     * @return The current content callback handler.
2451     */
2452     @UiThread
getContentDelegate()2453     public @Nullable ContentDelegate getContentDelegate() {
2454         ThreadUtils.assertOnUiThread();
2455         return mContentHandler.getDelegate();
2456     }
2457 
2458     /**
2459     * Set the progress callback handler.
2460     * This will replace the current handler.
2461     * @param delegate An implementation of ProgressDelegate.
2462     */
2463     @UiThread
setProgressDelegate(final @Nullable ProgressDelegate delegate)2464     public void setProgressDelegate(final @Nullable ProgressDelegate delegate) {
2465         ThreadUtils.assertOnUiThread();
2466         mProgressHandler.setDelegate(delegate, this);
2467     }
2468 
2469     /**
2470     * Get the progress callback handler.
2471     * @return The current progress callback handler.
2472     */
2473     @UiThread
getProgressDelegate()2474     public @Nullable ProgressDelegate getProgressDelegate() {
2475         ThreadUtils.assertOnUiThread();
2476         return mProgressHandler.getDelegate();
2477     }
2478 
2479     /**
2480     * Set the navigation callback handler.
2481     * This will replace the current handler.
2482     * @param delegate An implementation of NavigationDelegate.
2483     */
2484     @UiThread
setNavigationDelegate(final @Nullable NavigationDelegate delegate)2485     public void setNavigationDelegate(final @Nullable NavigationDelegate delegate) {
2486         ThreadUtils.assertOnUiThread();
2487         mNavigationHandler.setDelegate(delegate, this);
2488     }
2489 
2490     /**
2491     * Get the navigation callback handler.
2492     * @return The current navigation callback handler.
2493     */
2494     @UiThread
getNavigationDelegate()2495     public @Nullable NavigationDelegate getNavigationDelegate() {
2496         ThreadUtils.assertOnUiThread();
2497         return mNavigationHandler.getDelegate();
2498     }
2499 
2500     /**
2501     * Set the content scroll callback handler.
2502     * This will replace the current handler.
2503     * @param delegate An implementation of ScrollDelegate.
2504     */
2505     @UiThread
setScrollDelegate(final @Nullable ScrollDelegate delegate)2506     public void setScrollDelegate(final @Nullable ScrollDelegate delegate) {
2507         ThreadUtils.assertOnUiThread();
2508         mScrollHandler.setDelegate(delegate, this);
2509     }
2510 
2511     @UiThread
getScrollDelegate()2512     public @Nullable ScrollDelegate getScrollDelegate() {
2513         ThreadUtils.assertOnUiThread();
2514         return mScrollHandler.getDelegate();
2515     }
2516 
2517     /**
2518      * Set the history tracking delegate for this session, replacing the
2519      * current delegate if one is set.
2520      *
2521      * @param delegate The history tracking delegate, or {@code null} to unset.
2522      */
2523     @AnyThread
setHistoryDelegate(final @Nullable HistoryDelegate delegate)2524     public void setHistoryDelegate(final @Nullable HistoryDelegate delegate) {
2525         mHistoryHandler.setDelegate(delegate, this);
2526     }
2527 
2528     /** @return The history tracking delegate for this session. */
2529     @AnyThread
getHistoryDelegate()2530     public @Nullable HistoryDelegate getHistoryDelegate() {
2531         return mHistoryHandler.getDelegate();
2532     }
2533 
2534     /**
2535     * Set the content blocking callback handler.
2536     * This will replace the current handler.
2537     * @param delegate An implementation of {@link ContentBlocking.Delegate}.
2538     */
2539     @AnyThread
setContentBlockingDelegate(final @Nullable ContentBlocking.Delegate delegate)2540     public void setContentBlockingDelegate(final @Nullable ContentBlocking.Delegate delegate) {
2541         mContentBlockingHandler.setDelegate(delegate, this);
2542     }
2543 
2544     /**
2545     * Get the content blocking callback handler.
2546     * @return The current content blocking callback handler.
2547     */
2548     @AnyThread
getContentBlockingDelegate()2549     public @Nullable ContentBlocking.Delegate getContentBlockingDelegate() {
2550         return mContentBlockingHandler.getDelegate();
2551     }
2552 
2553     /**
2554      * Set the current prompt delegate for this GeckoSession.
2555      * @param delegate PromptDelegate instance or null to use the built-in delegate.
2556      */
2557     @AnyThread
setPromptDelegate(final @Nullable PromptDelegate delegate)2558     public void setPromptDelegate(final @Nullable PromptDelegate delegate) {
2559         mPromptDelegate = delegate;
2560     }
2561 
2562     /**
2563      * Get the current prompt delegate for this GeckoSession.
2564      * @return PromptDelegate instance or null if using built-in delegate.
2565      */
2566     @AnyThread
getPromptDelegate()2567     public @Nullable PromptDelegate getPromptDelegate() {
2568         return mPromptDelegate;
2569     }
2570 
2571     /**
2572      * Set the current selection action delegate for this GeckoSession.
2573      *
2574      * @param delegate SelectionActionDelegate instance or null to unset.
2575      */
2576     @UiThread
setSelectionActionDelegate(final @Nullable SelectionActionDelegate delegate)2577     public void setSelectionActionDelegate(final @Nullable SelectionActionDelegate delegate) {
2578         ThreadUtils.assertOnUiThread();
2579 
2580         if (getSelectionActionDelegate() != null) {
2581             // When the delegate is changed or cleared, make sure onHideAction is called
2582             // one last time to hide any existing selection action UI. Gecko doesn't keep
2583             // track of the old delegate, so we can't rely on Gecko to do that for us.
2584             getSelectionActionDelegate().onHideAction(
2585                     this, GeckoSession.SelectionActionDelegate.HIDE_REASON_NO_SELECTION);
2586         }
2587         mSelectionActionDelegate.setDelegate(delegate, this);
2588     }
2589 
2590     /**
2591      * Set the media callback handler.
2592      * This will replace the current handler.
2593      * @param delegate An implementation of MediaDelegate.
2594      */
2595     @AnyThread
setMediaDelegate(final @Nullable MediaDelegate delegate)2596     public void setMediaDelegate(final @Nullable MediaDelegate delegate) {
2597         mMediaHandler.setDelegate(delegate, this);
2598     }
2599 
2600     /**
2601      * Get the Media callback handler.
2602      * @return The current Media callback handler.
2603      */
2604     @AnyThread
getMediaDelegate()2605     public @Nullable MediaDelegate getMediaDelegate() {
2606         return mMediaHandler.getDelegate();
2607     }
2608 
2609 
2610     /**
2611      * Get the current selection action delegate for this GeckoSession.
2612      *
2613      * @return SelectionActionDelegate instance or null if not set.
2614      */
2615     @AnyThread
getSelectionActionDelegate()2616     public @Nullable SelectionActionDelegate getSelectionActionDelegate() {
2617         return mSelectionActionDelegate.getDelegate();
2618     }
2619 
handlePromptEvent(final GeckoSession session, final GeckoBundle message, final EventCallback callback)2620     /* package */ static void handlePromptEvent(final GeckoSession session,
2621                                                 final GeckoBundle message,
2622                                                 final EventCallback callback) {
2623         final PromptDelegate delegate = session.getPromptDelegate();
2624         if (delegate == null) {
2625             // Default behavior is same as calling dismiss() on callback.
2626             callback.sendSuccess(null);
2627             return;
2628         }
2629 
2630         final String type = message.getString("type");
2631         final String mode = message.getString("mode");
2632         final String title = message.getString("title");
2633         final String msg = message.getString("msg");
2634         GeckoResult<PromptDelegate.PromptResponse> res = null;
2635 
2636         switch (type) {
2637             case "alert": {
2638                 final PromptDelegate.AlertPrompt prompt =
2639                     new PromptDelegate.AlertPrompt(title, msg);
2640                 res = delegate.onAlertPrompt(session, prompt);
2641                 break;
2642             }
2643             case "beforeUnload": {
2644                 final PromptDelegate.BeforeUnloadPrompt prompt =
2645                     new PromptDelegate.BeforeUnloadPrompt();
2646                 res = delegate.onBeforeUnloadPrompt(session, prompt);
2647                 break;
2648             }
2649             case "button": {
2650                 final PromptDelegate.ButtonPrompt prompt =
2651                     new PromptDelegate.ButtonPrompt(title, msg);
2652                 res = delegate.onButtonPrompt(session, prompt);
2653                 break;
2654             }
2655             case "text": {
2656                 final String defaultValue = message.getString("value");
2657                 final PromptDelegate.TextPrompt prompt =
2658                     new PromptDelegate.TextPrompt(title, msg, defaultValue);
2659                 res = delegate.onTextPrompt(session, prompt);
2660                 break;
2661             }
2662             case "auth": {
2663                 final PromptDelegate.AuthPrompt.AuthOptions authOptions =
2664                     new PromptDelegate.AuthPrompt.AuthOptions(message.getBundle("options"));
2665                 final PromptDelegate.AuthPrompt prompt =
2666                     new PromptDelegate.AuthPrompt(title, msg, authOptions);
2667                 res = delegate.onAuthPrompt(session, prompt);
2668                 break;
2669             }
2670             case "choice": {
2671                 final int intMode;
2672                 if ("menu".equals(mode)) {
2673                     intMode = PromptDelegate.ChoicePrompt.Type.MENU;
2674                 } else if ("single".equals(mode)) {
2675                     intMode = PromptDelegate.ChoicePrompt.Type.SINGLE;
2676                 } else if ("multiple".equals(mode)) {
2677                     intMode = PromptDelegate.ChoicePrompt.Type.MULTIPLE;
2678                 } else {
2679                     callback.sendError("Invalid mode");
2680                     return;
2681                 }
2682 
2683                 GeckoBundle[] choiceBundles = message.getBundleArray("choices");
2684                 PromptDelegate.ChoicePrompt.Choice choices[];
2685                 if (choiceBundles == null || choiceBundles.length == 0) {
2686                     choices = new PromptDelegate.ChoicePrompt.Choice[0];
2687                 } else {
2688                     choices = new PromptDelegate.ChoicePrompt.Choice[choiceBundles.length];
2689                     for (int i = 0; i < choiceBundles.length; i++) {
2690                         choices[i] = new PromptDelegate.ChoicePrompt.Choice(choiceBundles[i]);
2691                     }
2692                 }
2693 
2694                 final PromptDelegate.ChoicePrompt prompt =
2695                     new PromptDelegate.ChoicePrompt(title, msg, intMode, choices);
2696                 res = delegate.onChoicePrompt(session, prompt);
2697                 break;
2698             }
2699             case "color": {
2700                 final String defaultValue = message.getString("value");
2701                 final PromptDelegate.ColorPrompt prompt =
2702                     new PromptDelegate.ColorPrompt(title, defaultValue);
2703                 res = delegate.onColorPrompt(session, prompt);
2704                 break;
2705             }
2706             case "datetime": {
2707                 final int intMode;
2708                 if ("date".equals(mode)) {
2709                     intMode = PromptDelegate.DateTimePrompt.Type.DATE;
2710                 } else if ("month".equals(mode)) {
2711                     intMode = PromptDelegate.DateTimePrompt.Type.MONTH;
2712                 } else if ("week".equals(mode)) {
2713                     intMode = PromptDelegate.DateTimePrompt.Type.WEEK;
2714                 } else if ("time".equals(mode)) {
2715                     intMode = PromptDelegate.DateTimePrompt.Type.TIME;
2716                 } else if ("datetime-local".equals(mode)) {
2717                     intMode = PromptDelegate.DateTimePrompt.Type.DATETIME_LOCAL;
2718                 } else {
2719                     callback.sendError("Invalid mode");
2720                     return;
2721                 }
2722 
2723                 final String defaultValue = message.getString("value");
2724                 final String minValue = message.getString("min");
2725                 final String maxValue = message.getString("max");
2726                 final PromptDelegate.DateTimePrompt prompt =
2727                     new PromptDelegate.DateTimePrompt(title, intMode, defaultValue, minValue, maxValue);
2728                 res = delegate.onDateTimePrompt(session, prompt);
2729                 break;
2730             }
2731             case "file": {
2732                 final int intMode;
2733                 if ("single".equals(mode)) {
2734                     intMode = PromptDelegate.FilePrompt.Type.SINGLE;
2735                 } else if ("multiple".equals(mode)) {
2736                     intMode = PromptDelegate.FilePrompt.Type.MULTIPLE;
2737                 } else {
2738                     callback.sendError("Invalid mode");
2739                     return;
2740                 }
2741 
2742                 String[] mimeTypes = message.getStringArray("mimeTypes");
2743                 int capture = message.getInt("capture");
2744                 final PromptDelegate.FilePrompt prompt =
2745                     new PromptDelegate.FilePrompt(title, intMode, capture, mimeTypes);
2746                 res = delegate.onFilePrompt(session, prompt);
2747                 break;
2748             }
2749             case "popup": {
2750                 final String targetUri = message.getString("targetUri");
2751                 final PromptDelegate.PopupPrompt prompt =
2752                     new PromptDelegate.PopupPrompt(targetUri);
2753                 res = delegate.onPopupPrompt(session, prompt);
2754                 break;
2755             }
2756             case "share": {
2757                 final String text = message.getString("text");
2758                 final String uri = message.getString("uri");
2759                 final PromptDelegate.SharePrompt prompt =
2760                     new PromptDelegate.SharePrompt(title, text, uri);
2761                 res = delegate.onSharePrompt(session, prompt);
2762                 break;
2763             }
2764             case "Autocomplete:Save:Login": {
2765                 final int hint = message.getInt("hint");
2766                 final GeckoBundle[] loginBundles =
2767                     message.getBundleArray("logins");
2768 
2769                 if (loginBundles == null) {
2770                     break;
2771                 }
2772 
2773                 final Autocomplete.LoginSaveOption[] options =
2774                     new Autocomplete.LoginSaveOption[loginBundles.length];
2775 
2776                 for (int i = 0; i < options.length; ++i) {
2777                     options[i] = new Autocomplete.LoginSaveOption(
2778                             new Autocomplete.LoginEntry(loginBundles[i]),
2779                             hint);
2780                 }
2781 
2782                 final PromptDelegate.AutocompleteRequest
2783                     <Autocomplete.LoginSaveOption> request =
2784                     new PromptDelegate.AutocompleteRequest<>(options);
2785 
2786                 res = delegate.onLoginSave(session, request);
2787                 break;
2788             }
2789             case "Autocomplete:Select:Login": {
2790                 final GeckoBundle[] optionBundles =
2791                     message.getBundleArray("options");
2792 
2793                 if (optionBundles == null) {
2794                     break;
2795                 }
2796 
2797                 final Autocomplete.LoginSelectOption[] options =
2798                     new Autocomplete.LoginSelectOption[optionBundles.length];
2799 
2800                 for (int i = 0; i < options.length; ++i) {
2801                     options[i] = Autocomplete.LoginSelectOption.fromBundle(
2802                         optionBundles[i]);
2803                 }
2804 
2805                 final PromptDelegate.AutocompleteRequest
2806                     <Autocomplete.LoginSelectOption> request =
2807                     new PromptDelegate.AutocompleteRequest<>(options);
2808 
2809                 res = delegate.onLoginSelect(session, request);
2810 
2811                 break;
2812             }
2813             default: {
2814                 callback.sendError("Invalid type");
2815                 return;
2816             }
2817         }
2818 
2819         if (res == null) {
2820             // Adhere to default behavior if the delegate returns null.
2821             callback.sendSuccess(null);
2822         } else {
2823             res.accept(value -> {
2824                 value.dispatch(callback);
2825             }, exception -> callback.sendError("Failed to get prompt response."));
2826         }
2827     }
2828 
2829     @UiThread
setShouldPinOnScreen(final boolean pinned)2830     protected void setShouldPinOnScreen(final boolean pinned) {
2831         if (DEBUG) {
2832             ThreadUtils.assertOnUiThread();
2833         }
2834 
2835         mShouldPinOnScreen = pinned;
2836     }
2837 
shouldPinOnScreen()2838     /* package */ boolean shouldPinOnScreen() {
2839         ThreadUtils.assertOnUiThread();
2840         return mShouldPinOnScreen;
2841     }
2842 
2843     @AnyThread
getEventDispatcher()2844     /* package */ @NonNull EventDispatcher getEventDispatcher() {
2845         return mEventDispatcher;
2846     }
2847 
2848     public interface ProgressDelegate {
2849         /**
2850          * Class representing security information for a site.
2851          */
2852         public class SecurityInformation {
2853             @Retention(RetentionPolicy.SOURCE)
2854             @IntDef({SECURITY_MODE_UNKNOWN, SECURITY_MODE_IDENTIFIED,
2855                      SECURITY_MODE_VERIFIED})
2856             /* package */ @interface SecurityMode {}
2857             public static final int SECURITY_MODE_UNKNOWN = 0;
2858             public static final int SECURITY_MODE_IDENTIFIED = 1;
2859             public static final int SECURITY_MODE_VERIFIED = 2;
2860 
2861             @Retention(RetentionPolicy.SOURCE)
2862             @IntDef({CONTENT_UNKNOWN, CONTENT_BLOCKED, CONTENT_LOADED})
2863             /* package */ @interface ContentType {}
2864             public static final int CONTENT_UNKNOWN = 0;
2865             public static final int CONTENT_BLOCKED = 1;
2866             public static final int CONTENT_LOADED = 2;
2867             /**
2868              * Indicates whether or not the site is secure.
2869              */
2870             public final boolean isSecure;
2871             /**
2872              * Indicates whether or not the site is a security exception.
2873              */
2874             public final boolean isException;
2875             /**
2876              * Contains the origin of the certificate.
2877              */
2878             public final @Nullable String origin;
2879             /**
2880              * Contains the host associated with the certificate.
2881              */
2882             public final @NonNull String host;
2883 
2884             /**
2885              * The server certificate in use, if any.
2886              */
2887             public final @Nullable X509Certificate certificate;
2888 
2889             /**
2890              * Indicates the security level of the site; possible values are SECURITY_MODE_UNKNOWN,
2891              * SECURITY_MODE_IDENTIFIED, and SECURITY_MODE_VERIFIED. SECURITY_MODE_IDENTIFIED
2892              * indicates domain validation only, while SECURITY_MODE_VERIFIED indicates extended validation.
2893              */
2894             public final @SecurityMode int securityMode;
2895             /**
2896              * Indicates the presence of passive mixed content; possible values are
2897              * CONTENT_UNKNOWN, CONTENT_BLOCKED, and CONTENT_LOADED.
2898              */
2899             public final @ContentType int mixedModePassive;
2900             /**
2901              * Indicates the presence of active mixed content; possible values are
2902              * CONTENT_UNKNOWN, CONTENT_BLOCKED, and CONTENT_LOADED.
2903              */
2904             public final @ContentType int mixedModeActive;
2905 
SecurityInformation(final GeckoBundle identityData)2906             /* package */ SecurityInformation(final GeckoBundle identityData) {
2907                 final GeckoBundle mode = identityData.getBundle("mode");
2908 
2909                 mixedModePassive = mode.getInt("mixed_display");
2910                 mixedModeActive = mode.getInt("mixed_active");
2911 
2912                 securityMode = mode.getInt("identity");
2913 
2914                 isSecure = identityData.getBoolean("secure");
2915                 isException = identityData.getBoolean("securityException");
2916                 origin = identityData.getString("origin");
2917                 host = identityData.getString("host");
2918 
2919                 X509Certificate decodedCert = null;
2920                 try {
2921                     final CertificateFactory factory = CertificateFactory.getInstance("X.509");
2922                     final String certString = identityData.getString("certificate");
2923                     if (certString != null) {
2924                         final byte[] certBytes = Base64.decode(certString, Base64.NO_WRAP);
2925                         decodedCert = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
2926                     }
2927                 } catch (CertificateException e) {
2928                     Log.e(LOGTAG, "Failed to decode certificate", e);
2929                 }
2930 
2931                 certificate = decodedCert;
2932             }
2933 
2934             /**
2935              * Empty constructor for tests
2936              */
SecurityInformation()2937             protected SecurityInformation() {
2938                 mixedModePassive = 0;
2939                 mixedModeActive = 0;
2940                 securityMode = 0;
2941                 isSecure = false;
2942                 isException = false;
2943                 origin = "";
2944                 host = "";
2945                 certificate = null;
2946             }
2947         }
2948 
2949         /**
2950         * A View has started loading content from the network.
2951         * @param session GeckoSession that initiated the callback.
2952         * @param url The resource being loaded.
2953         */
2954         @UiThread
onPageStart(@onNull GeckoSession session, @NonNull String url)2955         default void onPageStart(@NonNull GeckoSession session, @NonNull String url) {}
2956 
2957         /**
2958         * A View has finished loading content from the network.
2959         * @param session GeckoSession that initiated the callback.
2960         * @param success Whether the page loaded successfully or an error occurred.
2961         */
2962         @UiThread
onPageStop(@onNull GeckoSession session, boolean success)2963         default void onPageStop(@NonNull GeckoSession session, boolean success) {}
2964 
2965         /**
2966          * Page loading has progressed.
2967          * @param session GeckoSession that initiated the callback.
2968          * @param progress Current page load progress value [0, 100].
2969          */
2970         @UiThread
onProgressChange(@onNull GeckoSession session, int progress)2971         default void onProgressChange(@NonNull GeckoSession session, int progress) {}
2972 
2973         /**
2974         * The security status has been updated.
2975         * @param session GeckoSession that initiated the callback.
2976         * @param securityInfo The new security information.
2977         */
2978         @UiThread
onSecurityChange(@onNull GeckoSession session, @NonNull SecurityInformation securityInfo)2979         default void onSecurityChange(@NonNull GeckoSession session,
2980                                       @NonNull SecurityInformation securityInfo) {}
2981 
2982         /**
2983         * The browser session state has changed. This can happen in response to
2984         * navigation, scrolling, or form data changes; the session state passed
2985         * includes the most up to date information on all of these.
2986         * @param session GeckoSession that initiated the callback.
2987         * @param sessionState SessionState representing the latest browser state.
2988         */
2989         @UiThread
onSessionStateChange(@onNull GeckoSession session, @NonNull SessionState sessionState)2990         default void onSessionStateChange(@NonNull GeckoSession session,
2991                                           @NonNull SessionState sessionState) {}
2992     }
2993 
2994     /**
2995      * WebResponseInfo contains information about a single web response.
2996      */
2997     @AnyThread
2998     static public class WebResponseInfo {
2999         /**
3000          * The URI of the response. Cannot be null.
3001          */
3002         @NonNull public final String uri;
3003 
3004         /**
3005          * The content type (mime type) of the response. May be null.
3006          */
3007         @Nullable public final String contentType;
3008 
3009         /**
3010          * The content length of the response. May be 0 if unknokwn.
3011          */
3012         @Nullable public final long contentLength;
3013 
3014         /**
3015          * The filename obtained from the content disposition, if any.
3016          * May be null.
3017          */
3018         @Nullable public final String filename;
3019 
WebResponseInfo(final GeckoBundle message)3020         /* package */ WebResponseInfo(final GeckoBundle message) {
3021             uri = message.getString("uri");
3022             if (uri == null) {
3023                 throw new IllegalArgumentException("URI cannot be null");
3024             }
3025 
3026             contentType = message.getString("contentType");
3027             contentLength = message.getLong("contentLength");
3028             filename = message.getString("filename");
3029         }
3030 
3031         /**
3032          * Empty constructor for tests.
3033          */
WebResponseInfo()3034         protected WebResponseInfo() {
3035             uri = "";
3036             contentType = "";
3037             contentLength = 0;
3038             filename = "";
3039         }
3040     }
3041 
3042     public interface ContentDelegate {
3043         /**
3044         * A page title was discovered in the content or updated after the content
3045         * loaded.
3046         * @param session The GeckoSession that initiated the callback.
3047         * @param title The title sent from the content.
3048         */
3049         @UiThread
onTitleChange(@onNull GeckoSession session, @Nullable String title)3050         default void onTitleChange(@NonNull GeckoSession session, @Nullable String title) {}
3051 
3052         /**
3053         * A page has requested focus. Note that window.focus() in content will not result
3054         * in this being called.
3055         * @param session The GeckoSession that initiated the callback.
3056         */
3057         @UiThread
onFocusRequest(@onNull GeckoSession session)3058         default void onFocusRequest(@NonNull GeckoSession session) {}
3059 
3060         /**
3061         * A page has requested to close
3062         * @param session The GeckoSession that initiated the callback.
3063         */
3064         @UiThread
onCloseRequest(@onNull GeckoSession session)3065         default void onCloseRequest(@NonNull GeckoSession session) {}
3066 
3067         /**
3068          * A page has entered or exited full screen mode. Typically, the implementation
3069          * would set the Activity containing the GeckoSession to full screen when the page is
3070          * in full screen mode.
3071          *
3072          * @param session The GeckoSession that initiated the callback.
3073          * @param fullScreen True if the page is in full screen mode.
3074          */
3075         @UiThread
onFullScreen(@onNull GeckoSession session, boolean fullScreen)3076         default void onFullScreen(@NonNull GeckoSession session, boolean fullScreen) {}
3077 
3078         /**
3079          * A viewport-fit was discovered in the content or updated after the content.
3080          *
3081          * @param session The GeckoSession that initiated the callback.
3082          * @param viewportFit The value of viewport-fit of meta element in content.
3083          * @see <a href="https://drafts.csswg.org/css-round-display/#viewport-fit-descriptor">4.1. The viewport-fit descriptor</a>
3084          */
3085         @UiThread
onMetaViewportFitChange(@onNull GeckoSession session, @NonNull String viewportFit)3086         default void onMetaViewportFitChange(@NonNull GeckoSession session, @NonNull String viewportFit) {}
3087 
3088         /**
3089          * Element details for onContextMenu callbacks.
3090          */
3091         public static class ContextElement {
3092             @Retention(RetentionPolicy.SOURCE)
3093             @IntDef({TYPE_NONE, TYPE_IMAGE, TYPE_VIDEO, TYPE_AUDIO})
3094             /* package */ @interface Type {}
3095             public static final int TYPE_NONE = 0;
3096             public static final int TYPE_IMAGE = 1;
3097             public static final int TYPE_VIDEO = 2;
3098             public static final int TYPE_AUDIO = 3;
3099 
3100             /**
3101              * The base URI of the element's document.
3102              */
3103             public final @Nullable String baseUri;
3104 
3105             /**
3106              * The absolute link URI (href) of the element.
3107              */
3108             public final @Nullable String linkUri;
3109 
3110             /**
3111              * The title text of the element.
3112              */
3113             public final @Nullable String title;
3114 
3115             /**
3116              * The alternative text (alt) for the element.
3117              */
3118             public final @Nullable String altText;
3119 
3120             /**
3121              * The type of the element.
3122              * One of the {@link ContextElement#TYPE_NONE} flags.
3123              */
3124             public final @Type int type;
3125 
3126             /**
3127              * The source URI (src) of the element.
3128              * Set for (nested) media elements.
3129              */
3130             public final @Nullable String srcUri;
3131 
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)3132             protected ContextElement(
3133                     final @Nullable String baseUri,
3134                     final @Nullable String linkUri,
3135                     final @Nullable String title,
3136                     final @Nullable String altText,
3137                     final @NonNull String typeStr,
3138                     final @Nullable String srcUri) {
3139                 this.baseUri = baseUri;
3140                 this.linkUri = linkUri;
3141                 this.title = title;
3142                 this.altText = altText;
3143                 this.type = getType(typeStr);
3144                 this.srcUri = srcUri;
3145             }
3146 
getType(final String name)3147             private static int getType(final String name) {
3148                 if ("HTMLImageElement".equals(name)) {
3149                     return TYPE_IMAGE;
3150                 } else if ("HTMLVideoElement".equals(name)) {
3151                     return TYPE_VIDEO;
3152                 } else if ("HTMLAudioElement".equals(name)) {
3153                     return TYPE_AUDIO;
3154                 }
3155                 return TYPE_NONE;
3156             }
3157         }
3158 
3159         /**
3160          * A user has initiated the context menu via long-press.
3161          * This event is fired on links, (nested) images and (nested) media
3162          * elements.
3163          *
3164          * @param session The GeckoSession that initiated the callback.
3165          * @param screenX The screen coordinates of the press.
3166          * @param screenY The screen coordinates of the press.
3167          * @param element The details for the pressed element.
3168          */
3169         @UiThread
onContextMenu(@onNull GeckoSession session, int screenX, int screenY, @NonNull ContextElement element)3170         default void onContextMenu(@NonNull GeckoSession session,
3171                                    int screenX, int screenY,
3172                                    @NonNull ContextElement element) {}
3173 
3174         /**
3175          * This is fired when there is a response that cannot be handled
3176          * by Gecko (e.g., a download).
3177          *
3178          * @param session the GeckoSession that received the external response.
3179          * @param response the WebResponseInfo for the external response
3180          */
3181         @UiThread
onExternalResponse(@onNull GeckoSession session, @NonNull WebResponseInfo response)3182         default void onExternalResponse(@NonNull GeckoSession session,
3183                                         @NonNull WebResponseInfo response) {}
3184 
3185         /**
3186          * The content process hosting this GeckoSession has crashed. The
3187          * GeckoSession is now closed and unusable. You may call
3188          * {@link #open(GeckoRuntime)} to recover the session, but no state
3189          * is preserved. Most applications will want to call
3190          * {@link #loadUri(Uri)} or {@link #restoreState(SessionState)} at this point.
3191          *
3192          * @param session The GeckoSession for which the content process has crashed.
3193          */
3194         @UiThread
onCrash(@onNull GeckoSession session)3195         default void onCrash(@NonNull GeckoSession session) {}
3196 
3197         /**
3198          * The content process hosting this GeckoSession has been killed. The
3199          * GeckoSession is now closed and unusable. You may call
3200          * {@link #open(GeckoRuntime)} to recover the session, but no state
3201          * is preserved. Most applications will want to call
3202          * {@link #loadUri(Uri)} or {@link #restoreState(SessionState)} at this point.
3203          *
3204          * @param session The GeckoSession for which the content process has been killed.
3205          */
3206         @UiThread
onKill(@onNull GeckoSession session)3207         default void onKill(@NonNull GeckoSession session) {}
3208 
3209 
3210         /**
3211          * Notification that the first content composition has occurred.
3212          * This callback is invoked for the first content composite after either
3213          * a start or a restart of the compositor.
3214          * @param session The GeckoSession that had a first paint event.
3215          */
3216         @UiThread
onFirstComposite(@onNull GeckoSession session)3217         default void onFirstComposite(@NonNull GeckoSession session) {}
3218 
3219         /**
3220          * Notification that the first content paint has occurred.
3221          * This callback is invoked for the first content paint after
3222          * a page has been loaded. The function {@link #onFirstComposite(GeckoSession)}
3223          * will be called once the compositor has started rendering. However, it is
3224          * possible for the compositor to start rendering before there is any content to render.
3225          * onFirstContentfulPaint() is called once some content has been rendered. It may be nothing
3226          * more than the page background color. It is not an indication that the whole page has
3227          * been rendered.
3228          * @param session The GeckoSession that had a first paint event.
3229          */
3230         @UiThread
onFirstContentfulPaint(@onNull GeckoSession session)3231         default void onFirstContentfulPaint(@NonNull GeckoSession session) {}
3232 
3233         /**
3234          * This is fired when the loaded document has a valid Web App Manifest present.
3235          *
3236          * The various colors (theme_color, background_color, etc.) present in the manifest
3237          * have been  transformed into #AARRGGBB format.
3238          *
3239          * @param session The GeckoSession that contains the Web App Manifest
3240          * @param manifest A parsed and validated {@link JSONObject} containing the manifest contents.
3241          * @see <a href="https://www.w3.org/TR/appmanifest/">Web App Manifest specification</a>
3242          */
3243         @UiThread
onWebAppManifest(@onNull GeckoSession session, @NonNull JSONObject manifest)3244         default void onWebAppManifest(@NonNull GeckoSession session, @NonNull JSONObject manifest) {}
3245 
3246         /**
3247          * A script has exceeded it's execution timeout value
3248          * @param geckoSession GeckoSession that initiated the callback.
3249          * @param scriptFileName Filename of the slow script
3250          * @return A {@link GeckoResult} with a SlowScriptResponse value which indicates whether to
3251          *         allow the Slow Script to continue processing. Stop will halt the slow script.
3252          *         Continue will pause notifications for a period of time before resuming.
3253          */
3254         @UiThread
onSlowScript(@onNull GeckoSession geckoSession, @NonNull String scriptFileName)3255         default @Nullable GeckoResult<SlowScriptResponse> onSlowScript(@NonNull GeckoSession geckoSession,
3256                                                                        @NonNull String scriptFileName) {
3257             return null;
3258         }
3259     }
3260 
3261     public interface SelectionActionDelegate {
3262         /**
3263          * The selection is collapsed at a single position.
3264          */
3265         final int FLAG_IS_COLLAPSED = 1;
3266         /**
3267          * The selection is inside editable content such as an input element or
3268          * contentEditable node.
3269          */
3270         final int FLAG_IS_EDITABLE = 2;
3271         /**
3272          * The selection is inside a password field.
3273          */
3274         final int FLAG_IS_PASSWORD = 4;
3275 
3276         /**
3277          * Hide selection actions and cause {@link #onHideAction} to be called.
3278          */
3279         final String ACTION_HIDE = "org.mozilla.geckoview.HIDE";
3280         /**
3281          * Copy onto the clipboard then delete the selected content. Selection
3282          * must be editable.
3283          */
3284         final String ACTION_CUT = "org.mozilla.geckoview.CUT";
3285         /**
3286          * Copy the selected content onto the clipboard.
3287          */
3288         final String ACTION_COPY = "org.mozilla.geckoview.COPY";
3289         /**
3290          * Delete the selected content. Selection must be editable.
3291          */
3292         final String ACTION_DELETE = "org.mozilla.geckoview.DELETE";
3293         /**
3294          * Replace the selected content with the clipboard content. Selection
3295          * must be editable.
3296          */
3297         final String ACTION_PASTE = "org.mozilla.geckoview.PASTE";
3298         /**
3299          * Select the entire content of the document or editor.
3300          */
3301         final String ACTION_SELECT_ALL = "org.mozilla.geckoview.SELECT_ALL";
3302         /**
3303          * Clear the current selection. Selection must not be editable.
3304          */
3305         final String ACTION_UNSELECT = "org.mozilla.geckoview.UNSELECT";
3306         /**
3307          * Collapse the current selection to its start position.
3308          * Selection must be editable.
3309          */
3310         final String ACTION_COLLAPSE_TO_START = "org.mozilla.geckoview.COLLAPSE_TO_START";
3311         /**
3312          * Collapse the current selection to its end position.
3313          * Selection must be editable.
3314          */
3315         final String ACTION_COLLAPSE_TO_END = "org.mozilla.geckoview.COLLAPSE_TO_END";
3316 
3317         /**
3318          * Represents attributes of a selection.
3319          */
3320         class Selection {
3321             /**
3322              * Flags describing the current selection, as a bitwise combination
3323              * of the {@link #FLAG_IS_COLLAPSED FLAG_*} constants.
3324              */
3325             public final @SelectionActionDelegateFlag int flags;
3326 
3327             /**
3328              * Text content of the current selection. An empty string indicates the selection
3329              * is collapsed or the selection cannot be represented as plain text.
3330              */
3331             public final @NonNull String text;
3332 
3333             /**
3334              * The bounds of the current selection in client coordinates. Use {@link
3335              * GeckoSession#getClientToScreenMatrix} to perform transformation to screen
3336              * coordinates.
3337              */
3338             public final @Nullable RectF clientRect;
3339 
3340             /**
3341              * Set of valid actions available through {@link Selection#execute(String)}
3342              */
3343             public final @NonNull @SelectionActionDelegateAction Collection<String> availableActions;
3344 
3345             private final int mSeqNo;
3346 
3347             private final EventCallback mEventCallback;
3348 
Selection(final GeckoBundle bundle, final @NonNull @SelectionActionDelegateAction Set<String> actions, final EventCallback callback)3349             /* package */ Selection(final GeckoBundle bundle,
3350                                     final @NonNull @SelectionActionDelegateAction Set<String> actions,
3351                                     final EventCallback callback) {
3352                 flags = (bundle.getBoolean("collapsed") ?
3353                          SelectionActionDelegate.FLAG_IS_COLLAPSED : 0) |
3354                         (bundle.getBoolean("editable") ?
3355                          SelectionActionDelegate.FLAG_IS_EDITABLE : 0) |
3356                         (bundle.getBoolean("password") ?
3357                          SelectionActionDelegate.FLAG_IS_PASSWORD : 0);
3358                 text = bundle.getString("selection");
3359 
3360                 final GeckoBundle rectBundle = bundle.getBundle("clientRect");
3361                 if (rectBundle == null) {
3362                     clientRect = null;
3363                 } else {
3364                     clientRect = new RectF((float) rectBundle.getDouble("left"),
3365                                            (float) rectBundle.getDouble("top"),
3366                                            (float) rectBundle.getDouble("right"),
3367                                            (float) rectBundle.getDouble("bottom"));
3368                 }
3369 
3370                 availableActions = actions;
3371                 mSeqNo = bundle.getInt("seqNo");
3372                 mEventCallback = callback;
3373             }
3374 
3375             /**
3376              * Empty constructor for tests.
3377              */
Selection()3378             protected Selection() {
3379                 flags = 0;
3380                 text = "";
3381                 clientRect = null;
3382                 availableActions = new HashSet<>();
3383                 mSeqNo = 0;
3384                 mEventCallback = null;
3385             }
3386 
3387             /**
3388              * Checks if the passed action is available
3389              * @param action An {@link SelectionActionDelegate} to perform
3390              * @return True if the action is available.
3391              */
3392             @AnyThread
isActionAvailable(@onNull @electionActionDelegateAction final String action)3393             public boolean isActionAvailable(@NonNull @SelectionActionDelegateAction final String action) {
3394                 return availableActions.contains(action);
3395             }
3396 
3397             /**
3398              * Execute an {@link SelectionActionDelegate} action.
3399              *
3400              * @throws IllegalStateException If the action was not available.
3401              * @param action A {@link SelectionActionDelegate} action.
3402              */
3403             @AnyThread
execute(@onNull @electionActionDelegateAction final String action)3404             public void execute(@NonNull @SelectionActionDelegateAction final String action) {
3405                 if (!isActionAvailable(action)) {
3406                     throw new IllegalStateException("Action not available");
3407                 }
3408                 final GeckoBundle response = new GeckoBundle(2);
3409                 response.putString("id", action);
3410                 response.putInt("seqNo", mSeqNo);
3411                 mEventCallback.sendSuccess(response);
3412             }
3413 
3414             /**
3415              * Hide selection actions and cause {@link #onHideAction} to be called.
3416              *
3417              * @throws IllegalStateException If the action was not available.
3418              */
3419             @AnyThread
hide()3420             public void hide() {
3421                 execute(ACTION_HIDE);
3422             }
3423 
3424             /**
3425              * Copy onto the clipboard then delete the selected content.
3426              *
3427              * @throws IllegalStateException If the action was not available.
3428              */
3429             @AnyThread
cut()3430             public void cut() {
3431                 execute(ACTION_CUT);
3432             }
3433 
3434             /**
3435              * Copy the selected content onto the clipboard.
3436              *
3437              * @throws IllegalStateException If the action was not available.
3438              */
3439             @AnyThread
copy()3440             public void copy() {
3441                 execute(ACTION_COPY);
3442             }
3443 
3444             /**
3445              * Delete the selected content.
3446              *
3447              * @throws IllegalStateException If the action was not available.
3448              */
3449             @AnyThread
delete()3450             public void delete() {
3451                 execute(ACTION_DELETE);
3452             }
3453 
3454             /**
3455              * Replace the selected content with the clipboard content.
3456              *
3457              * @throws IllegalStateException If the action was not available.
3458              */
3459             @AnyThread
paste()3460             public void paste() {
3461                 execute(ACTION_PASTE);
3462             }
3463 
3464             /**
3465              * Select the entire content of the document or editor.
3466              *
3467              * @throws IllegalStateException If the action was not available.
3468              */
3469             @AnyThread
selectAll()3470             public void selectAll() {
3471                 execute(ACTION_SELECT_ALL);
3472             }
3473 
3474             /**
3475              * Clear the current selection.
3476              *
3477              * @throws IllegalStateException If the action was not available.
3478              */
3479             @AnyThread
unselect()3480             public void unselect() {
3481                 execute(ACTION_UNSELECT);
3482             }
3483 
3484             /**
3485              * Collapse the current selection to its start position.
3486              *
3487              * @throws IllegalStateException If the action was not available.
3488              */
3489             @AnyThread
collapseToStart()3490             public void collapseToStart() {
3491                 execute(ACTION_COLLAPSE_TO_START);
3492             }
3493 
3494             /**
3495              * Collapse the current selection to its end position.
3496              *
3497              * @throws IllegalStateException If the action was not available.
3498              */
3499             @AnyThread
collapseToEnd()3500             public void collapseToEnd() {
3501                 execute(ACTION_COLLAPSE_TO_END);
3502             }
3503         }
3504 
3505         /**
3506          * Selection actions are available. Selection actions become available when the
3507          * user selects some content in the document or editor. Inside an editor,
3508          * selection actions can also become available when the user explicitly requests
3509          * editor action UI, for example by tapping on the caret handle.
3510          *
3511          * In response to this callback, applications typically display a toolbar
3512          * containing the selection actions. To perform a certain action, check if the action
3513          * is available with {@link Selection#isActionAvailable} then either use the relevant
3514          * helper method or {@link Selection#execute}
3515          *
3516          * Once an {@link #onHideAction} call (with particular reasons) or another {@link
3517          * #onShowActionRequest} call is received, the previous Selection object is no longer
3518          * usable.
3519          *
3520          * @param session The GeckoSession that initiated the callback.
3521          * @param selection Current selection attributes and Callback object for performing built-in
3522          *                  actions. May be used multiple times to perform multiple actions at once.
3523          */
3524         @UiThread
onShowActionRequest(@onNull GeckoSession session, @NonNull Selection selection)3525         default void onShowActionRequest(@NonNull GeckoSession session,
3526                                          @NonNull Selection selection) {}
3527 
3528         /**
3529          * Actions are no longer available due to the user clearing the selection.
3530          */
3531         final int HIDE_REASON_NO_SELECTION = 0;
3532         /**
3533          * Actions are no longer available due to the user moving the selection out of view.
3534          * Previous actions are still available after a callback with this reason.
3535          */
3536         final int HIDE_REASON_INVISIBLE_SELECTION = 1;
3537         /**
3538          * Actions are no longer available due to the user actively changing the
3539          * selection. {@link #onShowActionRequest} may be called again once the user has
3540          * set a selection, if the new selection has available actions.
3541          */
3542         final int HIDE_REASON_ACTIVE_SELECTION = 2;
3543         /**
3544          * Actions are no longer available due to the user actively scrolling the page.
3545          * {@link #onShowActionRequest} may be called again once the user has stopped
3546          * scrolling the page, if the selection is still visible. Until then, previous
3547          * actions are still available after a callback with this reason.
3548          */
3549         final int HIDE_REASON_ACTIVE_SCROLL = 3;
3550 
3551         /**
3552          * Previous actions are no longer available due to the user interacting with the
3553          * page. Applications typically hide the action toolbar in response.
3554          *
3555          * @param session The GeckoSession that initiated the callback.
3556          * @param reason The reason that actions are no longer available, as one of the
3557          * {@link #HIDE_REASON_NO_SELECTION HIDE_REASON_*} constants.
3558          */
3559         @UiThread
onHideAction(@onNull GeckoSession session, @SelectionActionDelegateHideReason int reason)3560         default void onHideAction(@NonNull GeckoSession session,
3561                                   @SelectionActionDelegateHideReason int reason) {}
3562     }
3563 
3564     @Retention(RetentionPolicy.SOURCE)
3565     @StringDef({
3566             SelectionActionDelegate.ACTION_HIDE,
3567             SelectionActionDelegate.ACTION_CUT,
3568             SelectionActionDelegate.ACTION_COPY,
3569             SelectionActionDelegate.ACTION_DELETE,
3570             SelectionActionDelegate.ACTION_PASTE,
3571             SelectionActionDelegate.ACTION_SELECT_ALL,
3572             SelectionActionDelegate.ACTION_UNSELECT,
3573             SelectionActionDelegate.ACTION_COLLAPSE_TO_START,
3574             SelectionActionDelegate.ACTION_COLLAPSE_TO_END})
3575     /* package */ @interface SelectionActionDelegateAction {}
3576 
3577     @Retention(RetentionPolicy.SOURCE)
3578     @IntDef(flag = true, value = {
3579             SelectionActionDelegate.FLAG_IS_COLLAPSED,
3580             SelectionActionDelegate.FLAG_IS_EDITABLE})
3581             /* package */ @interface SelectionActionDelegateFlag {}
3582 
3583     @Retention(RetentionPolicy.SOURCE)
3584     @IntDef({
3585             SelectionActionDelegate.HIDE_REASON_NO_SELECTION,
3586             SelectionActionDelegate.HIDE_REASON_INVISIBLE_SELECTION,
3587             SelectionActionDelegate.HIDE_REASON_ACTIVE_SELECTION,
3588             SelectionActionDelegate.HIDE_REASON_ACTIVE_SCROLL})
3589     /* package */ @interface SelectionActionDelegateHideReason {}
3590 
3591     public interface NavigationDelegate {
3592         /**
3593         * A view has started loading content from the network.
3594         * @param session The GeckoSession that initiated the callback.
3595         * @param url The resource being loaded.
3596         */
3597         @UiThread
onLocationChange(@onNull GeckoSession session, @Nullable String url)3598         default void onLocationChange(@NonNull GeckoSession session, @Nullable String url) {}
3599 
3600         /**
3601         * The view's ability to go back has changed.
3602         * @param session The GeckoSession that initiated the callback.
3603         * @param canGoBack The new value for the ability.
3604         */
3605         @UiThread
onCanGoBack(@onNull GeckoSession session, boolean canGoBack)3606         default void onCanGoBack(@NonNull GeckoSession session, boolean canGoBack) {}
3607 
3608         /**
3609         * The view's ability to go forward has changed.
3610         * @param session The GeckoSession that initiated the callback.
3611         * @param canGoForward The new value for the ability.
3612         */
3613         @UiThread
onCanGoForward(@onNull GeckoSession session, boolean canGoForward)3614         default void onCanGoForward(@NonNull GeckoSession session, boolean canGoForward) {}
3615 
3616         public static final int TARGET_WINDOW_NONE = 0;
3617         public static final int TARGET_WINDOW_CURRENT = 1;
3618         public static final int TARGET_WINDOW_NEW = 2;
3619 
3620         // Match with nsIWebNavigation.idl.
3621         /**
3622          * The load request was triggered by an HTTP redirect.
3623          */
3624         static final int LOAD_REQUEST_IS_REDIRECT = 0x800000;
3625 
3626         /**
3627          * Load request details.
3628          */
3629         public static class LoadRequest {
LoadRequest(@onNull final String uri, @Nullable final String triggerUri, final int geckoTarget, final int flags, final boolean hasUserGesture, final boolean isDirectNavigation)3630             /* package */ LoadRequest(@NonNull final String uri,
3631                                       @Nullable final String triggerUri,
3632                                       final int geckoTarget,
3633                                       final int flags,
3634                                       final boolean hasUserGesture,
3635                                       final boolean isDirectNavigation) {
3636                 this.uri = uri;
3637                 this.triggerUri = triggerUri;
3638                 this.target = convertGeckoTarget(geckoTarget);
3639                 this.isRedirect = (flags & LOAD_REQUEST_IS_REDIRECT) != 0;
3640                 this.hasUserGesture = hasUserGesture;
3641                 this.isDirectNavigation = isDirectNavigation;
3642             }
3643 
3644             /**
3645              * Empty constructor for tests.
3646              */
LoadRequest()3647             protected LoadRequest() {
3648                 uri = "";
3649                 triggerUri = null;
3650                 target = 0;
3651                 isRedirect = false;
3652                 hasUserGesture = false;
3653                 isDirectNavigation = false;
3654             }
3655 
3656             // This needs to match nsIBrowserDOMWindow.idl
convertGeckoTarget(final int geckoTarget)3657             private @TargetWindow int convertGeckoTarget(final int geckoTarget) {
3658                 switch (geckoTarget) {
3659                     case 0: // OPEN_DEFAULTWINDOW
3660                     case 1: // OPEN_CURRENTWINDOW
3661                         return TARGET_WINDOW_CURRENT;
3662                     default: // OPEN_NEWWINDOW, OPEN_NEWTAB, OPEN_SWITCHTAB
3663                         return TARGET_WINDOW_NEW;
3664                 }
3665             }
3666 
3667             /**
3668              * The URI to be loaded.
3669              */
3670             public final @NonNull String uri;
3671 
3672             /**
3673              * The URI of the origin page that triggered the load request.
3674              * null for initial loads and loads originating from data: URIs.
3675              */
3676             public final @Nullable String triggerUri;
3677 
3678             /**
3679              * The target where the window has requested to open.
3680              * One of {@link #TARGET_WINDOW_NONE TARGET_WINDOW_*}.
3681              */
3682             public final @TargetWindow int target;
3683 
3684             /**
3685              * True if and only if the request was triggered by an HTTP redirect.
3686              *
3687              * If the user loads URI "a", which redirects to URI "b", then
3688              * <code>onLoadRequest</code> will be called twice, first with uri "a" and
3689              * <code>isRedirect = false</code>, then with uri "b" and
3690              * <code>isRedirect = true</code>.
3691              */
3692             public final boolean isRedirect;
3693 
3694             /**
3695              * True if there was an active user gesture when the load was requested.
3696              */
3697             public final boolean hasUserGesture;
3698 
3699             /**
3700              * This load request was initiated by a direct navigation from the
3701              * application. E.g. when calling {@link GeckoSession#loadUri}.
3702              */
3703             public final boolean isDirectNavigation;
3704 
3705             @Override
toString()3706             public String toString() {
3707                 final StringBuilder out = new StringBuilder("LoadRequest { ");
3708                 out
3709                     .append("uri: " + uri)
3710                     .append(", triggerUri: " + triggerUri)
3711                     .append(", target: " + target)
3712                     .append(", isRedirect: " + isRedirect)
3713                     .append(", hasUserGesture: " + hasUserGesture)
3714                     .append(", fromLoadUri: " + hasUserGesture)
3715                     .append(" }");
3716                 return out.toString();
3717             }
3718         }
3719 
3720         /**
3721          * A request to open an URI. This is called before each top-level page load to
3722          * allow custom behavior.
3723          * For example, this can be used to override the behavior of
3724          * TAGET_WINDOW_NEW requests, which defaults to requesting a new
3725          * GeckoSession via onNewSession.
3726          *
3727          * @param session The GeckoSession that initiated the callback.
3728          * @param request The {@link LoadRequest} containing the request details.
3729          *
3730          * @return A {@link GeckoResult} with a {@link AllowOrDeny} value which indicates whether
3731          *         or not the load was handled. If unhandled, Gecko will continue the
3732          *         load as normal. If handled (a {@link AllowOrDeny#DENY DENY} value), Gecko
3733          *         will abandon the load. A null return value is interpreted as
3734          *         {@link AllowOrDeny#ALLOW ALLOW} (unhandled).
3735          */
3736         @UiThread
onLoadRequest(@onNull GeckoSession session, @NonNull LoadRequest request)3737         default @Nullable GeckoResult<AllowOrDeny> onLoadRequest(@NonNull GeckoSession session,
3738                                                                  @NonNull LoadRequest request) {
3739             return null;
3740         }
3741 
3742         /**
3743          * A request to load a URI in a non-top-level context.
3744          *
3745          * @param session The GeckoSession that initiated the callback.
3746          * @param request The {@link LoadRequest} containing the request details.
3747          *
3748          * @return A {@link GeckoResult} with a {@link AllowOrDeny} value which indicates whether
3749          *         or not the load was handled. If unhandled, Gecko will continue the
3750          *         load as normal. If handled (a {@link AllowOrDeny#DENY DENY} value), Gecko
3751          *         will abandon the load. A null return value is interpreted as
3752          *         {@link AllowOrDeny#ALLOW ALLOW} (unhandled).
3753          */
3754         @UiThread
onSubframeLoadRequest(@onNull GeckoSession session, @NonNull LoadRequest request)3755         default @Nullable GeckoResult<AllowOrDeny> onSubframeLoadRequest(@NonNull GeckoSession session,
3756                                                                          @NonNull LoadRequest request) {
3757             return null;
3758         }
3759 
3760         /**
3761         * A request has been made to open a new session. The URI is provided only for
3762         * informational purposes. Do not call GeckoSession.loadUri() here. Additionally, the
3763         * returned GeckoSession must be a newly-created one.
3764         *
3765         * @param session The GeckoSession that initiated the callback.
3766         * @param uri The URI to be loaded.
3767         *
3768         * @return A {@link GeckoResult} which holds the returned GeckoSession. May be null, in
3769          *        which case the request for a new window by web content will fail. e.g.,
3770          *        <code>window.open()</code> will return null.
3771          *        The implementation of onNewSession is responsible for maintaining a reference
3772          *        to the returned object, to prevent it from being garbage collected.
3773         */
3774         @UiThread
onNewSession(@onNull GeckoSession session, @NonNull String uri)3775         default @Nullable GeckoResult<GeckoSession> onNewSession(@NonNull GeckoSession session,
3776                                                                  @NonNull String uri) {
3777             return null;
3778         }
3779 
3780         /**
3781          * @param session The GeckoSession that initiated the callback.
3782          * @param uri The URI that failed to load.
3783          * @param error A WebRequestError containing details about the error
3784          * @return A URI to display as an error. Returning null will halt the load entirely.
3785          *         The following special methods are made available to the URI:
3786          *         - document.addCertException(isTemporary), returns Promise
3787          *         - document.getFailedCertSecurityInfo(), returns FailedCertSecurityInfo
3788          *         - document.getNetErrorInfo(), returns NetErrorInfo
3789          * @see <a href="https://searchfox.org/mozilla-central/source/dom/webidl/FailedCertSecurityInfo.webidl">FailedCertSecurityInfo IDL</a>
3790          * @see <a href="https://searchfox.org/mozilla-central/source/dom/webidl/NetErrorInfo.webidl">NetErrorInfo IDL</a>
3791          */
3792         @UiThread
onLoadError(@onNull GeckoSession session, @Nullable String uri, @NonNull WebRequestError error)3793         default @Nullable GeckoResult<String> onLoadError(@NonNull GeckoSession session,
3794                                                           @Nullable String uri,
3795                                                           @NonNull WebRequestError error) {
3796             return null;
3797         }
3798     }
3799 
3800     @Retention(RetentionPolicy.SOURCE)
3801     @IntDef({NavigationDelegate.TARGET_WINDOW_NONE, NavigationDelegate.TARGET_WINDOW_CURRENT,
3802             NavigationDelegate.TARGET_WINDOW_NEW})
3803     /* package */ @interface TargetWindow {}
3804 
3805     /**
3806      * GeckoSession applications implement this interface to handle prompts triggered by
3807      * content in the GeckoSession, such as alerts, authentication dialogs, and select list
3808      * pickers.
3809      **/
3810     public interface PromptDelegate {
3811         /**
3812          * PromptResponse is an opaque class created upon confirming or dismissing a
3813          * prompt.
3814          */
3815         public class PromptResponse {
3816             private final BasePrompt mPrompt;
3817 
PromptResponse(@onNull final BasePrompt prompt)3818             /* package */ PromptResponse(@NonNull final BasePrompt prompt) {
3819                 mPrompt = prompt;
3820             }
3821 
dispatch(@onNull final EventCallback callback)3822             /* package */ void dispatch(@NonNull final EventCallback callback) {
3823                 if (mPrompt == null) {
3824                     throw new RuntimeException("Trying to confirm/dismiss a null prompt.");
3825                 }
3826                 mPrompt.dispatch(callback);
3827             }
3828         }
3829 
3830         // Prompt classes.
3831         public class BasePrompt {
3832             private boolean mIsCompleted;
3833             private boolean mIsConfirmed;
3834             private GeckoBundle mResult;
3835 
3836             /**
3837              * The title of this prompt; may be null.
3838              */
3839             public final @Nullable String title;
3840 
BasePrompt(@ullable final String title)3841             private BasePrompt(@Nullable final String title) {
3842                 this.title = title;
3843                 mIsConfirmed = false;
3844                 mIsCompleted = false;
3845             }
3846 
3847             @UiThread
confirm()3848             protected @NonNull PromptResponse confirm() {
3849                 if (mIsCompleted) {
3850                     throw new RuntimeException("Cannot confirm/dismiss a Prompt twice.");
3851                 }
3852 
3853                 mIsCompleted = true;
3854                 mIsConfirmed = true;
3855                 return new PromptResponse(this);
3856             }
3857 
3858             /**
3859              * This dismisses the prompt without sending any meaningful information back
3860              * to content.
3861              *
3862              * @return A {@link PromptResponse} with which you can complete the
3863              *         {@link GeckoResult} that corresponds to this prompt.
3864              */
3865             @UiThread
dismiss()3866             public @NonNull PromptResponse dismiss() {
3867                 if (mIsCompleted) {
3868                     throw new RuntimeException("Cannot confirm/dismiss a Prompt twice.");
3869                 }
3870 
3871                 mIsCompleted = true;
3872                 return new PromptResponse(this);
3873             }
3874 
ensureResult()3875             /* package */ GeckoBundle ensureResult() {
3876                 if (mResult == null) {
3877                     // Usually result object contains two items.
3878                     mResult = new GeckoBundle(2);
3879                 }
3880                 return mResult;
3881             }
3882 
3883             /**
3884              * This returns true if the prompt has already been confirmed or dismissed.
3885              *
3886              * @return A boolean which is true if the prompt has been confirmed or dismissed,
3887              *         and false otherwise.
3888              */
3889             @UiThread
isComplete()3890             public boolean isComplete() {
3891                 return mIsCompleted;
3892             }
3893 
dispatch(@onNull final EventCallback callback)3894             /* package */ void dispatch(@NonNull final EventCallback callback) {
3895                 if (!mIsCompleted) {
3896                     throw new RuntimeException("Trying to dispatch an incomplete prompt.");
3897                 }
3898 
3899                 if (!mIsConfirmed) {
3900                     callback.sendSuccess(null);
3901                 } else {
3902                     callback.sendSuccess(mResult);
3903                 }
3904             }
3905         }
3906 
3907         /**
3908          * BeforeUnloadPrompt represents the onbeforeunload prompt.
3909          * See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
3910          */
3911         class BeforeUnloadPrompt extends BasePrompt {
BeforeUnloadPrompt()3912             protected BeforeUnloadPrompt() {
3913                 super(null);
3914             }
3915 
3916             /**
3917              * Confirms the prompt.
3918              *
3919              * @param allowOrDeny whether the navigation should be allowed to continue or not.
3920              *
3921              * @return A {@link PromptResponse} which can be used to complete
3922              *         the {@link GeckoResult} associated with this prompt.
3923              */
3924             @UiThread
confirm(final @Nullable AllowOrDeny allowOrDeny)3925             public @NonNull PromptResponse confirm(final @Nullable AllowOrDeny allowOrDeny) {
3926                 ensureResult().putBoolean("allow", allowOrDeny != AllowOrDeny.DENY);
3927                 return super.confirm();
3928             }
3929         }
3930 
3931         /**
3932          * AlertPrompt contains the information necessary to represent a JavaScript
3933          * alert() call from content; it can only be dismissed, not confirmed.
3934          */
3935         public class AlertPrompt extends BasePrompt {
3936             /**
3937              * The message to be displayed with this alert; may be null.
3938              */
3939             public final @Nullable String message;
3940 
AlertPrompt(@ullable final String title, @Nullable final String message)3941             protected AlertPrompt(@Nullable final String title,
3942                                   @Nullable final String message) {
3943                 super(title);
3944                 this.message = message;
3945             }
3946         }
3947 
3948         /**
3949          * ButtonPrompt contains the information necessary to represent a JavaScript
3950          * confirm() call from content.
3951          */
3952         public class ButtonPrompt extends BasePrompt {
3953             @Retention(RetentionPolicy.SOURCE)
3954             @IntDef({Type.POSITIVE, Type.NEGATIVE})
3955             /* package */ @interface ButtonType {}
3956 
3957             public static class Type {
3958                 /**
3959                  * Index of positive response button (eg, "Yes", "OK")
3960                  */
3961                 public static final int POSITIVE = 0;
3962 
3963                 /**
3964                  * Index of negative response button (eg, "No", "Cancel")
3965                  */
3966                 public static final int NEGATIVE = 2;
3967 
Type()3968                 protected Type() {}
3969             }
3970 
3971             /**
3972              * The message to be displayed with this prompt; may be null.
3973              */
3974             public final @Nullable String message;
3975 
ButtonPrompt(@ullable final String title, @Nullable final String message)3976             protected ButtonPrompt(@Nullable final String title,
3977                                    @Nullable final String message) {
3978                 super(title);
3979                 this.message = message;
3980             }
3981 
3982             /**
3983              * Confirms this prompt, returning the selected button to content.
3984              *
3985              * @param selection An int representing the selected button, must be
3986              *                  one of {@link Type}.
3987              *
3988              * @return A {@link PromptResponse} which can be used to complete
3989              *         the {@link GeckoResult} associated with this prompt.
3990              */
3991             @UiThread
confirm(@uttonType final int selection)3992             public @NonNull PromptResponse confirm(@ButtonType final int selection) {
3993                 ensureResult().putInt("button", selection);
3994                 return super.confirm();
3995             }
3996         }
3997 
3998         /**
3999          * TextPrompt contains the information necessary to represent a Javascript
4000          * prompt() call from content.
4001          */
4002         public class TextPrompt extends BasePrompt {
4003             /**
4004              * The message to be displayed with this prompt; may be null.
4005              */
4006             public final @Nullable String message;
4007 
4008             /**
4009              * The default value for the text field; may be null.
4010              */
4011             public final @Nullable String defaultValue;
4012 
TextPrompt(@ullable final String title, @Nullable final String message, @Nullable final String defaultValue)4013             protected TextPrompt(@Nullable final String title,
4014                                  @Nullable final String message,
4015                                  @Nullable final String defaultValue) {
4016                 super(title);
4017                 this.message = message;
4018                 this.defaultValue = defaultValue;
4019             }
4020 
4021             /**
4022              * Confirms this prompt, returning the input text to content.
4023              *
4024              * @param text A String containing the text input given by the user.
4025              *
4026              * @return A {@link PromptResponse} which can be used to complete
4027              *         the {@link GeckoResult} associated with this prompt.
4028              */
4029             @UiThread
confirm(@onNull final String text)4030             public @NonNull PromptResponse confirm(@NonNull final String text) {
4031                 ensureResult().putString("text", text);
4032                 return super.confirm();
4033             }
4034         }
4035 
4036         /**
4037          * AuthPrompt contains the information necessary to represent an HTML
4038          * authorization prompt generated by content.
4039          */
4040         public class AuthPrompt extends BasePrompt {
4041             public static class AuthOptions {
4042                 @Retention(RetentionPolicy.SOURCE)
4043                 @IntDef(flag = true,
4044                         value = {Flags.HOST, Flags.PROXY, Flags.ONLY_PASSWORD,
4045                                  Flags.PREVIOUS_FAILED, Flags.CROSS_ORIGIN_SUB_RESOURCE})
4046                 /* package */ @interface AuthFlag {}
4047 
4048                 /**
4049                  * Auth prompt flags.
4050                  */
4051                 public static class Flags {
4052                     /**
4053                      * The auth prompt is for a network host.
4054                      */
4055                     public static final int HOST = 1;
4056                     /**
4057                      * The auth prompt is for a proxy.
4058                      */
4059                     public static final int PROXY = 2;
4060                     /**
4061                      * The auth prompt should only request a password.
4062                      */
4063                     public static final int ONLY_PASSWORD = 8;
4064                     /**
4065                      * The auth prompt is the result of a previous failed login.
4066                      */
4067                     public static final int PREVIOUS_FAILED = 16;
4068                     /**
4069                      * The auth prompt is for a cross-origin sub-resource.
4070                      */
4071                     public static final int CROSS_ORIGIN_SUB_RESOURCE = 32;
4072 
Flags()4073                     protected Flags() {}
4074                 }
4075 
4076                 @Retention(RetentionPolicy.SOURCE)
4077                 @IntDef({Level.NONE, Level.PW_ENCRYPTED, Level.SECURE})
4078                 /* package */ @interface AuthLevel {}
4079 
4080                 /**
4081                  * Auth prompt levels.
4082                  */
4083                 public static class Level {
4084                     /**
4085                      * The auth request is unencrypted or the encryption status is unknown.
4086                      */
4087                     public static final int NONE = 0;
4088                     /**
4089                      * The auth request only encrypts password but not data.
4090                      */
4091                     public static final int PW_ENCRYPTED = 1;
4092                     /**
4093                      * The auth request encrypts both password and data.
4094                      */
4095                     public static final int SECURE = 2;
4096 
Level()4097                     protected Level() {}
4098                 }
4099 
4100                 /**
4101                  * An int bit-field of {@link Flags}.
4102                  */
4103                 public @AuthFlag final int flags;
4104 
4105                 /**
4106                  * A string containing the URI for the auth request or null if unknown.
4107                  */
4108                 public @Nullable final String uri;
4109 
4110                 /**
4111                  * An int, one of {@link Level}, indicating level of encryption.
4112                  */
4113                 public @AuthLevel final int level;
4114 
4115                 /**
4116                  * A string containing the initial username or null if password-only.
4117                  */
4118                 public @Nullable final String username;
4119 
4120                 /**
4121                  * A string containing the initial password.
4122                  */
4123                 public @Nullable final String password;
4124 
AuthOptions(final GeckoBundle options)4125                 /* package */ AuthOptions(final GeckoBundle options) {
4126                     flags = options.getInt("flags");
4127                     uri = options.getString("uri");
4128                     level = options.getInt("level");
4129                     username = options.getString("username");
4130                     password = options.getString("password");
4131                 }
4132 
4133                 /**
4134                  * Empty constructor for tests
4135                  */
AuthOptions()4136                 protected AuthOptions() {
4137                     flags = 0;
4138                     uri = "";
4139                     level = 0;
4140                     username = "";
4141                     password = "";
4142                 }
4143             }
4144 
4145             /**
4146              * The message to be displayed with this prompt; may be null.
4147              */
4148             public final @Nullable String message;
4149 
4150             /**
4151              * The {@link AuthOptions} that describe the type of authorization prompt.
4152              */
4153             public final @NonNull AuthOptions authOptions;
4154 
AuthPrompt(@ullable final String title, @Nullable final String message, @NonNull final AuthOptions authOptions)4155             protected AuthPrompt(@Nullable final String title,
4156                                  @Nullable final String message,
4157                                  @NonNull final AuthOptions authOptions) {
4158                 super(title);
4159                 this.message = message;
4160                 this.authOptions = authOptions;
4161             }
4162 
4163             /**
4164              * Confirms this prompt with just a password, returning the password to content.
4165              *
4166              * @param password A String containing the password input by the user.
4167              *
4168              * @return A {@link PromptResponse} which can be used to complete
4169              *         the {@link GeckoResult} associated with this prompt.
4170              */
4171             @UiThread
confirm(@onNull final String password)4172             public @NonNull PromptResponse confirm(@NonNull final String password) {
4173                 ensureResult().putString("password", password);
4174                 return super.confirm();
4175             }
4176 
4177             /**
4178              * Confirms this prompt with a username and password, returning both to content.
4179              *
4180              * @param username A String containing the username input by the user.
4181              * @param password A String containing the password input by the user.
4182              *
4183              * @return A {@link PromptResponse} which can be used to complete
4184              *         the {@link GeckoResult} associated with this prompt.
4185              */
4186             @UiThread
confirm(@onNull final String username, @NonNull final String password)4187             public @NonNull PromptResponse confirm(@NonNull final String username,
4188                                                    @NonNull final String password) {
4189                 ensureResult().putString("username", username);
4190                 ensureResult().putString("password", password);
4191                 return super.confirm();
4192             }
4193         }
4194 
4195         /**
4196          * ChoicePrompt contains the information necessary to display a menu or list prompt
4197          * generated by content.
4198          */
4199         public class ChoicePrompt extends BasePrompt {
4200             public static class Choice {
4201                 /**
4202                  * A boolean indicating if the item is disabled. Item should not be
4203                  * selectable if this is true.
4204                  */
4205                 public final boolean disabled;
4206 
4207                 /**
4208                  * A String giving the URI of the item icon, or null if none exists
4209                  * (only valid for menus)
4210                  */
4211                 public final @Nullable String icon;
4212 
4213                 /**
4214                  * A String giving the ID of the item or group
4215                  */
4216                 public final @NonNull String id;
4217 
4218                 /**
4219                  * A Choice array of sub-items in a group, or null if not a group
4220                  */
4221                 public final @Nullable Choice[] items;
4222 
4223                 /**
4224                  * A string giving the label for displaying the item or group
4225                  */
4226                 public final @NonNull String label;
4227 
4228                 /**
4229                  * A boolean indicating if the item should be pre-selected
4230                  * (pre-checked for menu items)
4231                  */
4232                 public final boolean selected;
4233 
4234                 /**
4235                  * A boolean indicating if the item should be a menu separator
4236                  * (only valid for menus)
4237                  */
4238                 public final boolean separator;
4239 
Choice(final GeckoBundle choice)4240                 /* package */ Choice(final GeckoBundle choice) {
4241                     disabled = choice.getBoolean("disabled");
4242                     icon = choice.getString("icon");
4243                     id = choice.getString("id");
4244                     label = choice.getString("label");
4245                     selected = choice.getBoolean("selected");
4246                     separator = choice.getBoolean("separator");
4247 
4248                     GeckoBundle[] choices = choice.getBundleArray("items");
4249                     if (choices == null) {
4250                         items = null;
4251                     } else {
4252                         items = new Choice[choices.length];
4253                         for (int i = 0; i < choices.length; i++) {
4254                             items[i] = new Choice(choices[i]);
4255                         }
4256                     }
4257                 }
4258 
4259                 /**
4260                  * Empty constructor for tests.
4261                  */
Choice()4262                 protected Choice() {
4263                     disabled = false;
4264                     icon = "";
4265                     id = "";
4266                     label = "";
4267                     selected = false;
4268                     separator = false;
4269                     items = null;
4270                 }
4271             }
4272 
4273             @Retention(RetentionPolicy.SOURCE)
4274             @IntDef({Type.MENU, Type.SINGLE, Type.MULTIPLE})
4275             /* package */ @interface ChoiceType {}
4276 
4277             public static class Type {
4278                 /**
4279                  * Display choices in a menu that dismisses as soon as an item is chosen.
4280                  */
4281                 public static final int MENU = 1;
4282 
4283                 /**
4284                  * Display choices in a list that allows a single selection.
4285                  */
4286                 public static final int SINGLE = 2;
4287 
4288                 /**
4289                  * Display choices in a list that allows multiple selections.
4290                  */
4291                 public static final int MULTIPLE = 3;
4292 
Type()4293                 protected Type() {}
4294             }
4295 
4296             /**
4297              * The message to be displayed with this prompt; may be null.
4298              */
4299             public final @Nullable String message;
4300 
4301             /**
4302              * One of {@link Type}.
4303              */
4304             public final @ChoiceType int type;
4305 
4306             /**
4307              * An array of {@link Choice} representing possible choices.
4308              */
4309             public final @NonNull Choice[] choices;
4310 
ChoicePrompt(@ullable final String title, @Nullable final String message, @ChoiceType final int type, @NonNull final Choice[] choices)4311             protected ChoicePrompt(@Nullable final String title,
4312                                    @Nullable final String message,
4313                                    @ChoiceType final int type,
4314                                    @NonNull final Choice[] choices) {
4315                 super(title);
4316                 this.message = message;
4317                 this.type = type;
4318                 this.choices = choices;
4319             }
4320 
4321             /**
4322              * Confirms this prompt with the string id of a single choice.
4323              *
4324              * @param selectedId The string ID of the selected choice.
4325              *
4326              * @return A {@link PromptResponse} which can be used to complete
4327              *         the {@link GeckoResult} associated with this prompt.
4328              */
4329             @UiThread
confirm(@onNull final String selectedId)4330             public @NonNull PromptResponse confirm(@NonNull final String selectedId) {
4331                 return confirm(new String[] { selectedId });
4332             }
4333 
4334             /**
4335              * Confirms this prompt with the string ids of multiple choices
4336              *
4337              * @param selectedIds The string IDs of the selected choices.
4338              *
4339              * @return A {@link PromptResponse} which can be used to complete
4340              *         the {@link GeckoResult} associated with this prompt.
4341              */
4342             @UiThread
confirm(@onNull final String[] selectedIds)4343             public @NonNull PromptResponse confirm(@NonNull final String[] selectedIds) {
4344                 if ((Type.MENU == type || Type.SINGLE == type) &&
4345                     (selectedIds == null || selectedIds.length != 1)) {
4346                     throw new IllegalArgumentException();
4347                 }
4348                 ensureResult().putStringArray("choices", selectedIds);
4349                 return super.confirm();
4350             }
4351 
4352             /**
4353              * Confirms this prompt with a single choice.
4354              *
4355              * @param selectedChoice The selected choice.
4356              *
4357              * @return A {@link PromptResponse} which can be used to complete
4358              *         the {@link GeckoResult} associated with this prompt.
4359              */
4360             @UiThread
confirm(@onNull final Choice selectedChoice)4361             public @NonNull PromptResponse confirm(@NonNull final Choice selectedChoice) {
4362                 return confirm(selectedChoice == null ? null : selectedChoice.id);
4363             }
4364 
4365             /**
4366              * Confirms this prompt with multiple choices.
4367              *
4368              * @param selectedChoices The selected choices.
4369              *
4370              * @return A {@link PromptResponse} which can be used to complete
4371              *         the {@link GeckoResult} associated with this prompt.
4372              */
4373             @UiThread
confirm(@onNull final Choice[] selectedChoices)4374             public @NonNull PromptResponse confirm(@NonNull final Choice[] selectedChoices) {
4375                 if ((Type.MENU == type || Type.SINGLE == type) &&
4376                     (selectedChoices == null || selectedChoices.length != 1)) {
4377                     throw new IllegalArgumentException();
4378                 }
4379 
4380                 if (selectedChoices == null) {
4381                     return confirm((String[]) null);
4382                 }
4383 
4384                 final String[] ids = new String[selectedChoices.length];
4385                 for (int i = 0; i < ids.length; i++) {
4386                     ids[i] = (selectedChoices[i] == null) ? null : selectedChoices[i].id;
4387                 }
4388 
4389                 return confirm(ids);
4390             }
4391         }
4392 
4393         /**
4394          * ColorPrompt contains the information necessary to represent a prompt for color
4395          * input generated by content.
4396          */
4397         public class ColorPrompt extends BasePrompt {
4398             /**
4399              * The default value supplied by content.
4400              */
4401             public final @Nullable String defaultValue;
4402 
ColorPrompt(@ullable final String title, @Nullable final String defaultValue)4403             protected ColorPrompt(@Nullable final String title,
4404                                   @Nullable final String defaultValue) {
4405                 super(title);
4406                 this.defaultValue = defaultValue;
4407             }
4408 
4409             /**
4410              * Confirms the prompt and passes the color value back to content.
4411              *
4412              * @param color A String representing the color to be returned to content.
4413              *
4414              * @return A {@link PromptResponse} which can be used to complete
4415              *         the {@link GeckoResult} associated with this prompt.
4416              */
4417             @UiThread
confirm(@onNull final String color)4418             public @NonNull PromptResponse confirm(@NonNull final String color) {
4419                 ensureResult().putString("color", color);
4420                 return super.confirm();
4421             }
4422         }
4423 
4424         /**
4425          * DateTimePrompt contains the information necessary to represent a prompt for
4426          * date and/or time input generated by content.
4427          */
4428         public class DateTimePrompt extends BasePrompt {
4429             @Retention(RetentionPolicy.SOURCE)
4430             @IntDef({Type.DATE, Type.MONTH, Type.WEEK, Type.TIME, Type.DATETIME_LOCAL})
4431             /* package */ @interface DatetimeType {}
4432 
4433             public static class Type {
4434                 /**
4435                  * Prompt for year, month, and day.
4436                  */
4437                 public static final int DATE = 1;
4438 
4439                 /**
4440                  * Prompt for year and month.
4441                  */
4442                 public static final int MONTH = 2;
4443 
4444                 /**
4445                  * Prompt for year and week.
4446                  */
4447                 public static final int WEEK = 3;
4448 
4449                 /**
4450                  * Prompt for hour and minute.
4451                  */
4452                 public static final int TIME = 4;
4453 
4454                 /**
4455                  * Prompt for year, month, day, hour, and minute, without timezone.
4456                  */
4457                 public static final int DATETIME_LOCAL = 5;
4458 
Type()4459                 protected Type() {}
4460             }
4461 
4462             /**
4463              * One of {@link Type} indicating the type of prompt.
4464              */
4465             public final @DatetimeType int type;
4466 
4467             /**
4468              * A String representing the default value supplied by content.
4469              */
4470             public final @Nullable String defaultValue;
4471 
4472             /**
4473              * A String representing the minimum value allowed by content.
4474              */
4475             public final @Nullable String minValue;
4476 
4477             /**
4478              * A String representing the maximum value allowed by content.
4479              */
4480             public final @Nullable String maxValue;
4481 
DateTimePrompt(@ullable final String title, @DatetimeType final int type, @Nullable final String defaultValue, @Nullable final String minValue, @Nullable final String maxValue)4482             protected DateTimePrompt(@Nullable final String title,
4483                                      @DatetimeType final int type,
4484                                      @Nullable final String defaultValue,
4485                                      @Nullable final String minValue,
4486                                      @Nullable final String maxValue) {
4487                 super(title);
4488                 this.type = type;
4489                 this.defaultValue = defaultValue;
4490                 this.minValue = minValue;
4491                 this.maxValue = maxValue;
4492             }
4493 
4494             /**
4495              * Confirms the prompt and passes the date and/or time value back to content.
4496              *
4497              * @param datetime A String representing the date and time to be returned to content.
4498              *
4499              * @return A {@link PromptResponse} which can be used to complete
4500              *         the {@link GeckoResult} associated with this prompt.
4501              */
4502             @UiThread
confirm(@onNull final String datetime)4503             public @NonNull PromptResponse confirm(@NonNull final String datetime) {
4504                 ensureResult().putString("datetime", datetime);
4505                 return super.confirm();
4506             }
4507         }
4508 
4509         /**
4510          * FilePrompt contains the information necessary to represent a prompt for
4511          * a file or files generated by content.
4512          */
4513         public class FilePrompt extends BasePrompt {
4514             @Retention(RetentionPolicy.SOURCE)
4515             @IntDef({Type.SINGLE, Type.MULTIPLE})
4516             /* package */ @interface FileType {}
4517 
4518             /**
4519              * Types of file prompts.
4520              */
4521             public static class Type {
4522                 /**
4523                  * Prompt for a single file.
4524                  */
4525                 public static final int SINGLE = 1;
4526 
4527                 /**
4528                  * Prompt for multiple files.
4529                  */
4530                 public static final int MULTIPLE = 2;
4531 
Type()4532                 protected Type() {}
4533             }
4534 
4535             @Retention(RetentionPolicy.SOURCE)
4536             @IntDef({Capture.NONE, Capture.ANY, Capture.USER, Capture.ENVIRONMENT})
4537             /* package */ @interface CaptureType {}
4538 
4539             /**
4540              * Possible capture attribute values.
4541              */
4542             public static class Capture {
4543                 // These values should match the corresponding values in nsIFilePicker.idl
4544                 /**
4545                  * No capture attribute has been supplied by content.
4546                  */
4547                 public static final int NONE = 0;
4548 
4549                 /**
4550                  * The capture attribute was supplied with a missing or invalid value.
4551                  */
4552                 public static final int ANY = 1;
4553 
4554                 /**
4555                  * The "user" capture attribute has been supplied by content.
4556                  */
4557                 public static final int USER = 2;
4558 
4559                 /**
4560                  * The "environment" capture attribute has been supplied by content.
4561                  */
4562                 public static final int ENVIRONMENT = 3;
4563 
Capture()4564                 protected Capture() {}
4565             }
4566 
4567             /**
4568              * One of {@link Type} indicating the prompt type.
4569              */
4570             public final @FileType int type;
4571 
4572             /**
4573              * An array of Strings giving the MIME types specified by the "accept" attribute,
4574              * if any are specified.
4575              */
4576             public final @Nullable String[] mimeTypes;
4577 
4578             /**
4579              * One of {@link Capture} indicating the capture attribute supplied by content.
4580              */
4581             public final @CaptureType int capture;
4582 
FilePrompt(@ullable final String title, @FileType final int type, @CaptureType final int capture, @Nullable final String[] mimeTypes)4583             protected FilePrompt(@Nullable final String title,
4584                                  @FileType final int type,
4585                                  @CaptureType final int capture,
4586                                  @Nullable final String[] mimeTypes) {
4587                 super(title);
4588                 this.type = type;
4589                 this.capture = capture;
4590                 this.mimeTypes = mimeTypes;
4591             }
4592 
4593             /**
4594              * Confirms the prompt and passes the file URI back to content.
4595              *
4596              * @param context An Application context for parsing URIs.
4597              * @param uri The URI of the file chosen by the user.
4598              *
4599              * @return A {@link PromptResponse} which can be used to complete
4600              *         the {@link GeckoResult} associated with this prompt.
4601              */
4602             @UiThread
confirm(@onNull final Context context, @NonNull final Uri uri)4603             public @NonNull PromptResponse confirm(@NonNull final Context context,
4604                                                    @NonNull final Uri uri) {
4605                 return confirm(context, new Uri[] { uri });
4606             }
4607 
4608             /**
4609              * Confirms the prompt and passes the file URIs back to content.
4610              *
4611              * @param context An Application context for parsing URIs.
4612              * @param uris The URIs of the files chosen by the user.
4613              *
4614              * @return A {@link PromptResponse} which can be used to complete
4615              *         the {@link GeckoResult} associated with this prompt.
4616              */
4617             @UiThread
confirm(@onNull final Context context, @NonNull final Uri[] uris)4618             public @NonNull PromptResponse confirm(@NonNull final Context context,
4619                                                    @NonNull final Uri[] uris) {
4620                 if (Type.SINGLE == type && (uris == null || uris.length != 1)) {
4621                     throw new IllegalArgumentException();
4622                 }
4623 
4624                 final String[] paths = new String[uris != null ? uris.length : 0];
4625                 for (int i = 0; i < paths.length; i++) {
4626                     paths[i] = getFile(context, uris[i]);
4627                     if (paths[i] == null) {
4628                         Log.e(LOGTAG, "Only file URIs are supported: " + uris[i]);
4629                     }
4630                 }
4631                 ensureResult().putStringArray("files", paths);
4632 
4633                 return super.confirm();
4634             }
4635 
getFile(final @NonNull Context context, final @NonNull Uri uri)4636             private static String getFile(final @NonNull Context context, final @NonNull Uri uri) {
4637                 if (uri == null) {
4638                     return null;
4639                 }
4640                 if ("file".equals(uri.getScheme())) {
4641                     return uri.getPath();
4642                 }
4643                 final ContentResolver cr = context.getContentResolver();
4644                 final Cursor cur = cr.query(uri, new String[] { "_data" }, /* selection */ null,
4645                                             /* args */ null, /* sort */ null);
4646                 if (cur == null) {
4647                     return null;
4648                 }
4649                 try {
4650                     final int idx = cur.getColumnIndex("_data");
4651                     if (idx < 0 || !cur.moveToFirst()) {
4652                         return null;
4653                     }
4654                     do {
4655                         try {
4656                             final String path = cur.getString(idx);
4657                             if (path != null && !path.isEmpty()) {
4658                                 return path;
4659                             }
4660                         } catch (final Exception e) {
4661                         }
4662                     } while (cur.moveToNext());
4663                 } finally {
4664                     cur.close();
4665                 }
4666                 return null;
4667             }
4668         }
4669 
4670         /**
4671          * PopupPrompt contains the information necessary to represent a popup blocking
4672          * request.
4673          */
4674         public class PopupPrompt extends BasePrompt {
4675             /**
4676              * The target URI for the popup; may be null.
4677              */
4678             public final @Nullable String targetUri;
4679 
PopupPrompt(@ullable final String targetUri)4680             protected PopupPrompt(@Nullable final String targetUri) {
4681                 super(null);
4682                 this.targetUri = targetUri;
4683             }
4684 
4685             /**
4686              * Confirms the prompt and either allows or blocks the popup.
4687              *
4688              * @param response An {@link AllowOrDeny} specifying whether to allow or deny the popup.
4689              *
4690              * @return A {@link PromptResponse} which can be used to complete
4691              *         the {@link GeckoResult} associated with this prompt.
4692              */
4693             @UiThread
confirm(@onNull final AllowOrDeny response)4694             public @NonNull PromptResponse confirm(@NonNull final AllowOrDeny response) {
4695                 boolean res = false;
4696                 if (AllowOrDeny.ALLOW == response) {
4697                     res = true;
4698                 }
4699                 ensureResult().putBoolean("response", res);
4700                 return super.confirm();
4701             }
4702         }
4703 
4704         /**
4705          * SharePrompt contains the information necessary to represent a (v1) WebShare request.
4706          */
4707         public class SharePrompt extends BasePrompt {
4708             @Retention(RetentionPolicy.SOURCE)
4709             @IntDef({Result.SUCCESS, Result.FAILURE, Result.ABORT})
4710             /* package */ @interface ShareResult {}
4711 
4712             /**
4713              * Possible results to a {@link SharePrompt}.
4714              */
4715             public static class Result {
4716                 /**
4717                  * The user shared with another app successfully.
4718                  */
4719                 public static final int SUCCESS = 0;
4720 
4721                 /**
4722                  * The user attempted to share with another app, but it failed.
4723                  */
4724                 public static final int FAILURE = 1;
4725 
4726                 /**
4727                  * The user aborted the share.
4728                  */
4729                 public static final int ABORT = 2;
4730 
Result()4731                 protected Result() {}
4732             }
4733 
4734             /**
4735              * The text for the share request.
4736              */
4737             public final @Nullable String text;
4738 
4739             /**
4740              * The uri for the share request.
4741              */
4742             public final @Nullable String uri;
4743 
SharePrompt(@ullable final String title, @Nullable final String text, @Nullable final String uri)4744             protected SharePrompt(@Nullable final String title,
4745                                   @Nullable final String text,
4746                                   @Nullable final String uri) {
4747                 super(title);
4748                 this.text = text;
4749                 this.uri = uri;
4750             }
4751 
4752             /**
4753              * Confirms the prompt and either blocks or allows the share request.
4754              *
4755              * @param response One of {@link Result} specifying the outcome of the
4756              *                 share attempt.
4757              *
4758              * @return A {@link PromptResponse} which can be used to complete the
4759              *         {@link GeckoResult} associated with this prompt.
4760              */
4761             @UiThread
confirm(@hareResult final int response)4762             public @NonNull PromptResponse confirm(@ShareResult final int response) {
4763                 ensureResult().putInt("response", response);
4764                 return super.confirm();
4765             }
4766 
4767             /**
4768              * Dismisses the prompt and returns {@link Result#ABORT} to web content.
4769              *
4770              * @return A {@link PromptResponse} which can be used to complete the
4771              *         {@link GeckoResult} associated with this prompt.
4772              */
4773             @UiThread
dismiss()4774             public @NonNull PromptResponse dismiss() {
4775                 ensureResult().putInt("response", Result.ABORT);
4776                 return super.dismiss();
4777             }
4778         }
4779 
4780         /**
4781          * Request containing information required to resolve Autocomplete
4782          * prompt requests.
4783          */
4784         public class AutocompleteRequest<T extends Autocomplete.Option<?>>
4785                 extends BasePrompt {
4786             /**
4787              * The Autocomplete options for this request.
4788              * This can contain a single or multiple entries.
4789              */
4790             public final @NonNull T[] options;
4791 
AutocompleteRequest(final @NonNull T[] options)4792             protected AutocompleteRequest(final @NonNull T[] options) {
4793                 super(null);
4794                 this.options = options;
4795             }
4796 
4797             /**
4798              * Confirm the request by responding with a selection.
4799              * See the PromptDelegate callbacks for specifics.
4800              *
4801              * @param selection The {@link Autocomplete.Option} used to confirm
4802              *                  the request.
4803              *
4804              * @return A {@link PromptResponse} which can be used to complete
4805              *         the {@link GeckoResult} associated with this prompt.
4806              */
4807             @UiThread
confirm( final @NonNull Autocomplete.Option<?> selection)4808             public @NonNull PromptResponse confirm(
4809                     final @NonNull Autocomplete.Option<?> selection) {
4810                 ensureResult().putBundle("selection", selection.toBundle());
4811                 return super.confirm();
4812             }
4813 
4814             /**
4815              * Dismiss the request.
4816              * See the PromptDelegate callbacks for specifics.
4817              *
4818              * @return A {@link PromptResponse} which can be used to complete
4819              *         the {@link GeckoResult} associated with this prompt.
4820              */
4821             @UiThread
dismiss()4822             public @NonNull PromptResponse dismiss() {
4823                 return super.dismiss();
4824             }
4825         }
4826 
4827         // Delegate functions.
4828         /**
4829          * Display an alert prompt.
4830          *
4831          * @param session GeckoSession that triggered the prompt.
4832          * @param prompt The {@link AlertPrompt} that describes the prompt.
4833          *
4834          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4835          *         includes all necessary information to resolve the prompt.
4836          */
4837         @UiThread
onAlertPrompt(@onNull final GeckoSession session, @NonNull final AlertPrompt prompt)4838         default @Nullable GeckoResult<PromptResponse> onAlertPrompt(@NonNull final GeckoSession session,
4839                                                                     @NonNull final AlertPrompt prompt) {
4840             return null;
4841         }
4842 
4843         /**
4844          * Display a onbeforeunload prompt.
4845          *
4846          * See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
4847          * See {@link BeforeUnloadPrompt}
4848          *
4849          * @param session GeckoSession that triggered the prompt
4850          * @param prompt the {@link BeforeUnloadPrompt} that describes the
4851          *               prompt.
4852          * @return A {@link GeckoResult} resolving to {@link AllowOrDeny#ALLOW}
4853          *         if the page is allowed to continue with the navigation or
4854          *         {@link AllowOrDeny#DENY} otherwise.
4855          */
4856         @UiThread
onBeforeUnloadPrompt( @onNull final GeckoSession session, @NonNull final BeforeUnloadPrompt prompt )4857         default @Nullable GeckoResult<PromptResponse> onBeforeUnloadPrompt(
4858                 @NonNull final GeckoSession session,
4859                 @NonNull final BeforeUnloadPrompt prompt
4860         ) {
4861             return null;
4862         }
4863 
4864         /**
4865          * Display a button prompt.
4866          *
4867          * @param session GeckoSession that triggered the prompt.
4868          * @param prompt The {@link ButtonPrompt} that describes the prompt.
4869          *
4870          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4871          *         includes all necessary information to resolve the prompt.
4872          */
4873         @UiThread
onButtonPrompt(@onNull final GeckoSession session, @NonNull final ButtonPrompt prompt)4874         default @Nullable GeckoResult<PromptResponse> onButtonPrompt(@NonNull final GeckoSession session,
4875                                                                      @NonNull final ButtonPrompt prompt) {
4876             return null;
4877         }
4878 
4879         /**
4880          * Display a text prompt.
4881          *
4882          * @param session GeckoSession that triggered the prompt.
4883          * @param prompt The {@link TextPrompt} that describes the prompt.
4884          *
4885          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4886          *         includes all necessary information to resolve the prompt.
4887          */
4888         @UiThread
onTextPrompt(@onNull final GeckoSession session, @NonNull final TextPrompt prompt)4889         default @Nullable GeckoResult<PromptResponse> onTextPrompt(@NonNull final GeckoSession session,
4890                                                                    @NonNull final TextPrompt prompt) {
4891             return null;
4892         }
4893 
4894         /**
4895          * Display an authorization prompt.
4896          *
4897          * @param session GeckoSession that triggered the prompt.
4898          * @param prompt The {@link AuthPrompt} that describes the prompt.
4899          *
4900          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4901          *         includes all necessary information to resolve the prompt.
4902          */
4903         @UiThread
onAuthPrompt(@onNull final GeckoSession session, @NonNull final AuthPrompt prompt)4904         default @Nullable GeckoResult<PromptResponse> onAuthPrompt(@NonNull final GeckoSession session,
4905                                                                    @NonNull final AuthPrompt prompt) {
4906             return null;
4907         }
4908 
4909         /**
4910          * Display a list/menu prompt.
4911          *
4912          * @param session GeckoSession that triggered the prompt.
4913          * @param prompt The {@link ChoicePrompt} that describes the prompt.
4914          *
4915          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4916          *         includes all necessary information to resolve the prompt.
4917          */
4918         @UiThread
onChoicePrompt(@onNull final GeckoSession session, @NonNull final ChoicePrompt prompt)4919         default @Nullable GeckoResult<PromptResponse> onChoicePrompt(@NonNull final GeckoSession session,
4920                                                                      @NonNull final ChoicePrompt prompt) {
4921             return null;
4922         }
4923 
4924         /**
4925          * Display a color prompt.
4926          *
4927          * @param session GeckoSession that triggered the prompt.
4928          * @param prompt The {@link ColorPrompt} that describes the prompt.
4929          *
4930          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4931          *         includes all necessary information to resolve the prompt.
4932          */
4933         @UiThread
onColorPrompt(@onNull final GeckoSession session, @NonNull final ColorPrompt prompt)4934         default @Nullable GeckoResult<PromptResponse> onColorPrompt(@NonNull final GeckoSession session,
4935                                                                     @NonNull final ColorPrompt prompt) {
4936             return null;
4937         }
4938 
4939         /**
4940          * Display a date/time prompt.
4941          *
4942          * @param session GeckoSession that triggered the prompt.
4943          * @param prompt The {@link DateTimePrompt} that describes the prompt.
4944          *
4945          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4946          *         includes all necessary information to resolve the prompt.
4947          */
4948         @UiThread
onDateTimePrompt(@onNull final GeckoSession session, @NonNull final DateTimePrompt prompt)4949         default @Nullable GeckoResult<PromptResponse> onDateTimePrompt(@NonNull final GeckoSession session,
4950                                                                        @NonNull final DateTimePrompt prompt) {
4951             return null;
4952         }
4953 
4954         /**
4955          * Display a file prompt.
4956          *
4957          * @param session GeckoSession that triggered the prompt.
4958          * @param prompt The {@link FilePrompt} that describes the prompt.
4959          *
4960          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4961          *         includes all necessary information to resolve the prompt.
4962          */
4963         @UiThread
onFilePrompt(@onNull final GeckoSession session, @NonNull final FilePrompt prompt)4964         default @Nullable GeckoResult<PromptResponse> onFilePrompt(@NonNull final GeckoSession session,
4965                                                                    @NonNull final FilePrompt prompt) {
4966             return null;
4967         }
4968 
4969         /**
4970          * Display a popup request prompt; this occurs when content attempts to open
4971          * a new window in a way that doesn't appear to be the result of user input.
4972          *
4973          * @param session GeckoSession that triggered the prompt.
4974          * @param prompt The {@link PopupPrompt} that describes the prompt.
4975          *
4976          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4977          *         includes all necessary information to resolve the prompt.
4978          */
4979         @UiThread
onPopupPrompt(@onNull final GeckoSession session, @NonNull final PopupPrompt prompt)4980         default @Nullable GeckoResult<PromptResponse> onPopupPrompt(@NonNull final GeckoSession session,
4981                                                                     @NonNull final PopupPrompt prompt) {
4982             return null;
4983         }
4984 
4985         /**
4986          * Display a share request prompt; this occurs when content attempts to use the
4987          * WebShare API.
4988          * See: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
4989          *
4990          * @param session GeckoSession that triggered the prompt.
4991          * @param prompt The {@link SharePrompt} that describes the prompt.
4992          *
4993          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
4994          *         includes all necessary information to resolve the prompt.
4995          */
4996         @UiThread
onSharePrompt(@onNull final GeckoSession session, @NonNull final SharePrompt prompt)4997         default @Nullable GeckoResult<PromptResponse> onSharePrompt(@NonNull final GeckoSession session,
4998                                                                     @NonNull final SharePrompt prompt) {
4999             return null;
5000         }
5001 
5002         /**
5003          * Handle a login save prompt request.
5004          * This is triggered by the user entering new or modified login
5005          * credentials into a login form.
5006          *
5007          * @param session The {@link GeckoSession} that triggered the request.
5008          * @param request The {@link AutocompleteRequest} containing the request
5009          *                details.
5010          *
5011          * @return A {@link GeckoResult} resolving to a {@link PromptResponse}.
5012          *
5013          *         Confirm the request with an {@link Autocomplete.Option}
5014          *         to trigger a
5015          *         {@link Autocomplete.LoginStorageDelegate#onLoginSave} request
5016          *         to save the given selection.
5017          *         The confirmed selection may be an entry out of the request's
5018          *         options, a modified option, or a freshly created login entry.
5019          *
5020          *         Dismiss the request to deny the saving request.
5021          */
5022         @UiThread
onLoginSave( @onNull final GeckoSession session, @NonNull final AutocompleteRequest<Autocomplete.LoginSaveOption> request)5023         default @Nullable GeckoResult<PromptResponse> onLoginSave(
5024                 @NonNull final GeckoSession session,
5025                 @NonNull final AutocompleteRequest<Autocomplete.LoginSaveOption>
5026                     request) {
5027             return null;
5028         }
5029 
5030         /**
5031          * Handle a login selection prompt request.
5032          * This is triggered by the user focusing on a login username field.
5033          *
5034          * @param session The {@link GeckoSession} that triggered the request.
5035          * @param request The {@link AutocompleteRequest} containing the request
5036          *                details.
5037          *
5038          * @return A {@link GeckoResult} resolving to a {@link PromptResponse}
5039          *
5040          *         Confirm the request with an {@link Autocomplete.Option}
5041          *         to let GeckoView fill out the login forms with the given
5042          *         selection details.
5043          *         The confirmed selection may be an entry out of the request's
5044          *         options, a modified option, or a freshly created login entry.
5045          *
5046          *         Dismiss the request to deny autocompletion for the detected
5047          *         form.
5048          */
5049         @UiThread
onLoginSelect( @onNull final GeckoSession session, @NonNull final AutocompleteRequest<Autocomplete.LoginSelectOption> request)5050         default @Nullable GeckoResult<PromptResponse> onLoginSelect(
5051                 @NonNull final GeckoSession session,
5052                 @NonNull final AutocompleteRequest<Autocomplete.LoginSelectOption>
5053                     request) {
5054             return null;
5055         }
5056     }
5057 
5058     /**
5059      * GeckoSession applications implement this interface to handle content scroll
5060      * events.
5061      **/
5062     public interface ScrollDelegate {
5063         /**
5064          * The scroll position of the content has changed.
5065          *
5066         * @param session GeckoSession that initiated the callback.
5067         * @param scrollX The new horizontal scroll position in pixels.
5068         * @param scrollY The new vertical scroll position in pixels.
5069         */
5070         @UiThread
onScrollChanged(@onNull GeckoSession session, int scrollX, int scrollY)5071         default void onScrollChanged(@NonNull GeckoSession session, int scrollX, int scrollY) {}
5072     }
5073 
5074     /**
5075      * Get the PanZoomController instance for this session.
5076      *
5077      * @return PanZoomController instance.
5078      */
5079     @UiThread
getPanZoomController()5080     public @NonNull PanZoomController getPanZoomController() {
5081         ThreadUtils.assertOnUiThread();
5082 
5083         return mPanZoomController;
5084     }
5085 
5086     /**
5087      * Get the OverscrollEdgeEffect instance for this session.
5088      *
5089      * @return OverscrollEdgeEffect instance.
5090      */
5091     @UiThread
getOverscrollEdgeEffect()5092     public @NonNull OverscrollEdgeEffect getOverscrollEdgeEffect() {
5093         ThreadUtils.assertOnUiThread();
5094 
5095         if (mOverscroll == null) {
5096             mOverscroll = new OverscrollEdgeEffect(this);
5097         }
5098         return mOverscroll;
5099     }
5100 
5101     /**
5102      * Get the CompositorController instance for this session.
5103      *
5104      * @return CompositorController instance.
5105      */
5106     @UiThread
getCompositorController()5107     public @NonNull CompositorController getCompositorController() {
5108         ThreadUtils.assertOnUiThread();
5109 
5110         if (mController == null) {
5111             mController = new CompositorController(this);
5112             if (mCompositorReady) {
5113                 mController.onCompositorReady();
5114             }
5115         }
5116         return mController;
5117     }
5118 
5119     /**
5120      * Get a matrix for transforming from client coordinates to surface coordinates.
5121      *
5122      * @param matrix Matrix to be replaced by the transformation matrix.
5123      * @see #getClientToScreenMatrix(Matrix)
5124      * @see #getPageToSurfaceMatrix(Matrix)
5125      */
5126     @UiThread
getClientToSurfaceMatrix(@onNull final Matrix matrix)5127     public void getClientToSurfaceMatrix(@NonNull final Matrix matrix) {
5128         ThreadUtils.assertOnUiThread();
5129 
5130         matrix.setScale(mViewportZoom, mViewportZoom);
5131         if (mClientTop != mTop) {
5132             matrix.postTranslate(0, mClientTop - mTop);
5133         }
5134     }
5135 
5136     /**
5137      * Get a matrix for transforming from client coordinates to screen coordinates. The
5138      * client coordinates are in CSS pixels and are relative to the viewport origin; their
5139      * relation to screen coordinates does not depend on the current scroll position.
5140      *
5141      * @param matrix Matrix to be replaced by the transformation matrix.
5142      * @see #getClientToSurfaceMatrix(Matrix)
5143      * @see #getPageToScreenMatrix(Matrix)
5144      */
5145     @UiThread
getClientToScreenMatrix(@onNull final Matrix matrix)5146     public void getClientToScreenMatrix(@NonNull final Matrix matrix) {
5147         ThreadUtils.assertOnUiThread();
5148 
5149         getClientToSurfaceMatrix(matrix);
5150         matrix.postTranslate(mLeft, mTop);
5151     }
5152 
5153     /**
5154      * Get a matrix for transforming from page coordinates to screen coordinates. The page
5155      * coordinates are in CSS pixels and are relative to the page origin; their relation
5156      * to screen coordinates depends on the current scroll position of the outermost
5157      * frame.
5158      *
5159      * @param matrix Matrix to be replaced by the transformation matrix.
5160      * @see #getPageToSurfaceMatrix(Matrix)
5161      * @see #getClientToScreenMatrix(Matrix)
5162      */
5163     @UiThread
getPageToScreenMatrix(@onNull final Matrix matrix)5164     public void getPageToScreenMatrix(@NonNull final Matrix matrix) {
5165         ThreadUtils.assertOnUiThread();
5166 
5167         getPageToSurfaceMatrix(matrix);
5168         matrix.postTranslate(mLeft, mTop);
5169     }
5170 
5171     /**
5172      * Get a matrix for transforming from page coordinates to surface coordinates.
5173      *
5174      * @param matrix Matrix to be replaced by the transformation matrix.
5175      * @see #getPageToScreenMatrix(Matrix)
5176      * @see #getClientToSurfaceMatrix(Matrix)
5177      */
5178     @UiThread
getPageToSurfaceMatrix(@onNull final Matrix matrix)5179     public void getPageToSurfaceMatrix(@NonNull final Matrix matrix) {
5180         ThreadUtils.assertOnUiThread();
5181 
5182         getClientToSurfaceMatrix(matrix);
5183         matrix.postTranslate(-mViewportLeft, -mViewportTop);
5184     }
5185 
5186     /**
5187      * Get the bounds of the client area in client coordinates. The returned top-left
5188      * coordinates are always (0, 0). Use the matrix from {@link
5189      * #getClientToSurfaceMatrix(Matrix)} or {@link #getClientToScreenMatrix(Matrix)} to
5190      * map these bounds to surface or screen coordinates, respectively.
5191      *
5192      * @param rect RectF to be replaced by the client bounds in client coordinates.
5193      * @see #getSurfaceBounds(Rect)
5194      */
5195     @UiThread
getClientBounds(@onNull final RectF rect)5196     public void getClientBounds(@NonNull final RectF rect) {
5197         ThreadUtils.assertOnUiThread();
5198 
5199         rect.set(0.0f, 0.0f, (float) mWidth / mViewportZoom,
5200                 (float) mClientHeight / mViewportZoom);
5201     }
5202 
5203     /**
5204      * Get the bounds of the client area in surface coordinates. This is equivalent to
5205      * mapping the bounds returned by #getClientBounds(RectF) with the matrix returned by
5206      * #getClientToSurfaceMatrix(Matrix).
5207      *
5208      * @param rect Rect to be replaced by the client bounds in surface coordinates.
5209      */
5210     @UiThread
getSurfaceBounds(@onNull final Rect rect)5211     public void getSurfaceBounds(@NonNull final Rect rect) {
5212         ThreadUtils.assertOnUiThread();
5213 
5214         rect.set(0, mClientTop - mTop, mWidth, mHeight);
5215     }
5216 
5217     /**
5218      * GeckoSession applications implement this interface to handle requests for permissions
5219      * from content, such as geolocation and notifications. For each permission, usually
5220      * two requests are generated: one request for the Android app permission through
5221      * requestAppPermissions, which is typically handled by a system permission dialog;
5222      * and another request for the content permission (e.g. through
5223      * requestContentPermission), which is typically handled by an app-specific
5224      * permission dialog.
5225      *
5226      *
5227      * When denying an Android app permission, the response is not stored by GeckoView.
5228      * It is the responsibility of the consumer to store the response state and therefore prevent
5229      * further requests from being presented to the user.
5230      **/
5231     public interface PermissionDelegate {
5232         /**
5233          * Permission for using the geolocation API.
5234          * See: https://developer.mozilla.org/en-US/docs/Web/API/Geolocation
5235          */
5236         int PERMISSION_GEOLOCATION = 0;
5237 
5238         /**
5239          * Permission for using the notifications API.
5240          * See: https://developer.mozilla.org/en-US/docs/Web/API/notification
5241          */
5242         int PERMISSION_DESKTOP_NOTIFICATION = 1;
5243 
5244         /**
5245          * Permission for using the storage API.
5246          * See: https://developer.mozilla.org/en-US/docs/Web/API/Storage_API
5247          */
5248         int PERMISSION_PERSISTENT_STORAGE = 2;
5249 
5250         /**
5251          * Permission for using the WebXR API.
5252          * See: https://www.w3.org/TR/webxr
5253          */
5254         int PERMISSION_XR = 3;
5255 
5256         /**
5257          * Permission for allowing autoplay of inaudible (silent) video.
5258          */
5259         int PERMISSION_AUTOPLAY_INAUDIBLE = 4;
5260 
5261         /**
5262          * Permission for allowing autoplay of audible video.
5263          */
5264         int PERMISSION_AUTOPLAY_AUDIBLE = 5;
5265 
5266         /**
5267          * Permission for accessing system media keys used to decode DRM media.
5268          */
5269         int PERMISSION_MEDIA_KEY_SYSTEM_ACCESS = 6;
5270 
5271         /**
5272          * Callback interface for notifying the result of a permission request.
5273          */
5274         interface Callback {
5275             /**
5276              * Called by the implementation after permissions are granted; the
5277              * implementation must call either grant() or reject() for every request.
5278              */
5279             @UiThread
grant()5280             default void grant() {}
5281 
5282             /**
5283              * Called by the implementation when permissions are not granted; the
5284              * implementation must call either grant() or reject() for every request.
5285              */
5286             @UiThread
reject()5287             default void reject() {}
5288         }
5289 
5290         /**
5291          * Request Android app permissions.
5292          *
5293          * @param session GeckoSession instance requesting the permissions.
5294          * @param permissions List of permissions to request; possible values are,
5295          *                    android.Manifest.permission.ACCESS_COARSE_LOCATION
5296          *                    android.Manifest.permission.ACCESS_FINE_LOCATION
5297          *                    android.Manifest.permission.CAMERA
5298          *                    android.Manifest.permission.RECORD_AUDIO
5299          * @param callback Callback interface.
5300          */
5301         @UiThread
onAndroidPermissionsRequest(@onNull GeckoSession session, @Nullable String[] permissions, @NonNull Callback callback)5302         default void onAndroidPermissionsRequest(@NonNull GeckoSession session,
5303                                                  @Nullable String[] permissions,
5304                                                  @NonNull Callback callback) {
5305             callback.reject();
5306         }
5307 
5308         /**
5309          * Request content permission.
5310          *
5311          * Note, that in the case of PERMISSION_PERSISTENT_STORAGE, once permission has been granted
5312          * for a site, it cannot be revoked. If the permission has previously been granted, it is
5313          * the responsibility of the consuming app to remember the permission and prevent the prompt
5314          * from being redisplayed to the user.
5315          *
5316          * @param session GeckoSession instance requesting the permission.
5317          * @param uri The URI of the content requesting the permission.
5318          * @param type The type of the requested permission; possible values are,
5319          *             PERMISSION_GEOLOCATION
5320          *             PERMISSION_DESKTOP_NOTIFICATION
5321          *             PERMISSION_PERSISTENT_STORAGE
5322          *             PERMISSION_XR
5323          * @param callback Callback interface.
5324          */
5325         @UiThread
onContentPermissionRequest(@onNull GeckoSession session, @Nullable String uri, @Permission int type, @NonNull Callback callback)5326         default void onContentPermissionRequest(@NonNull GeckoSession session, @Nullable String uri,
5327                                                 @Permission int type, @NonNull Callback callback) {
5328             callback.reject();
5329         }
5330 
5331         class MediaSource {
5332             @Retention(RetentionPolicy.SOURCE)
5333             @IntDef({SOURCE_CAMERA, SOURCE_SCREEN,
5334                      SOURCE_MICROPHONE, SOURCE_AUDIOCAPTURE,
5335                      SOURCE_OTHER})
5336             /* package */ @interface Source {}
5337 
5338             /**
5339              * Constant to indicate that camera will be recorded.
5340              */
5341             public static final int SOURCE_CAMERA = 0;
5342 
5343             /**
5344              * Constant to indicate that screen will be recorded.
5345              */
5346             public static final int SOURCE_SCREEN  = 1;
5347 
5348             /**
5349              * Constant to indicate that microphone will be recorded.
5350              */
5351             public static final int SOURCE_MICROPHONE = 2;
5352 
5353             /**
5354              * Constant to indicate that device audio playback will be recorded.
5355              */
5356             public static final int SOURCE_AUDIOCAPTURE = 3;
5357 
5358             /**
5359              * Constant to indicate a media source that does not fall under the other categories.
5360              */
5361             public static final int SOURCE_OTHER = 4;
5362 
5363             @Retention(RetentionPolicy.SOURCE)
5364             @IntDef({TYPE_VIDEO, TYPE_AUDIO})
5365             /* package */ @interface Type {}
5366 
5367             /**
5368              * The media type is video.
5369              */
5370             public static final int TYPE_VIDEO = 0;
5371 
5372             /**
5373              * The media type is audio.
5374              */
5375             public static final int TYPE_AUDIO = 1;
5376 
5377             /**
5378              * A string giving the origin-specific source identifier.
5379              */
5380             public final @NonNull String id;
5381 
5382             /**
5383              * A string giving the non-origin-specific source identifier.
5384              */
5385             public final @NonNull String rawId;
5386 
5387             /**
5388              * A string giving the name of the video source from the system
5389              * (for example, "Camera 0, Facing back, Orientation 90").
5390              * May be empty.
5391              */
5392             public final @Nullable String name;
5393 
5394             /**
5395              * An int indicating the media source type.
5396              * Possible values for a video source are:
5397              * SOURCE_CAMERA, SOURCE_SCREEN, and SOURCE_OTHER.
5398              * Possible values for an audio source are:
5399              * SOURCE_MICROPHONE, SOURCE_AUDIOCAPTURE, and SOURCE_OTHER.
5400              */
5401             public final @Source int source;
5402 
5403             /**
5404              * An int giving the type of media, must be either TYPE_VIDEO or TYPE_AUDIO.
5405              */
5406             public final @Type int type;
5407 
getSourceFromString(final String src)5408             private static @Source int getSourceFromString(final String src) {
5409                 // The strings here should match those in MediaSourceEnum in MediaStreamTrack.webidl
5410                 if ("camera".equals(src)) {
5411                     return SOURCE_CAMERA;
5412                 } else if ("screen".equals(src) || "window".equals(src) || "browser".equals(src)) {
5413                     return SOURCE_SCREEN;
5414                 } else if ("microphone".equals(src)) {
5415                     return SOURCE_MICROPHONE;
5416                 } else if ("audioCapture".equals(src)) {
5417                     return SOURCE_AUDIOCAPTURE;
5418                 } else if ("other".equals(src) || "application".equals(src)) {
5419                     return SOURCE_OTHER;
5420                 } else {
5421                     throw new IllegalArgumentException("String: " + src + " is not a valid media source string");
5422                 }
5423             }
5424 
getTypeFromString(final String type)5425             private static @Type int getTypeFromString(final String type) {
5426                 // The strings here should match the possible types in MediaDevice::MediaDevice in MediaManager.cpp
5427                 if ("videoinput".equals(type)) {
5428                     return TYPE_VIDEO;
5429                 } else if ("audioinput".equals(type)) {
5430                     return TYPE_AUDIO;
5431                 } else {
5432                     throw new IllegalArgumentException("String: " + type + " is not a valid media type string");
5433                 }
5434             }
5435 
MediaSource(final GeckoBundle media)5436             /* package */ MediaSource(final GeckoBundle media) {
5437                 id = media.getString("id");
5438                 rawId = media.getString("rawId");
5439                 name = media.getString("name");
5440                 source = getSourceFromString(media.getString("mediaSource"));
5441                 type = getTypeFromString(media.getString("type"));
5442             }
5443 
5444             /**
5445              * Empty constructor for tests.
5446              */
MediaSource()5447             protected MediaSource() {
5448                 id = null;
5449                 rawId = null;
5450                 name = null;
5451                 source = 0;
5452                 type = 0;
5453             }
5454         }
5455 
5456         /**
5457          * Callback interface for notifying the result of a media permission request,
5458          * including which media source(s) to use.
5459          */
5460         interface MediaCallback {
5461             /**
5462              * Called by the implementation after permissions are granted; the
5463              * implementation must call one of grant() or reject() for every request.
5464              *
5465              * @param video "id" value from the bundle for the video source to use,
5466              *              or null when video is not requested.
5467              * @param audio "id" value from the bundle for the audio source to use,
5468              *              or null when audio is not requested.
5469              */
5470             @UiThread
grant(final @Nullable String video, final @Nullable String audio)5471             default void grant(final @Nullable String video, final @Nullable String audio) {}
5472 
5473             /**
5474              * Called by the implementation after permissions are granted; the
5475              * implementation must call one of grant() or reject() for every request.
5476              *
5477              * @param video MediaSource for the video source to use (must be an original
5478              *              MediaSource object that was passed to the implementation);
5479              *              or null when video is not requested.
5480              * @param audio MediaSource for the audio source to use (must be an original
5481              *              MediaSource object that was passed to the implementation);
5482              *              or null when audio is not requested.
5483              */
5484             @UiThread
grant(final @Nullable MediaSource video, final @Nullable MediaSource audio)5485             default void grant(final @Nullable MediaSource video, final @Nullable MediaSource audio) {}
5486 
5487             /**
5488              * Called by the implementation when permissions are not granted; the
5489              * implementation must call one of grant() or reject() for every request.
5490              */
5491             @UiThread
reject()5492             default void reject() {}
5493         }
5494 
5495         /**
5496          * Request content media permissions, including request for which video and/or
5497          * audio source to use.
5498          *
5499          * Media permissions will still be requested if the associated device permissions have been
5500          * denied if there are video or audio sources in that category that can still be accessed.
5501          * It is the responsibility of consumers to ensure that media permission requests are not
5502          * displayed in this case.
5503          *
5504          * @param session GeckoSession instance requesting the permission.
5505          * @param uri The URI of the content requesting the permission.
5506          * @param video List of video sources, or null if not requesting video.
5507          * @param audio List of audio sources, or null if not requesting audio.
5508          * @param callback Callback interface.
5509          */
5510         @UiThread
onMediaPermissionRequest(@onNull GeckoSession session, @NonNull String uri, @Nullable MediaSource[] video, @Nullable MediaSource[] audio, @NonNull MediaCallback callback)5511         default void onMediaPermissionRequest(@NonNull GeckoSession session, @NonNull String uri,
5512                                               @Nullable MediaSource[] video, @Nullable MediaSource[] audio,
5513                                               @NonNull MediaCallback callback) {
5514             callback.reject();
5515         }
5516     }
5517 
5518     @Retention(RetentionPolicy.SOURCE)
5519     @IntDef({PermissionDelegate.PERMISSION_GEOLOCATION,
5520             PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION,
5521             PermissionDelegate.PERMISSION_PERSISTENT_STORAGE,
5522             PermissionDelegate.PERMISSION_XR,
5523             PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE,
5524             PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE,
5525             PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS})
5526     /* package */ @interface Permission {}
5527 
5528     /**
5529      * Interface that SessionTextInput uses for performing operations such as opening and closing
5530      * the software keyboard. If the delegate is not set, these operations are forwarded to the
5531      * system {@link android.view.inputmethod.InputMethodManager} automatically.
5532      */
5533     public interface TextInputDelegate {
5534         /** Restarting input due to an input field gaining focus. */
5535         int RESTART_REASON_FOCUS = 0;
5536         /** Restarting input due to an input field losing focus. */
5537         int RESTART_REASON_BLUR = 1;
5538         /**
5539          * Restarting input due to the content of the input field changing. For example, the
5540          * input field type may have changed, or the current composition may have been committed
5541          * outside of the input method.
5542          */
5543         int RESTART_REASON_CONTENT_CHANGE = 2;
5544 
5545         /**
5546          * Reset the input method, and discard any existing states such as the current composition
5547          * or current autocompletion. Because the current focused editor may have changed, as
5548          * part of the reset, a custom input method would normally call {@link
5549          * SessionTextInput#onCreateInputConnection} to update its knowledge of the focused editor.
5550          * Note that {@code restartInput} should be used to detect changes in focus, rather than
5551          * {@link #showSoftInput} or {@link #hideSoftInput}, because focus changes are not always
5552          * accompanied by requests to show or hide the soft input. This method is always called,
5553          * even in viewless mode.
5554          *
5555          * @param session Session instance.
5556          * @param reason Reason for the reset.
5557          */
5558         @UiThread
restartInput(@onNull GeckoSession session, @RestartReason int reason)5559         default void restartInput(@NonNull GeckoSession session, @RestartReason int reason) {}
5560 
5561         /**
5562          * Display the soft input. May be called consecutively, even if the soft input is
5563          * already shown. This method is always called, even in viewless mode.
5564          *
5565          * @param session Session instance.
5566          * @see #hideSoftInput
5567          * */
5568         @UiThread
showSoftInput(@onNull GeckoSession session)5569         default void showSoftInput(@NonNull GeckoSession session) {}
5570 
5571         /**
5572          * Hide the soft input. May be called consecutively, even if the soft input is
5573          * already hidden. This method is always called, even in viewless mode.
5574          *
5575          * @param session Session instance.
5576          * @see #showSoftInput
5577          * */
5578         @UiThread
hideSoftInput(@onNull GeckoSession session)5579         default void hideSoftInput(@NonNull GeckoSession session) {}
5580 
5581         /**
5582          * Update the soft input on the current selection. This method is <i>not</i> called
5583          * in viewless mode.
5584          *
5585          * @param session Session instance.
5586          * @param selStart Start offset of the selection.
5587          * @param selEnd End offset of the selection.
5588          * @param compositionStart Composition start offset, or -1 if there is no composition.
5589          * @param compositionEnd Composition end offset, or -1 if there is no composition.
5590          */
5591         @UiThread
updateSelection(@onNull GeckoSession session, int selStart, int selEnd, int compositionStart, int compositionEnd)5592         default void updateSelection(@NonNull GeckoSession session, int selStart, int selEnd,
5593                                      int compositionStart, int compositionEnd) {}
5594 
5595         /**
5596          * Update the soft input on the current extracted text, as requested through
5597          * {@link android.view.inputmethod.InputConnection#getExtractedText}.
5598          * Consequently, this method is <i>not</i> called in viewless mode.
5599          *
5600          * @param session Session instance.
5601          * @param request The extract text request.
5602          * @param text The extracted text.
5603          */
5604         @UiThread
updateExtractedText(@onNull GeckoSession session, @NonNull ExtractedTextRequest request, @NonNull ExtractedText text)5605         default void updateExtractedText(@NonNull GeckoSession session,
5606                                          @NonNull ExtractedTextRequest request,
5607                                          @NonNull ExtractedText text) {}
5608 
5609         /**
5610          * Update the cursor-anchor information as requested through
5611          * {@link android.view.inputmethod.InputConnection#requestCursorUpdates}.
5612          * Consequently, this method is <i>not</i> called in viewless mode.
5613          *
5614          * @param session Session instance.
5615          * @param info Cursor-anchor information.
5616          */
5617         @UiThread
updateCursorAnchorInfo(@onNull GeckoSession session, @NonNull CursorAnchorInfo info)5618         default void updateCursorAnchorInfo(@NonNull GeckoSession session,
5619                                             @NonNull CursorAnchorInfo info) {}
5620     }
5621 
5622     @Retention(RetentionPolicy.SOURCE)
5623     @IntDef({TextInputDelegate.RESTART_REASON_FOCUS, TextInputDelegate.RESTART_REASON_BLUR,
5624             TextInputDelegate.RESTART_REASON_CONTENT_CHANGE})
5625             /* package */ @interface RestartReason {}
5626 
onSurfaceChanged(final Surface surface, final int x, final int y, final int width, final int height)5627     /* package */ void onSurfaceChanged(final Surface surface, final int x, final int y, final int width,
5628                                         final int height) {
5629         ThreadUtils.assertOnUiThread();
5630 
5631         mOffsetX = x;
5632         mOffsetY = y;
5633         mWidth = width;
5634         mHeight = height;
5635 
5636         if (mCompositorReady) {
5637             mCompositor.syncResumeResizeCompositor(x, y, width, height, surface);
5638             onWindowBoundsChanged();
5639             return;
5640         }
5641 
5642         // We have a valid surface but we're not attached or the compositor
5643         // is not ready; save the surface for later when we're ready.
5644         mSurface = surface;
5645 
5646         // Adjust bounds as the last step.
5647         onWindowBoundsChanged();
5648     }
5649 
onSurfaceDestroyed()5650     /* package */ void onSurfaceDestroyed() {
5651         ThreadUtils.assertOnUiThread();
5652 
5653         if (mCompositorReady) {
5654             mCompositor.syncPauseCompositor();
5655             return;
5656         }
5657 
5658         // While the surface was valid, we never became attached or the
5659         // compositor never became ready; clear the saved surface.
5660         mSurface = null;
5661     }
5662 
onScreenOriginChanged(final int left, final int top)5663     /* package */ void onScreenOriginChanged(final int left, final int top) {
5664         ThreadUtils.assertOnUiThread();
5665 
5666         if (mLeft == left && mTop == top) {
5667             return;
5668         }
5669 
5670         mLeft = left;
5671         mTop = top;
5672         onWindowBoundsChanged();
5673     }
5674 
setDynamicToolbarMaxHeight(final int height)5675     /* package */ void setDynamicToolbarMaxHeight(final int height) {
5676         if (mDynamicToolbarMaxHeight == height) {
5677             return;
5678         }
5679 
5680         if (mHeight != 0 && height != 0 && mHeight < height) {
5681             Log.w(LOGTAG, new AssertionError(
5682                     "The maximum height of the dynamic toolbar (" +
5683                     height +
5684                     ") should be smaller than GeckoView height (" +
5685                     mHeight + ")"));
5686         }
5687 
5688         mDynamicToolbarMaxHeight = height;
5689 
5690         if (mAttachedCompositor) {
5691             mCompositor.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
5692         }
5693     }
5694 
5695 
setFixedBottomOffset(final int offset)5696     /* package */ void setFixedBottomOffset(final int offset) {
5697         if (mFixedBottomOffset == offset) {
5698             return;
5699         }
5700 
5701         mFixedBottomOffset = offset;
5702 
5703         if (mCompositorReady) {
5704             mCompositor.setFixedBottomOffset(mFixedBottomOffset);
5705         }
5706     }
5707 
5708 
onCompositorAttached()5709     /* package */ void onCompositorAttached() {
5710         if (DEBUG) {
5711             ThreadUtils.assertOnUiThread();
5712         }
5713 
5714         mAttachedCompositor = true;
5715         mCompositor.attachNPZC(mPanZoomController.mNative);
5716 
5717         if (mSurface != null) {
5718             // If we have a valid surface, create the compositor now that we're attached.
5719             // Leave mSurface alone because we'll need it later for onCompositorReady.
5720             onSurfaceChanged(mSurface, mOffsetX, mOffsetY, mWidth, mHeight);
5721         }
5722 
5723         mCompositor.sendToolbarAnimatorMessage(IS_COMPOSITOR_CONTROLLER_OPEN);
5724         mCompositor.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
5725     }
5726 
onCompositorDetached()5727     /* package */ void onCompositorDetached() {
5728         if (DEBUG) {
5729             ThreadUtils.assertOnUiThread();
5730         }
5731 
5732         if (mController != null) {
5733             mController.onCompositorDetached();
5734         }
5735 
5736         mAttachedCompositor = false;
5737         mCompositorReady = false;
5738     }
5739 
handleCompositorMessage(final int message)5740     /* package */ void handleCompositorMessage(final int message) {
5741         if (DEBUG) {
5742             ThreadUtils.assertOnUiThread();
5743         }
5744 
5745         switch (message) {
5746             case COMPOSITOR_CONTROLLER_OPEN: {
5747                 if (isCompositorReady()) {
5748                     return;
5749                 }
5750 
5751                 // Delay calling onCompositorReady to avoid deadlock due
5752                 // to synchronous call to the compositor.
5753                 ThreadUtils.postToUiThread(this::onCompositorReady);
5754                 break;
5755             }
5756 
5757             case FIRST_PAINT: {
5758                 if (mController != null) {
5759                     mController.onFirstPaint();
5760                 }
5761                 ContentDelegate delegate = mContentHandler.getDelegate();
5762                 if (delegate != null) {
5763                     delegate.onFirstComposite(this);
5764                 }
5765                 break;
5766             }
5767 
5768             case LAYERS_UPDATED: {
5769                 if (mController != null) {
5770                     mController.notifyDrawCallbacks();
5771                 }
5772                 break;
5773             }
5774 
5775             default: {
5776                 Log.w(LOGTAG, "Unexpected message: " + message);
5777                 break;
5778             }
5779         }
5780     }
5781 
isCompositorReady()5782     /* package */ boolean isCompositorReady() {
5783         return mCompositorReady;
5784     }
5785 
onCompositorReady()5786     /* package */ void onCompositorReady() {
5787         if (DEBUG) {
5788             ThreadUtils.assertOnUiThread();
5789         }
5790 
5791         if (!mAttachedCompositor) {
5792             return;
5793         }
5794 
5795         mCompositorReady = true;
5796 
5797         if (mController != null) {
5798             mController.onCompositorReady();
5799         }
5800 
5801         if (mSurface != null) {
5802             // If we have a valid surface, resume the
5803             // compositor now that the compositor is ready.
5804             onSurfaceChanged(mSurface, mOffsetX, mOffsetY, mWidth, mHeight);
5805             mSurface = null;
5806         }
5807 
5808         if (mFixedBottomOffset != 0) {
5809             mCompositor.setFixedBottomOffset(mFixedBottomOffset);
5810         }
5811     }
5812 
updateOverscrollVelocity(final float x, final float y)5813     /* package */ void updateOverscrollVelocity(final float x, final float y) {
5814         if (DEBUG) {
5815             ThreadUtils.assertOnUiThread();
5816         }
5817 
5818         if (mOverscroll == null) {
5819             return;
5820         }
5821 
5822         // Multiply the velocity by 1000 to match what was done in JPZ.
5823         mOverscroll.setVelocity(x * 1000.0f, OverscrollEdgeEffect.AXIS_X);
5824         mOverscroll.setVelocity(y * 1000.0f, OverscrollEdgeEffect.AXIS_Y);
5825     }
5826 
updateOverscrollOffset(final float x, final float y)5827     /* package */ void updateOverscrollOffset(final float x, final float y) {
5828         if (DEBUG) {
5829             ThreadUtils.assertOnUiThread();
5830         }
5831 
5832         if (mOverscroll == null) {
5833             return;
5834         }
5835 
5836         mOverscroll.setDistance(x, OverscrollEdgeEffect.AXIS_X);
5837         mOverscroll.setDistance(y, OverscrollEdgeEffect.AXIS_Y);
5838     }
5839 
onMetricsChanged(final float scrollX, final float scrollY, final float zoom)5840     /* package */ void onMetricsChanged(final float scrollX, final float scrollY,
5841                                         final float zoom) {
5842         if (DEBUG) {
5843             ThreadUtils.assertOnUiThread();
5844         }
5845 
5846         mViewportLeft = scrollX;
5847         mViewportTop = scrollY;
5848         mViewportZoom = zoom;
5849     }
5850 
onWindowBoundsChanged()5851     /* protected */ void onWindowBoundsChanged() {
5852         if (DEBUG) {
5853             ThreadUtils.assertOnUiThread();
5854         }
5855 
5856         if (mHeight != 0 && mDynamicToolbarMaxHeight != 0 &&
5857             mHeight < mDynamicToolbarMaxHeight) {
5858             Log.w(LOGTAG, new AssertionError( "The maximum height of the dynamic toolbar (" +
5859                           mDynamicToolbarMaxHeight +
5860                           ") should be smaller than GeckoView height (" +
5861                           mHeight + ")"));
5862         }
5863 
5864         final int toolbarHeight = 0;
5865 
5866         mClientTop = mTop + toolbarHeight;
5867         // If the view is not tall enough to even fix the toolbar we just
5868         // default the client height to 0
5869         mClientHeight = Math.max(mHeight - toolbarHeight, 0);
5870 
5871         if (mAttachedCompositor) {
5872             mCompositor.onBoundsChanged(mLeft, mClientTop, mWidth, mClientHeight);
5873         }
5874 
5875         if (mOverscroll != null) {
5876             mOverscroll.setSize(mWidth, mClientHeight);
5877         }
5878     }
5879 
onSafeAreaInsetsChanged(final int top, final int right, final int bottom, final int left)5880     /* pacakge */ void onSafeAreaInsetsChanged(final int top, final int right, final int bottom, final int left) {
5881         ThreadUtils.assertOnUiThread();
5882 
5883         if (mAttachedCompositor) {
5884             mCompositor.onSafeAreaInsetsChanged(top, right, bottom, left);
5885         }
5886     }
5887 
5888     /**
5889      * GeckoSession applications implement this interface to handle media events.
5890      */
5891     public interface MediaDelegate {
5892 
5893         class RecordingDevice {
5894 
5895             /*
5896              * Default status flags for this RecordingDevice.
5897              */
5898             public static class Status {
5899                 public static final long RECORDING = 0;
5900                 public static final long INACTIVE = 1 << 0;
5901 
5902                 // Do not instantiate this class.
Status()5903                 protected Status() {}
5904             }
5905 
5906             /*
5907              * Default device types for this RecordingDevice.
5908              */
5909             public static class Type {
5910                 public static final long CAMERA = 0;
5911                 public static final long MICROPHONE = 1 << 0;
5912 
5913                 // Do not instantiate this class.
Type()5914                 protected Type() {}
5915             }
5916 
5917             @Retention(RetentionPolicy.SOURCE)
5918             @LongDef(flag = true,
5919                     value = { Status.RECORDING, Status.INACTIVE })
5920             /* package */ @interface RecordingStatus {}
5921 
5922             @Retention(RetentionPolicy.SOURCE)
5923             @LongDef(flag = true,
5924                     value = {Type.CAMERA, Type.MICROPHONE})
5925             /* package */ @interface DeviceType {}
5926 
5927             /**
5928              * A long giving the current recording status, must be either Status.RECORDING,
5929              * Status.PAUSED or Status.INACTIVE.
5930              */
5931             public final @RecordingStatus long status;
5932 
5933             /**
5934              * A long giving the type of the recording device, must be either Type.CAMERA or Type.MICROPHONE.
5935              */
5936             public final @DeviceType long type;
5937 
getTypeFromString(final String type)5938             private static @DeviceType long getTypeFromString(final String type) {
5939                 if ("microphone".equals(type)) {
5940                     return Type.MICROPHONE;
5941                 } else if ("camera".equals(type)) {
5942                     return Type.CAMERA;
5943                 } else {
5944                     throw new IllegalArgumentException("String: " + type + " is not a valid recording device string");
5945                 }
5946             }
5947 
getStatusFromString(final String type)5948             private static @RecordingStatus long getStatusFromString(final String type) {
5949                 if ("recording".equals(type)) {
5950                     return Status.RECORDING;
5951                 } else {
5952                     return Status.INACTIVE;
5953                 }
5954             }
5955 
RecordingDevice(final GeckoBundle media)5956             /* package */ RecordingDevice(final GeckoBundle media) {
5957                 status = getStatusFromString(media.getString("status"));
5958                 type = getTypeFromString(media.getString("type"));
5959             }
5960 
5961             /**
5962              * Empty constructor for tests.
5963              */
RecordingDevice()5964             protected RecordingDevice() {
5965                 status = Status.INACTIVE;
5966                 type = Type.CAMERA;
5967             }
5968         }
5969         /**
5970          * An HTMLMediaElement has been created.
5971          * @param session Session instance.
5972          * @param element The media element that was just created.
5973          */
5974         @UiThread
onMediaAdd(@onNull GeckoSession session, @NonNull MediaElement element)5975         default void onMediaAdd(@NonNull GeckoSession session, @NonNull MediaElement element) {}
5976 
5977         /**
5978          * An HTMLMediaElement has been unloaded.
5979          * @param session Session instance.
5980          * @param element The media element that was unloaded.
5981          */
5982         @UiThread
onMediaRemove(@onNull GeckoSession session, @NonNull MediaElement element)5983         default void onMediaRemove(@NonNull GeckoSession session, @NonNull MediaElement element) {}
5984 
5985         /**
5986          * A recording device has changed state.
5987          * Any change to the recording state of the devices microphone or camera will call this
5988          * delegate method. The argument provides details of the active recording devices.
5989          * @param session The session that the event has originated from.
5990          * @param devices The list of active devices and their recording state.
5991          */
5992         @UiThread
onRecordingStatusChanged(@onNull GeckoSession session, @NonNull RecordingDevice[] devices)5993         default void onRecordingStatusChanged(@NonNull GeckoSession session, @NonNull RecordingDevice[] devices) {}
5994     }
5995 
5996     /**
5997      * An interface for recording new history visits and fetching the visited
5998      * status for links.
5999      */
6000     public interface HistoryDelegate {
6001         /**
6002          * A representation of an entry in browser history.
6003          */
6004         public interface HistoryItem {
6005             /**
6006              * Get the URI of this history element.
6007              *
6008              * @return A String representing the URI of this history element.
6009              */
6010             @AnyThread
getUri()6011             default @NonNull String getUri() {
6012                 throw new UnsupportedOperationException("HistoryItem.getUri() called on invalid object.");
6013             }
6014 
6015             /**
6016              * Get the title of this history element.
6017              *
6018              * @return A String representing the title of this history element.
6019              */
6020             @AnyThread
getTitle()6021             default @NonNull String getTitle() {
6022                 throw new UnsupportedOperationException("HistoryItem.getString() called on invalid object.");
6023             }
6024         }
6025 
6026         /**
6027          * A representation of browser history, accessible as a `List`. The list itself
6028          * and its entries are immutable; any attempt to mutate will result in an
6029          * `UnsupportedOperationException`.
6030          */
6031         public interface HistoryList extends List<HistoryItem> {
6032             /**
6033              * Get the current index in browser history.
6034              *
6035              * @return An int representing the current index in browser history.
6036              */
6037             @AnyThread
getCurrentIndex()6038             default int getCurrentIndex() {
6039                 throw new UnsupportedOperationException("HistoryList.getCurrentIndex() called on invalid object.");
6040             }
6041         }
6042 
6043         // These flags are similar to those in `IHistory::LoadFlags`, but we use
6044         // different values to decouple GeckoView from Gecko changes. These
6045         // should be kept in sync with `GeckoViewHistory::GeckoViewVisitFlags`.
6046 
6047         /** The URL was visited a top-level window. */
6048         final int VISIT_TOP_LEVEL = 1 << 0;
6049         /** The URL is the target of a temporary redirect. */
6050         final int VISIT_REDIRECT_TEMPORARY = 1 << 1;
6051         /** The URL is the target of a permanent redirect. */
6052         final int VISIT_REDIRECT_PERMANENT = 1 << 2;
6053         /** The URL is temporarily redirected to another URL. */
6054         final int VISIT_REDIRECT_SOURCE = 1 << 3;
6055         /** The URL is permanently redirected to another URL. */
6056         final int VISIT_REDIRECT_SOURCE_PERMANENT = 1 << 4;
6057         /** The URL failed to load due to a client or server error. */
6058         final int VISIT_UNRECOVERABLE_ERROR = 1 << 5;
6059 
6060         /**
6061          * Records a visit to a page.
6062          *
6063          * @param session The session where the URL was visited.
6064          * @param url The visited URL.
6065          * @param lastVisitedURL The last visited URL in this session, to detect
6066          *                       redirects and reloads.
6067          * @param flags Additional flags for this visit, including redirect and
6068          *              error statuses. This is a bitmask of one or more
6069          *              {@link #VISIT_TOP_LEVEL VISIT_*} flags, OR-ed together.
6070          * @return A {@link GeckoResult} completed with a boolean indicating
6071          *         whether to highlight links for the new URL as visited
6072          *         ({@code true}) or unvisited ({@code false}).
6073          */
6074         @UiThread
onVisited(@onNull GeckoSession session, @NonNull String url, @Nullable String lastVisitedURL, @VisitFlags int flags)6075         default @Nullable GeckoResult<Boolean> onVisited(@NonNull GeckoSession session,
6076                                                          @NonNull String url,
6077                                                          @Nullable String lastVisitedURL,
6078                                                          @VisitFlags int flags) {
6079             return null;
6080         }
6081 
6082         /**
6083          * Returns the visited statuses for links on a page. This is used to
6084          * highlight links as visited or unvisited, for example.
6085          *
6086          * @param session The session requesting the visited statuses.
6087          * @param urls A list of URLs to check.
6088          * @return A {@link GeckoResult} completed with a list of booleans
6089          *         corresponding to the URLs in {@code urls}, and indicating
6090          *         whether to highlight links for each URL as visited
6091          *         ({@code true}) or unvisited ({@code false}).
6092          */
6093         @UiThread
getVisited(@onNull GeckoSession session, @NonNull String[] urls)6094         default @Nullable GeckoResult<boolean[]> getVisited(@NonNull GeckoSession session,
6095                                                             @NonNull String[] urls) {
6096             return null;
6097         }
6098 
6099         @UiThread
onHistoryStateChange(@onNull GeckoSession session, @NonNull HistoryList historyList)6100         default void onHistoryStateChange(@NonNull GeckoSession session, @NonNull HistoryList historyList) {}
6101     }
6102 
6103     @Retention(RetentionPolicy.SOURCE)
6104     @IntDef(flag = true,
6105             value = {
6106                 HistoryDelegate.VISIT_TOP_LEVEL,
6107                 HistoryDelegate.VISIT_REDIRECT_TEMPORARY,
6108                 HistoryDelegate.VISIT_REDIRECT_PERMANENT,
6109                 HistoryDelegate.VISIT_REDIRECT_SOURCE,
6110                 HistoryDelegate.VISIT_REDIRECT_SOURCE_PERMANENT,
6111                 HistoryDelegate.VISIT_UNRECOVERABLE_ERROR
6112             })
6113     /* package */ @interface VisitFlags {}
6114 
6115 
getAutofillSupport()6116     private Autofill.Support getAutofillSupport() {
6117         return mAutofillSupport;
6118     }
6119 
6120     /**
6121      * Sets the autofill delegate for this session.
6122      *
6123      * @param delegate An instance of {@link Autofill.Delegate}.
6124      */
6125     @UiThread
setAutofillDelegate(final @Nullable Autofill.Delegate delegate)6126     public void setAutofillDelegate(final @Nullable Autofill.Delegate delegate) {
6127         getAutofillSupport().setDelegate(delegate);
6128     }
6129 
6130     /**
6131      * @return The current {@link Autofill.Delegate} for this session, if any.
6132      */
6133     @UiThread
getAutofillDelegate()6134     public @Nullable Autofill.Delegate getAutofillDelegate() {
6135         return getAutofillSupport().getDelegate();
6136     }
6137 
6138     /**
6139      * Perform autofill using the specified values.
6140      *
6141      * @param values Map of autofill IDs to values.
6142      */
6143     @UiThread
autofill(final @NonNull SparseArray<CharSequence> values)6144     public void autofill(final @NonNull SparseArray<CharSequence> values) {
6145         getAutofillSupport().autofill(values);
6146     }
6147 
6148     /**
6149      * Provides an autofill structure similar to {@link View#onProvideAutofillVirtualStructure(ViewStructure, int)} , but
6150      * does not rely on {@link ViewStructure} to build the tree. This is useful for apps that want
6151      * to provide autofill functionality without using the Android autofill system or requiring
6152      * API 26.
6153      *
6154      * @return The elements available for autofill.
6155      */
6156     @UiThread
getAutofillSession()6157     public @NonNull Autofill.Session getAutofillSession() {
6158         return getAutofillSupport().getAutofillSession();
6159     }
6160 
rgbaToArgb(final String color)6161     private static String rgbaToArgb(final String color) {
6162         // We expect #rrggbbaa
6163         if (color.length() != 9 || !color.startsWith("#")) {
6164             throw new IllegalArgumentException("Invalid color format");
6165         }
6166 
6167         return "#" + color.substring(7) + color.substring(1, 7);
6168     }
6169 
fixupManifestColor(final JSONObject manifest, final String name)6170     private static void fixupManifestColor(final JSONObject manifest, final String name) throws JSONException {
6171         if (manifest.isNull(name)) {
6172             return;
6173         }
6174 
6175         manifest.put(name, rgbaToArgb(manifest.getString(name)));
6176     }
6177 
fixupWebAppManifest(final JSONObject manifest)6178     private static JSONObject fixupWebAppManifest(final JSONObject manifest) {
6179         // Colors are #rrggbbaa, but we want them to be #aarrggbb, since that's what
6180         // android.graphics.Color expects.
6181         try {
6182             fixupManifestColor(manifest, "theme_color");
6183             fixupManifestColor(manifest, "background_color");
6184         } catch (JSONException e) {
6185             Log.w(LOGTAG, "Failed to fixup web app manifest", e);
6186         }
6187 
6188         return manifest;
6189     }
6190 }
6191