Bug 1533057 - Created Web Notifications API. r=geckoview-reviewers,agi,snorp
☠☠ backed out by 691f28f0517f ☠ ☠
authorAlvina Waseem <awaseem@mozilla.com>
Thu, 15 Aug 2019 20:13:57 +0000
changeset 488344 6d823f106e69afd267b50f2737734ebb9c2ed71f
parent 488343 69224eff4d072682565cc98fcce41f0f4c6be8eb
child 488345 77ba426bcfc57ca743dbe6ed640a27ffec0864a7
push id113906
push userncsoregi@mozilla.com
push dateFri, 16 Aug 2019 04:07:24 +0000
treeherdermozilla-inbound@d887276421d3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgeckoview-reviewers, agi, snorp
bugs1533057
milestone70.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 1533057 - Created Web Notifications API. r=geckoview-reviewers,agi,snorp Differential Revision: https://phabricator.services.mozilla.com/D36342
mobile/android/components/geckoview/GeckoViewPermission.js
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebNotificationTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebNotification.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebNotificationDelegate.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/GeckoViewActivity.java
mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_status_logo.png
widget/android/AndroidAlerts.cpp
widget/android/AndroidAlerts.h
--- a/mobile/android/components/geckoview/GeckoViewPermission.js
+++ b/mobile/android/components/geckoview/GeckoViewPermission.js
@@ -258,15 +258,21 @@ GeckoViewPermission.prototype = {
         return true;
       })
       .catch(error => {
         Cu.reportError("Permission error: " + error);
         return /* granted */ false;
       })
       .then(granted => {
         (granted ? aRequest.allow : aRequest.cancel)();
+        Services.perms.addFromPrincipal(
+          aRequest.principal,
+          "desktop-notification",
+          Services.perms.ALLOW_ACTION,
+          Services.perms.EXPIRE_SESSION
+        );
         // Manually release the target request here to facilitate garbage collection.
         aRequest = undefined;
       });
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([GeckoViewPermission]);
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -77,16 +77,18 @@ import org.mozilla.geckoview.ScreenLengt
 import org.mozilla.geckoview.SessionAccessibility;
 import org.mozilla.geckoview.SessionFinder;
 import org.mozilla.geckoview.SessionTextInput;
 import org.mozilla.geckoview.StorageController;
 import org.mozilla.geckoview.WebExtension;
 import org.mozilla.geckoview.WebExtensionController;
 import org.mozilla.geckoview.WebExtensionEventDispatcher;
 import org.mozilla.geckoview.WebMessage;
