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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
James Willcox 2019-03-20 14:44:22 +00:00
Родитель c54e1232f7
Коммит cdb58ae6cf
12 изменённых файлов: 136 добавлений и 3 удалений

Просмотреть файл

@ -31,6 +31,7 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import org.json.JSONObject;
import org.mozilla.gecko.ActivityHandlerHelper; import org.mozilla.gecko.ActivityHandlerHelper;
import org.mozilla.gecko.BrowserApp; import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.Clipboard; import org.mozilla.gecko.Clipboard;

Просмотреть файл

@ -21,6 +21,7 @@ import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Toast; import android.widget.Toast;
import org.json.JSONObject;
import org.mozilla.gecko.ActivityHandlerHelper; import org.mozilla.gecko.ActivityHandlerHelper;
import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.BrowserApp; import org.mozilla.gecko.BrowserApp;

Просмотреть файл

@ -21,6 +21,7 @@ const SCROLL_BEHAVIOR_AUTO = 1;
XPCOMUtils.defineLazyModuleGetters(this, { XPCOMUtils.defineLazyModuleGetters(this, {
FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm", FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm",
GeckoViewAutoFill: "resource://gre/modules/GeckoViewAutoFill.jsm", GeckoViewAutoFill: "resource://gre/modules/GeckoViewAutoFill.jsm",
ManifestObtainer: "resource://gre/modules/ManifestObtainer.jsm",
PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm", PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm", SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm",
}); });
@ -77,6 +78,7 @@ class GeckoViewContentChild extends GeckoViewChildModule {
addEventListener("MozDOMFullscreen:Exited", this, false); addEventListener("MozDOMFullscreen:Exited", this, false);
addEventListener("MozDOMFullscreen:Request", this, false); addEventListener("MozDOMFullscreen:Request", this, false);
addEventListener("contextmenu", this, { capture: true }); addEventListener("contextmenu", this, { capture: true });
addEventListener("DOMContentLoaded", this, false);
} }
onDisable() { onDisable() {
@ -90,6 +92,7 @@ class GeckoViewContentChild extends GeckoViewChildModule {
removeEventListener("MozDOMFullscreen:Exited", this); removeEventListener("MozDOMFullscreen:Exited", this);
removeEventListener("MozDOMFullscreen:Request", this); removeEventListener("MozDOMFullscreen:Request", this);
removeEventListener("contextmenu", this, { capture: true }); removeEventListener("contextmenu", this, { capture: true });
removeEventListener("DOMContentLoaded", this);
} }
collectSessionState() { collectSessionState() {
@ -425,6 +428,24 @@ class GeckoViewContentChild extends GeckoViewChildModule {
}); });
} }
break; 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,
});
}
});
}
} }
} }

Просмотреть файл

@ -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 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 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 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 { public static class GeckoSession.ContentDelegate.ContextElement {

Просмотреть файл

@ -1,5 +1,8 @@
<html> <html>
<head><title>Hello, world!</title></head> <head>
<title>Hello, world!</title>
<link rel="manifest" href="manifest.webmanifest">
</head>
<body> <body>
<p>Hello, world!</p> <p>Hello, world!</p>
</body> </body>

Просмотреть файл

@ -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"
}]
}

Просмотреть файл

