diff --git a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java index 5aabe255d754..3b6f3a433f33 100644 --- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java +++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java @@ -31,6 +31,7 @@ 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; diff --git a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java index e3dd0d0cc8fc..be3dd94f7fcf 100644 --- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java +++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java @@ -21,6 +21,7 @@ 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; diff --git a/mobile/android/chrome/geckoview/GeckoViewContentChild.js b/mobile/android/chrome/geckoview/GeckoViewContentChild.js index cc281a77111f..9d8299ef8d2b 100644 --- a/mobile/android/chrome/geckoview/GeckoViewContentChild.js +++ b/mobile/android/chrome/geckoview/GeckoViewContentChild.js @@ -21,6 +21,7 @@ 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", }); @@ -77,6 +78,7 @@ class GeckoViewContentChild extends GeckoViewChildModule { addEventListener("MozDOMFullscreen:Exited", this, false); addEventListener("MozDOMFullscreen:Request", this, false); addEventListener("contextmenu", this, { capture: true }); + addEventListener("DOMContentLoaded", this, false); } onDisable() { @@ -90,6 +92,7 @@ class GeckoViewContentChild extends GeckoViewChildModule { removeEventListener("MozDOMFullscreen:Exited", this); removeEventListener("MozDOMFullscreen:Request", this); removeEventListener("contextmenu", this, { capture: true }); + removeEventListener("DOMContentLoaded", this); } collectSessionState() { @@ -425,6 +428,24 @@ class GeckoViewContentChild extends GeckoViewChildModule { }); } 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, + }); + } + }); + } } } diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt index e7088d9fdb84..5105bb452683 100644 --- a/mobile/android/geckoview/api.txt +++ b/mobile/android/geckoview/api.txt @@ -354,6 +354,7 @@ package org.mozilla.geckoview { 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 { diff --git a/mobile/android/geckoview/src/androidTest/assets/www/hello.html b/mobile/android/geckoview/src/androidTest/assets/www/hello.html index 158edcc0ab2e..b3ddfa60a403 100644 --- a/mobile/android/geckoview/src/androidTest/assets/www/hello.html +++ b/mobile/android/geckoview/src/androidTest/assets/www/hello.html @@ -1,5 +1,8 @@ - Hello, world! + + Hello, world! + +

Hello, world!

diff --git a/mobile/android/geckoview/src/androidTest/assets/www/manifest.webmanifest b/mobile/android/geckoview/src/androidTest/assets/www/manifest.webmanifest index e69de29bb2d1..55be988812fa 100644 --- 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 diff --git 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 index 191f57158cc5..cfc5fcf13d70 100644 --- 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 @@ -6,9 +6,9 @@ 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 @@ -21,6 +21,7 @@ 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 @@ -31,15 +32,22 @@ 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) @@ -580,4 +588,44 @@ class ContentDelegateTest : BaseSessionTest() { 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() + } + } } diff --git 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 index a612d81d786b..590581e78602 100644 --- 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 @@ -5,6 +5,7 @@ package org.mozilla.geckoview.test; +import org.json.JSONObject; import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.GeckoDisplay; import org.mozilla.geckoview.GeckoResult; @@ -113,6 +114,10 @@ public class TestRunnerActivity extends Activity { @Override public void onFirstComposite(final GeckoSession session) { } + + @Override + public void onWebAppManifest(final GeckoSession session, final JSONObject manifest) { + } }; private GeckoSession createSession() { diff --git 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 index 6e1e6261752a..bdffe418bbf4 100644 --- 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 @@ -18,6 +18,7 @@ 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 diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java index 6b70e721039b..b97926dfd0ab 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java @@ -11,6 +11,8 @@ 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; @@ -348,6 +350,7 @@ public class GeckoSession implements Parcelable { "GeckoView:ExternalResponse", "GeckoView:FullScreenEnter", "GeckoView:FullScreenExit", + "GeckoView:WebAppManifest", } ) { @Override @@ -387,6 +390,17 @@ public class GeckoSession implements Parcelable { 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); + } } } }; @@ -2769,6 +2783,16 @@ public class GeckoSession implements Parcelable { */ @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 Web App Manifest specification + */ + @UiThread + default void onWebAppManifest(@NonNull GeckoSession session, @NonNull JSONObject manifest) {} } public interface SelectionActionDelegate { diff --git 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 index d577e2d0b702..7bf43f8f95ae 100644 --- 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 @@ -96,6 +96,11 @@ exclude: true - 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 @@ -215,4 +220,4 @@ exclude: true [65.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String- [65.25]: ../GeckoResult.html -[api-version]: e1330c0e7cfa08420041813f07f24a9389020564 +[api-version]: 07af02921c277f9461d7532f2a6a78c527c9cb47 diff --git 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 index b3e97ab30c1f..f6003e7d16e5 100644 --- 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 @@ -5,6 +5,7 @@ package org.mozilla.geckoview_example; +import org.json.JSONObject; import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.BasicSelectionActionDelegate; import org.mozilla.geckoview.ContentBlocking; @@ -533,6 +534,11 @@ public class GeckoViewActivity extends AppCompatActivity { 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 {