+import org.mozilla.geckoview.WebNotification;
+import org.mozilla.geckoview.WebNotificationDelegate;
 import org.mozilla.geckoview.WebRequest;
 import org.mozilla.geckoview.WebRequestError;
 import org.mozilla.geckoview.WebResponse;
 
 package org.mozilla.geckoview {
 
   @AnyThread public final enum AllowOrDeny {
     method public static AllowOrDeny valueOf(String);
@@ -293,33 +295,34 @@ package org.mozilla.geckoview {
     method @AnyThread @Nullable public GeckoResult<U> onValue(@Nullable T);
   }
 
   public static final class GeckoResult.UncaughtException extends RuntimeException {
     ctor public UncaughtException(Throwable);
   }
 
   public final class GeckoRuntime implements Parcelable {
-    ctor public GeckoRuntime();
     method @UiThread public void attachTo(@NonNull Context);
     method @UiThread public void configurationChanged(@NonNull Configuration);
     method @UiThread @NonNull public static GeckoRuntime create(@NonNull Context);
     method @UiThread @NonNull public static GeckoRuntime create(@NonNull Context, @NonNull GeckoRuntimeSettings);
     method @UiThread @NonNull public static synchronized GeckoRuntime getDefault(@NonNull Context);
     method @UiThread @Nullable public GeckoRuntime.Delegate getDelegate();
     method @UiThread @Nullable public File getProfileDir();
     method @AnyThread @NonNull public GeckoRuntimeSettings getSettings();
     method @UiThread @NonNull public StorageController getStorageController();
     method @UiThread @NonNull public RuntimeTelemetry getTelemetry();
     method @UiThread @NonNull public WebExtensionController getWebExtensionController();
+    method @UiThread @Nullable public WebNotificationDelegate getWebNotificationDelegate();
     method @UiThread public void orientationChanged();
     method @UiThread public void orientationChanged(int);
     method @AnyThread public void readFromParcel(@NonNull Parcel);
     method @UiThread @NonNull public GeckoResult<Void> registerWebExtension(@NonNull WebExtension);
     method @UiThread public void setDelegate(@Nullable GeckoRuntime.Delegate);
+    method @UiThread public void setWebNotificationDelegate(@Nullable WebNotificationDelegate);
     method @AnyThread public void shutdown();
     method @UiThread @NonNull public GeckoResult<Void> unregisterWebExtension(@NonNull WebExtension);
     field public static final String ACTION_CRASHED = "org.mozilla.gecko.ACTION_CRASHED";
     field public static final Parcelable.Creator<GeckoRuntime> CREATOR;
     field public static final String EXTRA_CRASH_FATAL = "fatal";
     field public static final String EXTRA_EXTRAS_PATH = "extrasPath";
     field public static final String EXTRA_MINIDUMP_PATH = "minidumpPath";
   }
@@ -1237,16 +1240,33 @@ package org.mozilla.geckoview {
   }
 
   @AnyThread public abstract static class WebMessage.Builder {
     method @NonNull public WebMessage.Builder addHeader(@NonNull String, @NonNull String);
     method @NonNull public WebMessage.Builder header(@NonNull String, @NonNull String);
     method @NonNull public WebMessage.Builder uri(@NonNull String);
   }
 
+  public class WebNotification {
+    method @UiThread public void click();
+    method @UiThread public void dismiss();
+    field @Nullable public final String imageUrl;
+    field @Nullable public final String lang;
+    field @NonNull public final boolean requireInteraction;
+    field @NonNull public final String tag;
+    field @Nullable public final String text;
+    field @Nullable public final String textDirection;
+    field @Nullable public final String title;
+  }
+
+  public interface WebNotificationDelegate {
+    method @AnyThread default public void onCloseNotification(@NonNull WebNotification);
+    method @AnyThread default public void onShowNotification(@NonNull WebNotification);
+  }
+
   @AnyThread public class WebRequest extends WebMessage {
     ctor public WebRequest(@NonNull String);
     field public static final int CACHE_MODE_DEFAULT = 1;
     field public static final int CACHE_MODE_FORCE_CACHE = 5;
     field public static final int CACHE_MODE_NO_CACHE = 4;
     field public static final int CACHE_MODE_NO_STORE = 2;
     field public static final int CACHE_MODE_ONLY_IF_CACHED = 6;
     field public static final int CACHE_MODE_RELOAD = 3;
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebNotificationTest.kt
@@ -0,0 +1,140 @@
+package org.mozilla.geckoview.test
+
+import android.support.test.filters.MediumTest
+import android.support.test.runner.AndroidJUnit4
+import org.hamcrest.Matchers.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.WebNotification
+import org.mozilla.geckoview.WebNotificationDelegate
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class WebNotificationTest : BaseSessionTest() {
+
+    @Before fun setup() {
+        mainSession.loadTestPath(HELLO_HTML_PATH)
+        mainSession.waitForPageStop()
+
+        val result = mainSession.waitForJS("Notification.requestPermission()")
+        assertThat("Permission should be granted",
+                result as String, equalTo("granted"))
+    }
+
+    @Test fun onShowNotification() {
+        val runtime = sessionRule.runtime
+        val notificationResult = GeckoResult<Void>()
+        val register = {  delegate: WebNotificationDelegate -> runtime.webNotificationDelegate = delegate}
+        val unregister = { _: WebNotificationDelegate -> runtime.webNotificationDelegate = null }
+
+        sessionRule.addExternalDelegateDuringNextWait(WebNotificationDelegate::class, register,
+            unregister, object : WebNotificationDelegate {
+                @GeckoSessionTestRule.AssertCalled
+                override fun onShowNotification(notification: WebNotification) {
+                    assertThat("Title should match", notification.title, equalTo("The Title"))
+                    assertThat("Body should match", notification.text, equalTo("The Text"))
+                    assertThat("Tag should match", notification.tag, endsWith("Tag"))
+                    assertThat("ImageUrl should match", notification.imageUrl, endsWith("icon.png"))
+                    assertThat("Language should match", notification.lang, equalTo("en-US"))
+                    assertThat("Direction should match", notification.textDirection, equalTo("ltr"))
+                    assertThat("Require Interaction should match", notification.requireInteraction, equalTo(true))
+                    notificationResult.complete(null)
+                }
+        })
+
+        mainSession.evaluateJS("""
+            new Notification('The Title', { body: 'The Text', cookie: 'Cookie',
+                icon: 'icon.png', tag: 'Tag', dir: 'ltr', lang: 'en-US',
+                requireInteraction: true });
+            """.trimIndent())
+
+        sessionRule.waitForResult(notificationResult)
+    }
+
+    @Test fun onCloseNotification() {
+        val runtime = sessionRule.runtime
+        val closeCalled = GeckoResult<Void>()
+        val register = {  delegate: WebNotificationDelegate -> runtime.webNotificationDelegate = delegate}
+        val unregister = { _: WebNotificationDelegate -> runtime.webNotificationDelegate = null }
+
+        sessionRule.addExternalDelegateDuringNextWait(WebNotificationDelegate::class, register,
+            unregister, object : WebNotificationDelegate {
+                @GeckoSessionTestRule.AssertCalled
+                override fun onCloseNotification(notification: WebNotification) {
+                    closeCalled.complete(null)
+                }
+        })
+
+        mainSession.evaluateJS("""
+            const notification = new Notification('The Title', { body: 'The Text'});
+            notification.close();
+        """.trimIndent())
+
+        sessionRule.waitForResult(closeCalled)
+    }
+
+    @Test fun clickNotification() {
+        val runtime = sessionRule.runtime
+        val notificationResult = GeckoResult<Void>()
+        val register = {  delegate: WebNotificationDelegate -> runtime.webNotificationDelegate = delegate}
+        val unregister = { _: WebNotificationDelegate -> runtime.webNotificationDelegate = null }
+        var notificationShown: WebNotification? = null
+
+        sessionRule.addExternalDelegateDuringNextWait(WebNotificationDelegate::class, register,
+            unregister, object : WebNotificationDelegate {
+                @GeckoSessionTestRule.AssertCalled
+                override fun onShowNotification(notification: WebNotification) {
+                    notificationShown = notification
+                    notificationResult.complete(null)
+                }
+        })
+
+        val promiseResult = mainSession.evaluatePromiseJS("""
+            new Promise(resolve => {
+                const notification = new Notification('The Title', { body: 'The Text' });
+                notification.onclick = function() {
+                    resolve(1);
+                }
+            });
+        """.trimIndent())
+
+        sessionRule.waitForResult(notificationResult)
+        notificationShown!!.click()
+
+        assertThat("Promise should have been resolved.", promiseResult.value as Double, equalTo(1.0))
+    }
+
+    @Test fun dismissNotification() {
+        val runtime = sessionRule.runtime
+        val notificationResult = GeckoResult<Void>()
+        val register = {  delegate: WebNotificationDelegate -> runtime.webNotificationDelegate = delegate}
+        val unregister = { _: WebNotificationDelegate -> runtime.webNotificationDelegate = null }
+        var notificationShown: WebNotification? = null
+
+        sessionRule.addExternalDelegateDuringNextWait(WebNotificationDelegate::class, register,
+            unregister, object : WebNotificationDelegate {
+                @GeckoSessionTestRule.AssertCalled
+                override fun onShowNotification(notification: WebNotification) {
+                    notificationShown = notification
+                    notificationResult.complete(null)
+                }
+        })
+
+        val promiseResult = mainSession.evaluatePromiseJS("""
+            new Promise(resolve => {
+                const notification = new Notification('The Title', { body: 'The Text'});
+                notification.onclose = function() {
+                    resolve(1);
+                }
+            });
+        """.trimIndent())
+
+        sessionRule.waitForResult(notificationResult)
+        notificationShown!!.dismiss()
+
+        assertThat("Promise should have been resolved", promiseResult.value as Double, equalTo(1.0))
+    }
+}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
@@ -30,16 +30,17 @@ import android.util.Log;
 
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoNetworkManager;
 import org.mozilla.gecko.GeckoScreenOrientation;
 import org.mozilla.gecko.GeckoSystemStateListener;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.ContextUtils;
 import org.mozilla.gecko.util.DebugConfig;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.yaml.snakeyaml.error.YAMLException;
 
@@ -151,26 +152,38 @@ public final class GeckoRuntime implemen
             sDefaultRuntime = new GeckoRuntime();
             sDefaultRuntime.attachTo(context);
             sDefaultRuntime.init(context, new GeckoRuntimeSettings());
         }
 
         return sDefaultRuntime;
     }
 
+    private static GeckoRuntime sRuntime;
     private GeckoRuntimeSettings mSettings;
     private Delegate mDelegate;
+    private WebNotificationDelegate mNotificationDelegate;
     private RuntimeTelemetry mTelemetry;
     private final WebExtensionEventDispatcher mWebExtensionDispatcher;
     private StorageController mStorageController;
     private final WebExtensionController mWebExtensionController;
 
-    public GeckoRuntime() {
+    private GeckoRuntime() {
         mWebExtensionDispatcher = new WebExtensionEventDispatcher();
         mWebExtensionController = new WebExtensionController(this, mWebExtensionDispatcher);
+        if (sRuntime != null) {
+            throw new IllegalStateException("Only one GeckoRuntime instance is allowed");
+        }
+        sRuntime = this;
+    }
+
+    @WrapForJNI
+    @UiThread
+    private @Nullable static GeckoRuntime getInstance() {
+        return sRuntime;
     }
 
     /**
      * Attach the runtime to the given context.
      *
      * @param context The new context to attach to.
      */
     @UiThread
@@ -498,16 +511,57 @@ public final class GeckoRuntime implemen
      *
      * @return an instance of {@link GeckoRuntime.Delegate} or null if no delegate has been set.
      */
     @UiThread
     public @Nullable Delegate getDelegate() {
         return mDelegate;
     }
 
+    /**
+     * Sets the delegate to be used for handling Web Notifications.
+     *
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification">Web Notifications</a>
+     */
+    @UiThread
+    public void setWebNotificationDelegate(final @Nullable WebNotificationDelegate delegate) {
+        mNotificationDelegate = delegate;
+    }
+
+    /**
+     * Returns the current WebNotificationDelegate, if any
+     *
+     * @return an instance of  WebNotificationDelegate or null if no delegate has been set
+     */
+    @WrapForJNI
+    @UiThread
+    public @Nullable WebNotificationDelegate getWebNotificationDelegate() {
+        return mNotificationDelegate;
+    }
+
+    @WrapForJNI
+    @UiThread
+    private void notifyOnShow(final WebNotification notification) {
+        ThreadUtils.getUiHandler().post(() -> {
+            if (mNotificationDelegate != null) {
+                mNotificationDelegate.onShowNotification(notification);
+            }
+        });
+    }
+
+    @WrapForJNI
+    @UiThread
+    private void notifyOnClose(final WebNotification notification) {
+        ThreadUtils.getUiHandler().post(() -> {
+            if (mNotificationDelegate != null) {
+                mNotificationDelegate.onCloseNotification(notification);
+            }
+        });
+    }
+
     @AnyThread
     public @NonNull GeckoRuntimeSettings getSettings() {
         return mSettings;
     }
 
     /* package */ void setPref(final String name, final Object value,
                                final boolean override) {
         if (override || !GeckoAppShell.isFennec()) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebNotification.java
@@ -0,0 +1,99 @@
+package org.mozilla.geckoview;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class WebNotification {
+
+    /**
+     * Title is shown at the top of the notification window.
+     *
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/title">Web Notification - title</a>
+     */
+    public final @Nullable String title;
+
+    /**
+     * Tag is the ID of the notification.
+     *
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/tag">Web Notification - tag</a>
+     */
+    public final @NonNull String tag;
+
+    private final @Nullable String mCookie;
+
+    /**
+     * Text represents the body of the notification.
+     *
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/body">Web Notification - text</a>
+     */
+    public final @Nullable String text;
+
+    /**
+     * ImageURL contains the URL of an icon to be displayed as part of the notification.
+     *
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/icon">Web Notification - icon</a>
+     */
+    public final @Nullable String imageUrl;
+
+    /**
+     * TextDirection indicates the direction that the language of the text is displayed.
+     * Possible values are:
+     * auto: adopts the browser's language setting behaviour (the default.)
+     * ltr: left to right.
+     * rtl: right to left.
+     *
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/dir">Web Notification - dir</a>
+     */
+    public final @Nullable String textDirection;
+
+    /**
+     * Lang indicates the notification's language, as specified using a DOMString
+     * representing a BCP 47 language tag.
+     *
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/DOMString">DOM String</a>
+     * @see <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">BCP 47</a>
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/lang">Web Notification - lang</a>
+     */
+    public final @Nullable String lang;
+
+    /**
+     * RequireInteraction indicates whether a notification should remain active until the user
+     * clicks or dismisses it, rather than closing automatically.
+     *
+     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/requireInteraction">Web Notification - requireInteraction</a>
+     */
+    public final @NonNull boolean requireInteraction;
+
+    @WrapForJNI
+    /* package */ WebNotification(@Nullable final String title, @NonNull final String tag,
+                        @Nullable final String cookie, @Nullable final String text,
+                        @Nullable final String imageUrl, @Nullable final String textDirection,
+                        @Nullable final String lang, @NonNull final boolean requireInteraction) {
+        this.tag = tag;
+        this.mCookie = cookie;
+        this.title = title;
+        this.text = text;
+        this.imageUrl = imageUrl;
+        this.textDirection = textDirection;
+        this.lang = lang;
+        this.requireInteraction = requireInteraction;
+    }
+
+    @UiThread
+    public void click() {
+        ThreadUtils.assertOnUiThread();
+        GeckoAppShell.onNotificationClick(tag, mCookie);
+
+    }
+
+    @UiThread
+    public void dismiss() {
+        ThreadUtils.assertOnUiThread();
+        GeckoAppShell.onNotificationClose(tag, mCookie);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebNotificationDelegate.java
@@ -0,0 +1,27 @@
+package org.mozilla.geckoview;
+
+import android.support.annotation.AnyThread;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public interface WebNotificationDelegate {
+    /**
+     * This is called when a new notification is created.
+     *
+     * @param notification The WebNotification received.
+     */
+    @AnyThread
+    @WrapForJNI
+    default void onShowNotification(@NonNull WebNotification notification) {}
+
+    /**
+     * This is called when an existing notification is closed.
+     *
+     * @param notification The WebNotification received.
+     */
+    @AnyThread
+    @WrapForJNI
+    default void onCloseNotification(@NonNull WebNotification notification) {}
+}
+
--- 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
@@ -39,31 +39,46 @@ exclude: true
   ([bug 1499394]({{bugzilla}}1499394))
 - Added [`RuntimeTelemetry.Delegate`][70.12] that receives streaming telemetry
   data from GeckoView.
   ([bug 1566367]({{bugzilla}}1566367))
 - Updated [`ContentBlocking`][70.13] to better report blocked and allowed ETP events.
   ([bug 1567268]({{bugzilla}}1567268))
 - Added API for controlling Gecko logging [`GeckoRuntimeSettings.debugLogging`][70.14]
   ([bug 1573304]({{bugzilla}}1573304))
+- Added API for session context assignment
+  [`GeckoSessionSettings.Builder.contextId`][70.1] and deletion of data
+  related to a session context
+  [`StorageController.clearDataForSessionContext`][70.2].
+- Removed `setSession(session, runtime)` from [`GeckoView`][70.5]. With this change, `GeckoView` will no longer
+  manage opening/closing of the [`GeckoSession`][70.6] and instead leave that up to the app. It's also now allowed
+  to call [`setSession`][70.10] with a closed `GeckoSession`.
+- Added an overload of [`GeckoSession.loadUri()`][70.8] that accepts a referring [`GeckoSession`][70.6]. This should be used
+  when the URI we're loading originates from another page. A common example of this would be long pressing
+  a link and then opening that in a new `GeckoSession`.
+- Added capture parameter to [`onFilePrompt`][70.9] and corresponding [`CAPTURE_TYPE_*`][70.7] constants.
+- Added [`WebNotification`][70.11] and [`WebNotificationDelegate`][70.12] for handling Web Notifications.
+  ([bug 1533057]({{bugzilla}}1533057))
 
 [70.1]: {{javadoc_uri}}/GeckoSessionSettings.Builder.html#contextId-java.lang.String-
 [70.2]: {{javadoc_uri}}/StorageController.html#clearDataForSessionContext-java.lang.String-
 [70.3]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-java.io.File-java.io.File-java.lang.String-
 [70.4]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-java.io.File-java.util.Map-java.lang.String-
 [70.5]: {{javadoc_uri}}/GeckoView.html
 [70.6]: {{javadoc_uri}}/GeckoSession.html
 [70.7]: {{javadoc_uri}}/GeckoSession.PromptDelegate.html#CAPTURE_TYPE_NONE
 [70.8]: {{javadoc_uri}}/GeckoSession.html#loadUri-java.lang.String-org.mozilla.geckoview.GeckoSession-int-
 [70.9]: {{javadoc_uri}}/GeckoSession.PromptDelegate.html#onFilePrompt-org.mozilla.geckoview.GeckoSession-java.lang.String-int-java.lang.String:A-int-org.mozilla.geckoview.GeckoSession.PromptDelegate.FileCallback-
 [70.10]: {{javadoc_uri}}/GeckoView.html#setSession-org.mozilla.geckoview.GeckoSession-
 [70.11]: {{javadoc_uri}}/GeckoSession.PromptDelegate.html
 [70.12]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html
 [70.13]: {{javadoc_uri}}/ContentBlocking.html
 [70.14]: {{javadoc_uri}}/GeckoRuntimeSettings.Builder.html#debugLogging-boolean-
+[70.11]: {{javadoc_uri}}/WebNotification.html
+[70.12]: {{javadoc_uri}}/WebNotificationDelegate.html
 
 ## v69
 - Modified behavior of ['setAutomaticFontSizeAdjustment'][69.1] so that it no 
   longer has any effect on ['setFontInflationEnabled'][69.2]
 - Add [GeckoSession.LOAD_FLAGS_FORCE_ALLOW_DATA_URI][69.14]
 - Added [`GeckoResult.accept`][69.3] for consuming a result without
   transforming it.
 - [`GeckoSession.setMessageDelegate`][69.13] callers must now specify the
@@ -306,9 +321,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]: 99204a5c93667e6d440e55d5d330d01cf4e8783f
+[api-version]: 15b1503a237289c51c147c7760afd7ff726d8809
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -11,30 +11,39 @@ import org.mozilla.geckoview.AllowOrDeny
 import org.mozilla.geckoview.BasicSelectionActionDelegate;
 import org.mozilla.geckoview.ContentBlocking;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.GeckoView;
+import org.mozilla.geckoview.GeckoWebExecutor;
 import org.mozilla.geckoview.WebExtension;
 import org.mozilla.geckoview.WebExtensionController;
+import org.mozilla.geckoview.WebNotification;
+import org.mozilla.geckoview.WebNotificationDelegate;
+import org.mozilla.geckoview.WebRequest;
 import org.mozilla.geckoview.WebRequestError;
 import org.mozilla.geckoview.RuntimeTelemetry;
+import org.mozilla.geckoview.WebResponse;
 
 import android.Manifest;
 import android.app.Activity;
 import android.app.DownloadManager;
+import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.SystemClock;
 import android.support.annotation.NonNull;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.NotificationCompat;
@@ -50,16 +59,17 @@ import android.view.View;
 import android.view.WindowManager;
 import android.widget.ProgressBar;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Locale;
 
 public class GeckoViewActivity extends AppCompatActivity {
     private static final String LOGTAG = "GeckoViewActivity";
     private static final String USE_MULTIPROCESS_EXTRA = "use_multiprocess";
     private static final String FULL_ACCESSIBILITY_TREE_EXTRA = "full_accessibility_tree";
@@ -85,16 +95,20 @@ public class GeckoViewActivity extends A
     private ArrayList<String> mAcceptedPersistentStorage = new ArrayList<String>();
 
     private ToolbarLayout mToolbarView;
     private String mCurrentUri;
     private boolean mCanGoBack;
     private boolean mCanGoForward;
     private boolean mFullScreen;
 
+    private HashMap<String, Integer> mNotificationIDMap = new HashMap<>();
+    private HashMap<Integer, WebNotification> mNotificationMap = new HashMap<>();
+    private int mLastID = 100;
+
     private ProgressBar mProgressView;
 
     private LinkedList<GeckoSession.WebResponseInfo> mPendingDownloads = new LinkedList<>();
 
     private LocationView.CommitListener mCommitListener = new LocationView.CommitListener() {
         @Override
         public void onCommit(String text) {
             if ((text.contains(".") || text.contains(":")) && !text.contains(" ")) {
@@ -170,16 +184,64 @@ public class GeckoViewActivity extends A
                 }
                 @Override
                 public GeckoResult<AllowOrDeny> onCloseTab(WebExtension source, GeckoSession session) {
                     TabSession tabSession = mTabSessionManager.getSession(session);
                     closeTab(tabSession);
                     return GeckoResult.ALLOW;
                 }
             });
+
+            // `getSystemService` call requires API level 23
+            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+                sGeckoRuntime.setWebNotificationDelegate(new WebNotificationDelegate() {
+                    NotificationManager notificationManager = getSystemService(NotificationManager.class);
+                    @Override
+                    public void onShowNotification(@NonNull WebNotification notification) {
+                        Intent clickIntent = new Intent(GeckoViewActivity.this, GeckoViewActivity.class);
+                        clickIntent.putExtra("onClick",notification.tag);
+                        PendingIntent dismissIntent = PendingIntent.getActivity(GeckoViewActivity.this, mLastID, clickIntent, 0);
+
+                        Notification.Builder builder = new Notification.Builder(GeckoViewActivity.this)
+                                .setContentTitle(notification.title)
+                                .setContentText(notification.text)
+                                .setSmallIcon(R.drawable.ic_status_logo)
+                                .setContentIntent(dismissIntent)
+                                .setAutoCancel(true);
+
+                        mNotificationIDMap.put(notification.tag, mLastID);
+                        mNotificationMap.put(mLastID, notification);
+
+                        if (notification.imageUrl != null && notification.imageUrl.length() > 0) {
+                            final GeckoWebExecutor executor = new GeckoWebExecutor(sGeckoRuntime);
+
+                            GeckoResult<WebResponse> response = executor.fetch(
+                                    new WebRequest.Builder(notification.imageUrl)
+                                            .addHeader("Accept", "image")
+                                            .build());
+                            response.accept(value -> {
+                                Bitmap bitmap = BitmapFactory.decodeStream(value.body);
+                                builder.setLargeIcon(Icon.createWithBitmap(bitmap));
+                                notificationManager.notify(mLastID++, builder.build());
+                            });
+                        } else {
+                            notificationManager.notify(mLastID++, builder.build());
+                        }
+
+                    }
+
+                    @Override
+                    public void onCloseNotification(@NonNull WebNotification notification) {
+                        notificationManager.cancel(mNotificationIDMap.get(notification.tag));
+                        mNotificationIDMap.remove(notification.tag);
+                    }
+                });
+
+
+            }
         }
 
         if(savedInstanceState == null) {
             TabSession session = getIntent().getParcelableExtra("session");
             if (session != null) {
                 connectSession(session);
 
                 if (!session.isOpen()) {
@@ -425,28 +487,36 @@ public class GeckoViewActivity extends A
     public void onDestroy() {
         if (mKillProcessOnDestroy) {
             android.os.Process.killProcess(android.os.Process.myPid());
         }
 
         super.onDestroy();
     }
 
+    @Override
     protected void onNewIntent(final Intent intent) {
         super.onNewIntent(intent);
 
         if (ACTION_SHUTDOWN.equals(intent.getAction())) {
             mKillProcessOnDestroy = true;
             if (sGeckoRuntime != null) {
                 sGeckoRuntime.shutdown();
             }
             finish();
             return;
         }
 
+        if (intent.hasExtra("onClick")) {
+            int key = intent.getExtras().getInt("onClick");
+            WebNotification notification = mNotificationMap.get(key);
+            notification.click();
+            mNotificationMap.remove(key);
+        }
+
         setIntent(intent);
 
         if (intent.getData() != null) {
             loadFromIntent(intent);
         }
     }
 
 
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..374ef69857e23d816e070e0c0f6067523c3f3263
GIT binary patch
literal 717
zc$@*v0y6!HP)<h;3K|Lk000e1NJLTq001Na001Ni1ONa4O9@aD0007%Nkl<ZNXM;~
zOGs2v7{_NcBeDlWx=Ik!a3K*AEu_by2WUl`utkBi8<5&W5H2cG=tW6Ev?!u1vY?Xq
z{^wq2QkkHl7D8wxEh2i+3nMe7Npnu;-nrNNIOpDJ%5yd|_xsQ9eCM41_sxw&@*1o$
zQ5E_Bp_k$-uFL=kr-_TXFZYTI!lJ2p^uqulW1zIPfkLeu55qv(b~7Jy;s_q6W(6gF
zoqlWOo}-N=YMd-ty8|*|O#uf<QzG2gU&Nz4D!!NDmU!|4i){s6^@8Y=6LaUY!&+xt
z{CpHW_#i&N>|Nw2=#me_=}~`?3_2vqINRz`?3NWcDT9~-)aX_uD=;B}GFa`!G|3n-
zf1v|=slna=&M0zv!V9J`&u7zHCCHk3tpzGk>9l(17~^3WoOj)ACEiK#o6g310}vUy
zjpe?wT|&=%stL<$EEWiARhNqYV%Qopzzb<u0U5&>+oJ*reKT%&v>n+RTBgjVPA^MV
z1Qb&jx@f?q9m#HO112TVRBBF0J2@c~cilEIA9q*dqavfWkUn|9IAWxywS(C`7hp)a
zx#PUhh$pJ3L9g^=bk3*kv;+48JT#0HUT3$8R>#2@^ex$92QGve(khnsc2;_=-)-I1
zHfU3*v&V7DX(v2NeLlq<SFPc@0&o;NDX!If?~jg2mAJ}%e3uoVw6@E8TMy>rqNEz~
zI%BEAONQ(o#f3fQvKXyohw(+B8StCzY|aB$&x1ZtvRLh=@F^~$MDlB*hDP(mq+LzT
zv%NZu<bipE^}$uN!g6vh$Ruk|7Ki@-_bjB&oS2_>7tb-Yjd&UY`o=c@^$QG->g#Zg
zK1+~SXwyso;f+YN`fU_P3)@f4+LoY>Iiuqb!DD%0i|>-B00000NkvXXu0mjf%j8Wf
--- a/widget/android/AndroidAlerts.cpp
+++ b/widget/android/AndroidAlerts.cpp
@@ -8,16 +8,18 @@
 #include "nsAlertsUtils.h"
 
 namespace mozilla {
 namespace widget {
 
 NS_IMPL_ISUPPORTS(AndroidAlerts, nsIAlertsService)
 
 StaticAutoPtr<AndroidAlerts::ListenerMap> AndroidAlerts::sListenerMap;
+nsDataHashtable<nsStringHashKey, java::WebNotification::GlobalRef>
+    AndroidAlerts::mNotificationsMap;
 
 NS_IMETHODIMP
 AndroidAlerts::ShowAlertNotification(
     const nsAString& aImageUrl, const nsAString& aAlertTitle,
     const nsAString& aAlertText, bool aAlertTextClickable,
     const nsAString& aAlertCookie, nsIObserver* aAlertListener,
     const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
     const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
@@ -56,44 +58,78 @@ AndroidAlerts::ShowPersistentNotificatio
   nsAutoString cookie;
   rv = aAlert->GetCookie(cookie);
   NS_ENSURE_SUCCESS(rv, NS_OK);
 
   nsAutoString name;
   rv = aAlert->GetName(name);
   NS_ENSURE_SUCCESS(rv, NS_OK);
 
+  nsAutoString lang;
+  rv = aAlert->GetLang(lang);
+  NS_ENSURE_SUCCESS(rv, NS_OK);
+
+  nsAutoString dir;
+  rv = aAlert->GetDir(dir);
+  NS_ENSURE_SUCCESS(rv, NS_OK);
+
+  bool requireInteraction;
+  rv = aAlert->GetRequireInteraction(&requireInteraction);
+  NS_ENSURE_SUCCESS(rv, NS_OK);
+
   nsCOMPtr<nsIPrincipal> principal;
   rv = aAlert->GetPrincipal(getter_AddRefs(principal));
   NS_ENSURE_SUCCESS(rv, NS_OK);
 
   nsAutoString host;
   nsAlertsUtils::GetSourceHostPort(principal, host);
 
   if (aPersistentData.IsEmpty() && aAlertListener) {
     if (!sListenerMap) {
       sListenerMap = new ListenerMap();
     }
     // This will remove any observers already registered for this name.
     sListenerMap->Put(name, aAlertListener);
   }
 
-  java::GeckoAppShell::ShowNotification(
-      name, cookie, title, text, host, imageUrl,
-      !aPersistentData.IsEmpty() ? jni::StringParam(aPersistentData)
-                                 : jni::StringParam(nullptr));
+  if (jni::IsFennec()) {
+    java::GeckoAppShell::ShowNotification(
+        name, cookie, title, text, host, imageUrl,
+        !aPersistentData.IsEmpty() ? jni::StringParam(aPersistentData)
+                                   : jni::StringParam(nullptr));
+  } else {
+    java::GeckoRuntime::LocalRef runtime = java::GeckoRuntime::GetInstance();
+    java::WebNotificationDelegate::LocalRef delegate =
+        runtime->GetWebNotificationDelegate();
+    java::WebNotification::LocalRef notification = notification->New(
+        title, name, cookie, text, imageUrl, dir, lang, requireInteraction);
+    runtime->NotifyOnShow(notification);
+    mNotificationsMap.Put(name, notification);
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 AndroidAlerts::CloseAlert(const nsAString& aAlertName,
                           nsIPrincipal* aPrincipal) {
-  // We delete the entry in sListenerMap later, when CloseNotification calls
-  // NotifyListener.
-  java::GeckoAppShell::CloseNotification(aAlertName);
+  if (jni::IsFennec()) {
+    // We delete the entry in sListenerMap later, when CloseNotification calls
+    // NotifyListener.
+    java::GeckoAppShell::CloseNotification(aAlertName);
+  } else {
+    java::WebNotification::LocalRef notification =
+        mNotificationsMap.Get(aAlertName);
+    java::GeckoRuntime::LocalRef runtime = java::GeckoRuntime::GetInstance();
+    java::WebNotificationDelegate::LocalRef delegate =
+        runtime->GetWebNotificationDelegate();
+    runtime->NotifyOnClose(notification);
+    mNotificationsMap.Remove(aAlertName);
+  }
+
   return NS_OK;
 }
 
 void AndroidAlerts::NotifyListener(const nsAString& aName, const char* aTopic,
                                    const char16_t* aCookie) {
   if (!sListenerMap) {
     return;
   }
@@ -102,13 +138,14 @@ void AndroidAlerts::NotifyListener(const
   if (!listener) {
     return;
   }
 
   listener->Observe(nullptr, aTopic, aCookie);
 
   if (NS_LITERAL_CSTRING("alertfinished").Equals(aTopic)) {
     sListenerMap->Remove(aName);
+    mNotificationsMap.Remove(aName);
   }
 }
 
 }  // namespace widget
 }  // namespace mozilla
--- a/widget/android/AndroidAlerts.h
+++ b/widget/android/AndroidAlerts.h
@@ -22,16 +22,19 @@ class AndroidAlerts : public nsIAlertsSe
   NS_DECL_ISUPPORTS
   NS_DECL_NSIALERTSSERVICE
 
   AndroidAlerts() {}
 
   static void NotifyListener(const nsAString& aName, const char* aTopic,
                              const char16_t* aCookie);
 
+  static nsDataHashtable<nsStringHashKey, java::WebNotification::GlobalRef>
+      mNotificationsMap;
+
  protected:
   virtual ~AndroidAlerts() { sListenerMap = nullptr; }
 
   using ListenerMap = nsInterfaceHashtable<nsStringHashKey, nsIObserver>;
   static StaticAutoPtr<ListenerMap> sListenerMap;
 };
 
 }  // namespace widget