Bug 1522451 - Add ContentDelegate.onWebAppManifest() r=geckoview-reviewers,agi,droeh
☠☠ backed out by 0ec36abbdb31 ☠ ☠
authorJames Willcox <snorp@snorp.net>
Thu, 14 Mar 2019 19:18:01 +0000
changeset 521941 2ddf151f9535ce36ebc56513ff3917096041eed5
parent 521940 49ebb69e88937a8eabf435f90f47c0d4cf28aed0
child 521942 5a892aa56e3748ce956064e55d05a80bead278eb
push id10870
push usernbeleuzu@mozilla.com
push dateFri, 15 Mar 2019 20:00:07 +0000
treeherdermozilla-beta@c594aee5b7a4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgeckoview-reviewers, agi, droeh
bugs1522451
milestone67.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 1522451 - Add ContentDelegate.onWebAppManifest() r=geckoview-reviewers,agi,droeh This delivers a parsed and validated Web App Manifest to the application, if present, during the page load process. Differential Revision: https://phabricator.services.mozilla.com/D22612
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
mobile/android/chrome/geckoview/GeckoViewContentChild.js
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/assets/www/hello.html
mobile/android/geckoview/src/androidTest/assets/www/manifest.webmanifest
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.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/GeckoViewActivity.java
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -26,16 +26,17 @@ import android.support.v7.widget.Toolbar
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.ProgressBar;
 
+import org.json.JSONObject;
 import org.mozilla.gecko.ActivityHandlerHelper;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.Clipboard;
 import org.mozilla.gecko.DoorHangerPopup;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.FormAssistPopup;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.R;
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -16,16 +16,17 @@ import android.os.Bundle;
 import android.support.customtabs.CustomTabsIntent;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.view.ActionMode;
 import android.util.Log;
 import android.view.Window;
 import android.view.WindowManager;
 import android.widget.Toast;
 
+import org.json.JSONObject;
 import org.mozilla.gecko.ActivityHandlerHelper;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.DoorHangerPopup;
 import org.mozilla.gecko.FormAssistPopup;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.GeckoScreenOrientation;
 import org.mozilla.gecko.R;
--- a/mobile/android/chrome/geckoview/GeckoViewContentChild.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContentChild.js
@@ -16,16 +16,17 @@ const SCREEN_LENGTH_DOCUMENT_HEIGHT = 4;
 
 // This need to match PanZoomController.java
 const SCROLL_BEHAVIOR_SMOOTH = 0;
 const SCROLL_BEHAVIOR_AUTO = 1;
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm",
   GeckoViewAutoFill: "resource://gre/modules/GeckoViewAutoFill.jsm",
