Bug 1629113 - Implement nsIPromptCollection on GeckoView. r=snorp,droeh
authorAgi Sferro <agi@sferro.dev>
Fri, 22 May 2020 23:22:41 +0000
changeset 531744 1f2dfbcafe57e8aac38dca4df96e691b659bae8e
parent 531743 54af712fe5983704e25b581afbd760557f24e948
child 531745 d50b48a2181595127b473e7b96f8791f86b3ee35
push id37442
push userncsoregi@mozilla.com
push dateSat, 23 May 2020 09:21:24 +0000
treeherdermozilla-central@bbcc193fe0f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, droeh
bugs1629113
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1629113 - Implement nsIPromptCollection on GeckoView. r=snorp,droeh Differential Revision: https://phabricator.services.mozilla.com/D72722
mobile/android/components/geckoview/PromptCollection.jsm
mobile/android/components/geckoview/components.conf
mobile/android/components/geckoview/moz.build
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/assets/www/beforeunload.html
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PromptDelegateTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
mobile/android/geckoview_example/src/main/res/values/strings.xml
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/geckoview/PromptCollection.jsm
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["PromptCollection"];
+
+const { GeckoViewUtils } = ChromeUtils.import(
+  "resource://gre/modules/GeckoViewUtils.jsm"
+);
+
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.jsm",
+});
+
+const { debug, warn } = GeckoViewUtils.initLogging("PromptCollection"); // eslint-disable-line no-unused-vars
+
+class PromptCollection {
+  beforeUnloadCheck(browsingContext) {
+    const msg = {
+      type: "beforeUnload",
+    };
+    const prompter = new GeckoViewPrompter(browsingContext);
+    const result = prompter.showPrompt(msg);
+    return !!result?.allow;
+  }
+}
+
+PromptCollection.prototype.classID = Components.ID(
+  "{3e30d2a0-9934-11ea-bb37-0242ac130002}"
+);
+
+PromptCollection.prototype.QueryInterface = ChromeUtils.generateQI([
+  Ci.nsIPromptCollection,
+]);
--- a/mobile/android/components/geckoview/components.conf
+++ b/mobile/android/components/geckoview/components.conf
@@ -9,16 +9,22 @@ Classes = [
         'cid': '{0937a705-91a6-417a-8292-b22eb10da86c}',
         'contract_ids': ['@mozilla.org/browser/history;1'],
         'singleton': True,
         'type': 'GeckoViewHistory',
         'headers': ['GeckoViewHistory.h'],
         'constructor': 'GeckoViewHistory::GetSingleton',
     },
     {
+        'cid': '{3e30d2a0-9934-11ea-bb37-0242ac130002}',
+        'contract_ids': ['@mozilla.org/embedcomp/prompt-collection;1'],
+        'jsm': 'resource://gre/modules/PromptCollection.jsm',
+        'constructor': 'PromptCollection',
+    },
+    {
         'cid': '{91455c77-64a1-4c37-be00-f94eb9c7b8e1}',
         'contract_ids': [
             '@mozilla.org/uriloader/external-helper-app-service;1',
         ],
         'type': 'GeckoViewExternalAppService',
         'constructor': 'GeckoViewExternalAppService::GetSingleton',
         'headers': ['GeckoViewExternalAppService.h'],
         'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS,
--- a/mobile/android/components/geckoview/moz.build
+++ b/mobile/android/components/geckoview/moz.build
@@ -24,11 +24,12 @@ EXTRA_COMPONENTS += [
     'GeckoViewPermission.js',
     'GeckoViewPrompt.js',
     'GeckoViewPush.js',
     'GeckoViewStartup.js',
 ]
 
 EXTRA_JS_MODULES += [
     'GeckoViewPrompter.jsm',
+    'PromptCollection.jsm',
 ]
 
 FINAL_LIBRARY = 'xul'
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -825,16 +825,17 @@ package org.mozilla.geckoview {
     field public final int mixedModePassive;
     field @Nullable public final String origin;
     field public final int securityMode;
   }
 
   public static interface GeckoSession.PromptDelegate {
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onAlertPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.AlertPrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onAuthPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.AuthPrompt);
+    method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onBeforeUnloadPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.BeforeUnloadPrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onButtonPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.ButtonPrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onChoicePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.ChoicePrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onColorPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.ColorPrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onDateTimePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.DateTimePrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onFilePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.FilePrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onLoginStoragePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.LoginStoragePrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onPopupPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.PopupPrompt);
     method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onSharePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.SharePrompt);