@ -6,9 +6,9 @@ package org.mozilla.geckoview.test
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.graphics.SurfaceTexture import android.graphics.SurfaceTexture
import android.net.Uri
import android.os.Build import android.os.Build
import org.mozilla.geckoview.AllowOrDeny import org.mozilla.geckoview.AllowOrDeny
import org.mozilla.geckoview.GeckoDisplay
import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest 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 org.mozilla.geckoview.test.util.UiThreadUtils
import android.os.Looper import android.os.Looper
import android.support.test.InstrumentationRegistry
import android.support.test.filters.MediumTest import android.support.test.filters.MediumTest
import android.support.test.filters.SdkSuppress import android.support.test.filters.SdkSuppress
import android.support.test.runner.AndroidJUnit4 import android.support.test.runner.AndroidJUnit4
@ -31,15 +32,22 @@ import android.view.View
import android.view.ViewStructure import android.view.ViewStructure
import android.widget.EditText import android.widget.EditText
import org.hamcrest.Matchers.* import org.hamcrest.Matchers.*
import org.json.JSONObject
import org.junit.Assume.assumeThat import org.junit.Assume.assumeThat
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mozilla.geckoview.test.util.HttpBin
import java.net.URI
import kotlin.concurrent.thread import kotlin.concurrent.thread
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest
class ContentDelegateTest : BaseSessionTest() { class ContentDelegateTest : BaseSessionTest() {
companion object {
val TEST_ENDPOINT: String = "http://localhost:4243"
}
@Test fun titleChange() { @Test fun titleChange() {
sessionRule.session.loadTestPath(TITLE_CHANGE_HTML_PATH) sessionRule.session.loadTestPath(TITLE_CHANGE_HTML_PATH)
@ -580,4 +588,44 @@ class ContentDelegateTest : BaseSessionTest() {
display.surfaceDestroyed() display.surfaceDestroyed()
mainSession.releaseDisplay(display) 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()
}
}
} }

Просмотреть файл

@ -5,6 +5,7 @@
package org.mozilla.geckoview.test; package org.mozilla.geckoview.test;
import org.json.JSONObject;
import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.AllowOrDeny;
import org.mozilla.geckoview.GeckoDisplay; import org.mozilla.geckoview.GeckoDisplay;
import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoResult;
@ -113,6 +114,10 @@ public class TestRunnerActivity extends Activity {
@Override @Override
public void onFirstComposite(final GeckoSession session) { public void onFirstComposite(final GeckoSession session) {
} }
@Override
public void onWebAppManifest(final GeckoSession session, final JSONObject manifest) {
}
}; };
private GeckoSession createSession() { private GeckoSession createSession() {

Просмотреть файл

@ -18,6 +18,7 @@ import org.mozilla.geckoview.WebRequestError
import android.view.inputmethod.CursorAnchorInfo import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.ExtractedText import android.view.inputmethod.ExtractedText
import android.view.inputmethod.ExtractedTextRequest import android.view.inputmethod.ExtractedTextRequest
import org.json.JSONObject
class Callbacks private constructor() { class Callbacks private constructor() {
object Default : All object Default : All

Просмотреть файл

@ -11,6 +11,8 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.UUID; import java.util.UUID;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.WrapForJNI; import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoAppShell;
@ -348,6 +350,7 @@ public class GeckoSession implements Parcelable {
"GeckoView:ExternalResponse", "GeckoView:ExternalResponse",
"GeckoView:FullScreenEnter", "GeckoView:FullScreenEnter",
"GeckoView:FullScreenExit", "GeckoView:FullScreenExit",
"GeckoView:WebAppManifest",
} }
) { ) {
@Override @Override
@ -387,6 +390,17 @@ public class GeckoSession implements Parcelable {
delegate.onFullScreen(GeckoSession.this, false); delegate.onFullScreen(GeckoSession.this, false);
} else if ("GeckoView:ExternalResponse".equals(event)) { } else if ("GeckoView:ExternalResponse".equals(event)) {
delegate.onExternalResponse(GeckoSession.this, new WebResponseInfo(message)); 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 @UiThread
default void onFirstComposite(@NonNull GeckoSession session) {} 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 { public interface SelectionActionDelegate {

Просмотреть файл

@ -96,6 +96,11 @@ exclude: true
- Added `default` implementations for all non-functional `interface`s. - 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 ## v66
- Removed redundant field `trackingMode` from [`SecurityInformation`][66.6]. - Removed redundant field `trackingMode` from [`SecurityInformation`][66.6].
Use `TrackingProtectionDelegate.onTrackerBlocked` for notification of blocked 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.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
[65.25]: ../GeckoResult.html [65.25]: ../GeckoResult.html
[api-version]: e1330c0e7cfa08420041813f07f24a9389020564 [api-version]: 07af02921c277f9461d7532f2a6a78c527c9cb47

Просмотреть файл

@ -5,6 +5,7 @@
package org.mozilla.geckoview_example; package org.mozilla.geckoview_example;
import org.json.JSONObject;
import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.AllowOrDeny;
import org.mozilla.geckoview.BasicSelectionActionDelegate; import org.mozilla.geckoview.BasicSelectionActionDelegate;
import org.mozilla.geckoview.ContentBlocking; import org.mozilla.geckoview.ContentBlocking;
@ -533,6 +534,11 @@ public class GeckoViewActivity extends AppCompatActivity {
public void onFirstComposite(final GeckoSession session) { public void onFirstComposite(final GeckoSession session) {
Log.d(LOGTAG, "onFirstComposite"); 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 class ExampleProgressDelegate implements GeckoSession.ProgressDelegate {