+  ManifestObtainer: "resource://gre/modules/ManifestObtainer.jsm",
   PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
   SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm",
 });
 
 class GeckoViewContentChild extends GeckoViewChildModule {
   onInit() {
     debug `onInit`;
 
@@ -72,29 +73,31 @@ class GeckoViewContentChild extends Geck
     addEventListener("DOMTitleChanged", this, false);
     addEventListener("DOMWindowFocus", this, false);
     addEventListener("DOMWindowClose", this, false);
     addEventListener("MozDOMFullscreen:Entered", this, false);
     addEventListener("MozDOMFullscreen:Exit", this, false);
     addEventListener("MozDOMFullscreen:Exited", this, false);
     addEventListener("MozDOMFullscreen:Request", this, false);
     addEventListener("contextmenu", this, { capture: true });
+    addEventListener("DOMContentLoaded", this, false);
   }
 
   onDisable() {
     debug `onDisable`;
 
     removeEventListener("DOMTitleChanged", this);
     removeEventListener("DOMWindowFocus", this);
     removeEventListener("DOMWindowClose", this);
     removeEventListener("MozDOMFullscreen:Entered", this);
     removeEventListener("MozDOMFullscreen:Exit", this);
     removeEventListener("MozDOMFullscreen:Exited", this);
     removeEventListener("MozDOMFullscreen:Request", this);
     removeEventListener("contextmenu", this, { capture: true });
+    removeEventListener("DOMContentLoaded", this);
   }
 
   collectSessionState() {
     let history = SessionHistory.collect(docShell);
     let formdata = SessionStoreUtils.collectFormData(content);
     let scrolldata = SessionStoreUtils.collectScrollPosition(content);
 
     // Save the current document resolution.
@@ -420,16 +423,34 @@ class GeckoViewContentChild extends Geck
       case "mozcaretstatechanged":
         if (aEvent.reason === "presscaret" || aEvent.reason === "releasecaret") {
           this.eventDispatcher.sendRequest({
             type: "GeckoView:PinOnScreen",
             pinned: aEvent.reason === "presscaret",
           });
         }
         break;
+      case "DOMContentLoaded": {
+        content.requestIdleCallback(async () => {
+          let manifest = null;
+          try {
+            manifest = await ManifestObtainer.contentObtainManifest(content);
+          } catch (e) {
+            // Unfortunately, this throws if there is no manifest present, so we
+            // probably don't want to log anything here. Bug 1534756.
+          }
+
+          if (manifest) {
+            this.eventDispatcher.sendRequest({
+              type: "GeckoView:WebAppManifest",
+              manifest,
+            });
+          }
+        });
+      }
     }
   }
 
   // WebProgress event handler.
   onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
     debug `onLocationChange`;
 
     if (this._savedState) {
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -348,16 +348,17 @@ package org.mozilla.geckoview {
     method @android.support.annotation.UiThread default public void onCloseRequest(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession);
     method @android.support.annotation.UiThread default public void onContextMenu(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession, int, int, @android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement);
     method @android.support.annotation.UiThread default public void onCrash(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession);
     method @android.support.annotation.UiThread default public void onExternalResponse(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession, @android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession.WebResponseInfo);
     method @android.support.annotation.UiThread default public void onFirstComposite(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession);
     method @android.support.annotation.UiThread default public void onFocusRequest(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession);
     method @android.support.annotation.UiThread default public void onFullScreen(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession, boolean);
     method @android.support.annotation.UiThread default public void onTitleChange(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession, @android.support.annotation.Nullable java.lang.String);
+    method @android.support.annotation.UiThread default public void onWebAppManifest(@android.support.annotation.NonNull org.mozilla.geckoview.GeckoSession, @android.support.annotation.NonNull org.json.JSONObject);
   }
 
   public static class GeckoSession.ContentDelegate.ContextElement {
     ctor protected ContextElement(@android.support.annotation.Nullable java.lang.String, @android.support.annotation.Nullable java.lang.String, @android.support.annotation.Nullable java.lang.String, @android.support.annotation.Nullable java.lang.String, @android.support.annotation.NonNull java.lang.String, @android.support.annotation.Nullable java.lang.String);
     field public static final int TYPE_AUDIO = 3;
     field public static final int TYPE_IMAGE = 1;
     field public static final int TYPE_NONE = 0;
     field public static final int TYPE_VIDEO = 2;
--- a/mobile/android/geckoview/src/androidTest/assets/www/hello.html
+++ b/mobile/android/geckoview/src/androidTest/assets/www/hello.html
@@ -1,6 +1,9 @@
 <html>
-    <head><title>Hello, world!</title></head>
+    <head>
+        <title>Hello, world!</title>
+        <link rel="manifest" href="manifest.webmanifest">
+    </head>
     <body>
         <p>Hello, world!</p>
     </body>
 </html>
\ No newline at end of file
--- a/mobile/android/geckoview/src/androidTest/assets/www/manifest.webmanifest
+++ b/mobile/android/geckoview/src/androidTest/assets/www/manifest.webmanifest
@@ -0,0 +1,17 @@
+{
+  "name": "App",
+  "short_name": "app",
+  "start_url": "./start/index.html",
+  "display": "standalone",
+  "background_color": "#c0feee",
+  "theme_color": "cadetblue",
+  "icons": [{
+    "src": "images/test.gif",
+    "sizes": "192x192",
+    "type": "image/gif"
+  }],
+  "related_applications": [{
+    "platform": "play",
+    "url": "https://play.google.com/store/apps/details?id=my.first.webapp"
+  }]
+}
\ No newline at end of file
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -1,50 +1,58 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
 import android.app.assist.AssistStructure
 import android.graphics.SurfaceTexture
+import android.net.Uri
 import android.os.Build
 import org.mozilla.geckoview.AllowOrDeny
-import org.mozilla.geckoview.GeckoDisplay
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.util.Callbacks
 import org.mozilla.geckoview.test.util.UiThreadUtils
 
 import android.os.Looper
+import android.support.test.InstrumentationRegistry
 import android.support.test.filters.MediumTest
 import android.support.test.filters.SdkSuppress
 import android.support.test.runner.AndroidJUnit4
 import android.text.InputType
 import android.util.SparseArray
 import android.view.Surface
 import android.view.View
 import android.view.ViewStructure
 import android.widget.EditText
 import org.hamcrest.Matchers.*
+import org.json.JSONObject
 import org.junit.Assume.assumeThat
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mozilla.geckoview.test.util.HttpBin
+
+import java.net.URI
 
 import kotlin.concurrent.thread
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class ContentDelegateTest : BaseSessionTest() {
+    companion object {
+        val TEST_ENDPOINT: String = "http://localhost:4243"
+    }
 
     @Test fun titleChange() {
         sessionRule.session.loadTestPath(TITLE_CHANGE_HTML_PATH)
 
         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
             @AssertCalled(count = 2)
             override fun onTitleChange(session: GeckoSession, title: String?) {
                 assertThat("Title should match", title,
@@ -575,9 +583,49 @@ class ContentDelegateTest : BaseSessionT
         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
             @AssertCalled(count = 1)
             override fun onFirstComposite(session: GeckoSession) {
             }
         })
         display.surfaceDestroyed()
         mainSession.releaseDisplay(display)
     }
+
+    @Test fun webAppManifest() {
+        val httpBin = HttpBin(InstrumentationRegistry.getTargetContext(), URI.create(TEST_ENDPOINT))
+
+        try {
+            httpBin.start()
+
+            mainSession.loadUri("$TEST_ENDPOINT$HELLO_HTML_PATH")
+            mainSession.waitUntilCalled(object : Callbacks.All {
+
+                @AssertCalled(count = 1)
+                override fun onPageStop(session: GeckoSession, success: Boolean) {
+                    assertThat("Page load should succeed", success, equalTo(true))
+                }
+
+                @AssertCalled(count = 1)
+                override fun onWebAppManifest(session: GeckoSession, manifest: JSONObject) {
+                    // These values come from the manifest at assets/www/manifest.webmanifest
+                    assertThat("name should match", manifest.getString("name"), equalTo("App"))
+                    assertThat("short_name should match", manifest.getString("short_name"), equalTo("app"))
+                    assertThat("display should match", manifest.getString("display"), equalTo("standalone"))
+
+                    // The color here is "cadetblue" converted to hex.
+                    assertThat("theme_color should match", manifest.getString("theme_color"), equalTo("#5f9ea0"))
+                    assertThat("background_color should match", manifest.getString("background_color"), equalTo("#c0feee"))
+                    assertThat("start_url should match", manifest.getString("start_url"), equalTo("$TEST_ENDPOINT/assets/www/start/index.html"))
+
+                    val icon = manifest.getJSONArray("icons").getJSONObject(0);
+
+                    val iconSrc = Uri.parse(icon.getString("src"))
+                    assertThat("icon should have a valid src", iconSrc, notNullValue())
+                    assertThat("icon src should be absolute", iconSrc.isAbsolute, equalTo(true))
+                    assertThat("icon should have sizes", icon.getString("sizes"),  not(isEmptyOrNullString()))
+                    assertThat("icon type should match", icon.getString("type"), equalTo("image/gif"))
+                }
+            })
+        } finally {
+            httpBin.stop()
+        }
+    }
 }
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview.test;
 
+import org.json.JSONObject;
 import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.GeckoDisplay;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.GeckoView;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
@@ -108,16 +109,20 @@ public class TestRunnerActivity extends 
             if (System.getenv("MOZ_CRASHREPORTER_SHUTDOWN") != null) {
                 sRuntime.shutdown();
             }
         }
 
         @Override
         public void onFirstComposite(final GeckoSession session) {
         }
+
+        @Override
+        public void onWebAppManifest(final GeckoSession session, final JSONObject manifest) {
+        }
     };
 
     private GeckoSession createSession() {
         return createSession(null);
     }
 
     private GeckoSession createSession(GeckoSessionSettings settings) {
         if (settings == null) {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
@@ -13,16 +13,17 @@ import org.mozilla.geckoview.GeckoSessio
 import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement
 import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest
 import org.mozilla.geckoview.MediaElement
 import org.mozilla.geckoview.WebRequestError
 
 import android.view.inputmethod.CursorAnchorInfo
 import android.view.inputmethod.ExtractedText
 import android.view.inputmethod.ExtractedTextRequest
+import org.json.JSONObject
 
 class Callbacks private constructor() {
     object Default : All
 
     interface All : ContentBlockingDelegate, ContentDelegate,
                     HistoryDelegate, MediaDelegate,
                     NavigationDelegate, PermissionDelegate, ProgressDelegate,
                     PromptDelegate, ScrollDelegate, SelectionActionDelegate,
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -6,16 +6,18 @@
 
 package org.mozilla.geckoview;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.util.UUID;
 
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.IGeckoEditableParent;
 import org.mozilla.gecko.mozglue.JNIObject;
 import org.mozilla.gecko.NativeQueue;
 import org.mozilla.gecko.util.BundleEventListener;
@@ -338,16 +340,17 @@ public class GeckoSession implements Par
                 "GeckoView:ContentCrash",
                 "GeckoView:ContextMenu",
                 "GeckoView:DOMTitleChanged",
                 "GeckoView:DOMWindowFocus",
                 "GeckoView:DOMWindowClose",
                 "GeckoView:ExternalResponse",
                 "GeckoView:FullScreenEnter",
                 "GeckoView:FullScreenExit",
+                "GeckoView:WebAppManifest",
             }
         ) {
             @Override
             public void handleMessage(final ContentDelegate delegate,
                                       final String event,
                                       final GeckoBundle message,
                                       final EventCallback callback) {
 
@@ -377,16 +380,27 @@ public class GeckoSession implements Par
                 } else if ("GeckoView:DOMWindowClose".equals(event)) {
                     delegate.onCloseRequest(GeckoSession.this);
                 } else if ("GeckoView:FullScreenEnter".equals(event)) {
                     delegate.onFullScreen(GeckoSession.this, true);
                 } else if ("GeckoView:FullScreenExit".equals(event)) {
                     delegate.onFullScreen(GeckoSession.this, false);
                 } else if ("GeckoView:ExternalResponse".equals(event)) {
                     delegate.onExternalResponse(GeckoSession.this, new WebResponseInfo(message));
+                } else if ("GeckoView:WebAppManifest".equals(event)) {
+                    final GeckoBundle manifest = message.getBundle("manifest");
+                    if (manifest == null) {
+                        return;
+                    }
+
+                    try {
+                        delegate.onWebAppManifest(GeckoSession.this, manifest.toJSONObject());
+                    } catch (JSONException e) {
+                        Log.e(LOGTAG, "Failed to convert web app manifest to JSON", e);
+                    }
                 }
             }
         };
 
     private final GeckoSessionHandler<NavigationDelegate> mNavigationHandler =
         new GeckoSessionHandler<NavigationDelegate>(
             "GeckoViewNavigation", this,
             new String[]{
@@ -2752,16 +2766,26 @@ public class GeckoSession implements Par
         /**
          * Notification that the first content composition has occurred.
          * This callback is invoked for the first content composite after either
          * a start or a restart of the compositor.
          * @param session The GeckoSession that had a first paint event.
          */
         @UiThread
         default void onFirstComposite(@NonNull GeckoSession session) {}
+
+        /**
+         * This is fired when the loaded document has a valid Web App Manifest present.
+         *
+         * @param session The GeckoSession that contains the Web App Manifest
+         * @param manifest A parsed and validated {@link JSONObject} containing the manifest contents.
+         * @see <a href="https://www.w3.org/TR/appmanifest/">Web App Manifest specification</a>
+         */
+        @UiThread
+        default void onWebAppManifest(@NonNull GeckoSession session, @NonNull JSONObject manifest) {}
     }
 
     public interface SelectionActionDelegate {
         @Retention(RetentionPolicy.SOURCE)
         @IntDef(flag = true, value = {FLAG_IS_COLLAPSED,
                                       FLAG_IS_EDITABLE})
         /* package */ @interface Flag {}
 
--- 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
@@ -85,16 +85,21 @@ exclude: true
 [67.20]: ../GeckoDisplay.html
 [67.21]: ../GeckoDisplay.html#capturePixels
 
 - Add missing `@Nullable` annotation to return value for
   `GeckoSession.PromptDelegate.ChoiceCallback.onPopupResult()`
 
 - Added `default` implementations for all non-functional `interface`s.
 
+- Added [`ContentDelegate.onWebAppManifest`][67.22], which will deliver the contents of a parsed
+  and validated Web App Manifest on pages that contain one.
+
+[67.22]: ../GeckoSession.ContentDelegate.html#onWebAppManifest-org.mozilla.geckoview.GeckoSession-org.json.JSONObject
+
 ## v66
 - Removed redundant field `trackingMode` from [`SecurityInformation`][66.6].
   Use `TrackingProtectionDelegate.onTrackerBlocked` for notification of blocked
   elements during page load.
 
 [66.6]: ../GeckoSession.ProgressDelegate.SecurityInformation.html
 
 - Added [`@NonNull`][66.1] or [`@Nullable`][66.2] to all APIs.
@@ -204,9 +209,9 @@ exclude: true
 [65.23]: ../GeckoSession.FinderResult.html
 
 - Update [`CrashReporter#sendCrashReport`][65.24] to return the crash ID as a
   [`GeckoResult<String>`][65.25].
 
 [65.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: ../GeckoResult.html
 
-[api-version]: 09c473360eb5e17aa801fa0f966cd8671cf2f3d2
+[api-version]: 910e94d6c3ec93faf7ba4ef73c4746b86e77b1c4
--- 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
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview_example;
 
+import org.json.JSONObject;
 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;
@@ -528,16 +529,21 @@ public class GeckoViewActivity extends A
             session.open(sGeckoRuntime);
             session.loadUri(DEFAULT_URL);
         }
 
         @Override
         public void onFirstComposite(final GeckoSession session) {
             Log.d(LOGTAG, "onFirstComposite");
         }
+
+        @Override
+        public void onWebAppManifest(final GeckoSession session, JSONObject manifest) {
+            Log.d(LOGTAG, "onWebAppManifest: " + manifest);
+        }
     }
 
     private class ExampleProgressDelegate implements GeckoSession.ProgressDelegate {
         private ExampleContentBlockingDelegate mCb;
 
         private ExampleProgressDelegate(final ExampleContentBlockingDelegate cb) {
             mCb = cb;
         }