@@ -881,16 +882,21 @@ package org.mozilla.geckoview {
 
   public static class GeckoSession.PromptDelegate.BasePrompt {
     method @UiThread @NonNull public GeckoSession.PromptDelegate.PromptResponse dismiss();
     method @UiThread public boolean isComplete();
     method @UiThread @NonNull protected GeckoSession.PromptDelegate.PromptResponse confirm();
     field @Nullable public final String title;
   }
 
+  public static class GeckoSession.PromptDelegate.BeforeUnloadPrompt extends GeckoSession.PromptDelegate.BasePrompt {
+    ctor protected BeforeUnloadPrompt();
+    method @UiThread @NonNull public GeckoSession.PromptDelegate.PromptResponse confirm(@Nullable AllowOrDeny);
+  }
+
   public static class GeckoSession.PromptDelegate.ButtonPrompt extends GeckoSession.PromptDelegate.BasePrompt {
     ctor protected ButtonPrompt(@Nullable String, @Nullable String);
     method @UiThread @NonNull public GeckoSession.PromptDelegate.PromptResponse confirm(int);
     field @Nullable public final String message;
   }
 
   public static class GeckoSession.PromptDelegate.ButtonPrompt.Type {
     ctor protected Type();
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/www/beforeunload.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html><html>
+<body onbeforeunload="return beforeUnload()">
+  <a id=navigateAway href="./hello.html">Click Me</a>
+  <a id=navigateAway2 href="./hello2.html">Click Me</a>
+  <script>
+    function beforeUnload() {
+      return "Please don't leave.";
+    }
+  </script>
+</body>
+</html>
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -21,16 +21,17 @@ import org.junit.rules.ErrorCollector
 import kotlin.reflect.KClass
 
 /**
  * Common base class for tests using GeckoSessionTestRule,
  * providing the test rule and other utilities.
  */
 open class BaseSessionTest(noErrorCollector: Boolean = false) {
     companion object {
+        const val BEFORE_UNLOAD = "/assets/www/beforeunload.html"
         const val CLICK_TO_RELOAD_HTML_PATH = "/assets/www/clickToReload.html"
         const val CONTENT_CRASH_URL = "about:crashcontent"
         const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
         const val FORMS_HTML_PATH = "/assets/www/forms.html"
         const val FORMS2_HTML_PATH = "/assets/www/forms2.html"
         const val FORMS3_HTML_PATH = "/assets/www/forms3.html"
         const val FORMS_AUTOCOMPLETE_HTML_PATH = "/assets/www/forms_autocomplete.html"
         const val HELLO_HTML_PATH = "/assets/www/hello.html"
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PromptDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PromptDelegateTest.kt
@@ -109,18 +109,20 @@ class PromptDelegateTest : BaseSessionTe
             @AssertCalled(count = 1)
             override fun onAuthPrompt(session: GeckoSession, prompt: PromptDelegate.AuthPrompt): GeckoResult<PromptDelegate.PromptResponse>? {
                 //TODO: Figure out some better testing here.
                 return null
             }
         })
     }
 
-    @Ignore // TODO: Reenable when 1501574 is fixed.
     @Test fun buttonTest() {
+        sessionRule.session.loadTestPath(HELLO_HTML_PATH)
+        sessionRule.waitForPageStop()
+
         sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
             @AssertCalled(count = 1)
             override fun onButtonPrompt(session: GeckoSession, prompt: PromptDelegate.ButtonPrompt): GeckoResult<PromptDelegate.PromptResponse> {
                 assertThat("Message should match", "Confirm?", equalTo(prompt.message))
                 return GeckoResult.fromValue(prompt.confirm(PromptDelegate.ButtonPrompt.Type.POSITIVE))
             }
         })
 
@@ -136,16 +138,67 @@ class PromptDelegateTest : BaseSessionTe
             }
         })
 
         assertThat("Result should match",
                 sessionRule.session.waitForJS("confirm('Confirm?')") as Boolean,
                 equalTo(false))
     }
 
+    @Test
+    fun onBeforeUnloadTest() {
+        sessionRule.setPrefsUntilTestEnd(mapOf(
+                "dom.require_user_interaction_for_beforeunload" to false
+        ))
+        sessionRule.session.loadTestPath(BEFORE_UNLOAD)
+        sessionRule.waitForPageStop()
+
+        val result = GeckoResult<Void>()
+        sessionRule.delegateUntilTestEnd(object: Callbacks.ProgressDelegate {
+            override fun onPageStart(session: GeckoSession, url: String) {
+                assertThat("Only HELLO2_HTML_PATH should load", url, endsWith(HELLO2_HTML_PATH))
+                result.complete(null)
+            }
+        })
+
+        var promptResult = GeckoResult<PromptDelegate.PromptResponse>()
+        sessionRule.delegateUntilTestEnd(object : Callbacks.PromptDelegate {
+            override fun onBeforeUnloadPrompt(session: GeckoSession, prompt: PromptDelegate.BeforeUnloadPrompt): GeckoResult<PromptDelegate.PromptResponse>? {
+                // We have to return something here because otherwise the delegate will be invoked
+                // before we have a chance to override it in the waitUntilCalled call below
+                return promptResult
+            }
+        })
+
+        // This will try to load "hello.html" but will be denied, if the request
+        // goes through anyway the onLoadRequest delegate above will throw an exception
+        sessionRule.session.evaluateJS("document.querySelector('#navigateAway').click()")
+        sessionRule.waitUntilCalled(object : Callbacks.PromptDelegate {
+            @AssertCalled(count = 1)
+            override fun onBeforeUnloadPrompt(session: GeckoSession, prompt: PromptDelegate.BeforeUnloadPrompt): GeckoResult<PromptDelegate.PromptResponse>? {
+                promptResult.complete(prompt.confirm(AllowOrDeny.DENY))
+                return promptResult
+            }
+        })
+
+        // This request will go through and end the test. Doing the negative case first will
+        // ensure that if either of this tests fail the test will fail.
+        promptResult = GeckoResult()
+        sessionRule.session.evaluateJS("document.querySelector('#navigateAway2').click()")
+        sessionRule.waitUntilCalled(object : Callbacks.PromptDelegate {
+            @AssertCalled(count = 1)
+            override fun onBeforeUnloadPrompt(session: GeckoSession, prompt: PromptDelegate.BeforeUnloadPrompt): GeckoResult<PromptDelegate.PromptResponse>? {
+                promptResult.complete(prompt.confirm(AllowOrDeny.ALLOW))
+                return promptResult
+            }
+        })
+
+        sessionRule.waitForResult(result)
+    }
+
     @Test fun textTest() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.session.waitForPageStop()
 
         sessionRule.delegateUntilTestEnd(object : Callbacks.PromptDelegate {
             @AssertCalled(count = 1)
             override fun onTextPrompt(session: GeckoSession, prompt: PromptDelegate.TextPrompt): GeckoResult<PromptDelegate.PromptResponse> {
                 assertThat("Message should match", "Prompt:", equalTo(prompt.message))
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -2635,16 +2635,22 @@ public class GeckoSession implements Par
 
         switch (type) {
             case "alert": {
                 final PromptDelegate.AlertPrompt prompt =
                     new PromptDelegate.AlertPrompt(title, msg);
                 res = delegate.onAlertPrompt(session, prompt);
                 break;
             }
+            case "beforeUnload": {
+                final PromptDelegate.BeforeUnloadPrompt prompt =
+                    new PromptDelegate.BeforeUnloadPrompt();
+                res = delegate.onBeforeUnloadPrompt(session, prompt);
+                break;
+            }
             case "button": {
                 final PromptDelegate.ButtonPrompt prompt =
                     new PromptDelegate.ButtonPrompt(title, msg);
                 res = delegate.onButtonPrompt(session, prompt);
                 break;
             }
             case "text": {
                 final String defaultValue = message.getString("value");
@@ -3868,16 +3874,40 @@ public class GeckoSession implements Par
                     callback.sendSuccess(null);
                 } else {
                     callback.sendSuccess(mResult);
                 }
             }
         }
 
         /**
+         * BeforeUnloadPrompt represents the onbeforeunload prompt.
+         * See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
+         */
+        class BeforeUnloadPrompt extends BasePrompt {
+            protected BeforeUnloadPrompt() {
+                super(null);
+            }
+
+            /**
+             * Confirms the prompt.
+             *
+             * @param allowOrDeny whether the navigation should be allowed to continue or not.
+             *
+             * @return A {@link PromptResponse} which can be used to complete
+             *         the {@link GeckoResult} associated with this prompt.
+             */
+            @UiThread
+            public @NonNull PromptResponse confirm(final @Nullable AllowOrDeny allowOrDeny) {
+                ensureResult().putBoolean("allow", allowOrDeny != AllowOrDeny.DENY);
+                return super.confirm();
+            }
+        }
+
+        /**
          * AlertPrompt contains the information necessary to represent a JavaScript
          * alert() call from content; it can only be dismissed, not confirmed.
          */
         public class AlertPrompt extends BasePrompt {
             /**
              * The message to be displayed with this alert; may be null.
              */
             public final @Nullable String message;
@@ -4827,16 +4857,24 @@ public class GeckoSession implements Par
          *         includes all necessary information to resolve the prompt.
          */
         @UiThread
         default @Nullable GeckoResult<PromptResponse> onAlertPrompt(@NonNull final GeckoSession session,
                                                                     @NonNull final AlertPrompt prompt) {
             return null;
         }
 
+        @UiThread
+        default @Nullable GeckoResult<PromptResponse> onBeforeUnloadPrompt(
+                @NonNull final GeckoSession session,
+                @NonNull final BeforeUnloadPrompt prompt
+        ) {
+            return null;
+        }
+
         /**
          * Display a button prompt.
          *
          * @param session GeckoSession that triggered the prompt.
          * @param prompt The {@link ButtonPrompt} that describes the prompt.
          *
          * @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
          *         includes all necessary information to resolve the prompt.
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -22,22 +22,24 @@ exclude: true
   enabling dynamic first party isolation; this will block tracking cookies and
   isolate all other third party cookies by keying them based on the first party
   from which they are accessed.
 - Added `cookieStoreId` field to [`WebExtension.CreateTabDetails`][78.3]. This adds the optional
   ability to create a tab with a given cookie store ID for its [`contextual identity`][78.4].
   ([bug 1622500]({{bugzilla}}1622500))
 - Added [`NavigationDelegate.onSubframeLoadRequest`][78.5] to allow intercepting
   non-top-level navigations.
+- Added [`BeforeUnloadPrompt`][78.6] to respond to prompts from onbeforeunload.
 
 [78.1]: {{javadoc_uri}}/WebExtensionController.html#installBuiltIn-java.lang.String-
 [78.2]: {{javadoc_uri}}/ContentBlocking.CookieBehavior.html#ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS
 [78.3]: {{javadoc_uri}}/WebExtension.CreateTabDetails.html
 [78.4]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/contextualIdentities
 [78.5]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.html#onSubframeLoadRequest-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest-
+[78.6]: {{javadoc_uri}}/GeckoSession.PromptDelegate.BeforeUnloadPrompt.html
 
 ## v77
 - Added [`GeckoRuntime.appendAppNotesToCrashReport`][77.1] For adding app notes to the crash report.
   ([bug 1626979]({{bugzilla}}1626979))
 - ⚠️ Remove the `DynamicToolbarAnimator` API along with accesors on `GeckoView` and `GeckoSession`.
   ([bug 1627716]({{bugzilla}}1627716))
 
 [77.1]: {{javadoc_uri}}/GeckoRuntime.html#appendAppNotesToCrashReport-java.lang.String-
@@ -691,9 +693,9 @@ exclude: true
 [65.19]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest.html#isRedirect
 [65.20]: {{javadoc_uri}}/GeckoSession.html#LOAD_FLAGS_BYPASS_CLASSIFIER
 [65.21]: {{javadoc_uri}}/GeckoSession.ContentDelegate.ContextElement.html
 [65.22]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onContextMenu-org.mozilla.geckoview.GeckoSession-int-int-org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement-
 [65.23]: {{javadoc_uri}}/GeckoSession.FinderResult.html
 [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: {{javadoc_uri}}/GeckoResult.html
 
-[api-version]: bde8001c948235193636d0d21f684baeb551e739
+[api-version]: f23cb7b15d085f0c3a9be06fe2281b0ffd9adf1e
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
@@ -13,16 +13,18 @@ import android.content.ClipData;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.res.TypedArray;
 import android.graphics.Color;
 import android.graphics.PorterDuff;
 import android.net.Uri;
 import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.text.InputType;
 import android.text.format.DateFormat;
 import android.util.Log;
 import android.view.InflateException;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
@@ -115,16 +117,47 @@ final class BasicGeckoViewPrompt impleme
     }
 
     @Override
     public GeckoResult<PromptResponse> onSharePrompt(final GeckoSession session,
                                                      final SharePrompt prompt) {
         return GeckoResult.fromValue(prompt.dismiss());
     }
 
+    @Nullable
+    @Override
+    public GeckoResult<PromptResponse> onBeforeUnloadPrompt(final GeckoSession session,
+                                                            final BeforeUnloadPrompt prompt) {
+        final Activity activity = mActivity;
+        if (activity == null) {
+            return GeckoResult.fromValue(prompt.dismiss());
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
+                .setTitle(R.string.before_unload_title)
+                .setMessage(R.string.before_unload_message);
+
+        GeckoResult<PromptResponse> res = new GeckoResult<>();
+
+        final DialogInterface.OnClickListener listener = (dialog, which) -> {
+            if (which == DialogInterface.BUTTON_POSITIVE) {
+                res.complete(prompt.confirm(AllowOrDeny.ALLOW));
+            } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+                res.complete(prompt.confirm(AllowOrDeny.DENY));
+            } else {
+                res.complete(prompt.dismiss());
+            }
+        };
+
+        builder.setPositiveButton(R.string.before_unload_leave_page, listener);
+        builder.setNegativeButton(R.string.before_unload_stay, listener);
+
+        createStandardDialog(builder, prompt, res).show();
+        return res;
+    }
+
     private int getViewPadding(final AlertDialog.Builder builder) {
         final TypedArray attr = builder.getContext().obtainStyledAttributes(
                 new int[] { android.R.attr.listPreferredItemPaddingLeft });
         final int padding = attr.getDimensionPixelSize(0, 1);
         attr.recycle();
         return padding;
     }
 
--- a/mobile/android/geckoview_example/src/main/res/values/strings.xml
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -53,16 +53,20 @@
     <string name="key_tracking_protection">tracking_protection</string>
     <string name="key_enhanced_tracking_protection">enhanced_tracking_protection</string>
     <string name="key_dfpi">dfpi</string>
     <string name="key_autoplay">autoplay</string>
     <string name="key_remote_debugging">remote_debugging</string>
     <string name="key_allow_extensions_in_private_browsing">allow_extensions_in_private_browsing</string>
     <string name="key_preferred_color_scheme">preferred_color_scheme</string>
     <string name="key_user_agent_override">user_agent_override</string>
+    <string name="before_unload_message">This page is asking you to confirm that you want to leave - data you have entered may not be saved</string>
+    <string name="before_unload_title">Are you sure?</string>
+    <string name="before_unload_leave_page">Leave Page</string>
+    <string name="before_unload_stay">Stay on Page</string>
 
     <string-array name="pref_preferred_color_scheme_display_names">
         <item>Follow System Preference</item>
         <item>Light</item>
         <item>Dark</item>
     </string-array>
     <string-array name="pref_preferred_color_scheme_values">
         <item>-1</item>