Bug 1402369 - Add WebShare support to GeckoView. r=geckoview-reviewers,snorp,marcosc,esawin

Differential Revision: https://phabricator.services.mozilla.com/D49614

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Dylan Roeh 2019-10-30 22:31:04 +00:00
Родитель 1824e6f651
Коммит 195f623a27
11 изменённых файлов: 418 добавлений и 3 удалений

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

@ -1402,7 +1402,7 @@ Promise* Navigator::Share(const ShareData& aData, ErrorResult& aRv) {
// The spec does the "triggered by user activation" after the data checks.
// Unfortunately, both Chrome and Safari behave this way, so interop wins.
// https://github.com/w3c/web-share/pull/118
if (!UserActivation::IsHandlingUserInput()) {
if (StaticPrefs::dom_webshare_requireinteraction() && !UserActivation::IsHandlingUserInput()) {
NS_WARNING("Attempt to share not triggered by user activation");
aRv.Throw(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return nullptr;

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

@ -387,3 +387,5 @@ MathML_DeprecatedStyleAttributeWarning=MathML attributes “background”, “co
MathML_DeprecatedXLinkAttributeWarning=XLink attributes “href”, “type”, “show” and “actuate” are deprecated on MathML elements and will be removed at a future date.
# LOCALIZATION NOTE: Do not translate title, text, url as they are the names of JS properties.
WebShareAPI_NeedOneMember=title or text or url member of the ShareData dictionary. At least one of the members is required.
WebShareAPI_Failed=The share operation has failed.
WebShareAPI_Aborted=The share operation was aborted.

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

@ -38,6 +38,9 @@ pref("geckoview.console.enabled", false);
pref("geckoview.logging", "Debug");
#endif
// Enable WebShare support.
pref("dom.webshare.enabled", true);
// Enable capture attribute for file input.
pref("dom.capture.enabled", true);

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

@ -19,6 +19,8 @@ component {aa0dd6fc-73dd-4621-8385-c0b377e02cee} GeckoViewPrompt.js process=main
contract @mozilla.org/colorpicker;1 {aa0dd6fc-73dd-4621-8385-c0b377e02cee} process=main
component {e4565e36-f101-4bf5-950b-4be0887785a9} GeckoViewPrompt.js process=main
contract @mozilla.org/filepicker;1 {e4565e36-f101-4bf5-950b-4be0887785a9} process=main
component {1201d357-8417-4926-a694-e6408fbedcf8} GeckoViewPrompt.js process=main
contract @mozilla.org/sharepicker;1 {1201d357-8417-4926-a694-e6408fbedcf8} process=main
# GeckoViewExternalAppService.js
component {a89eeec6-6608-42ee-a4f8-04d425992f45} GeckoViewExternalAppService.js

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

@ -20,6 +20,10 @@ XPCOMUtils.defineLazyServiceGetter(
"nsIUUIDGenerator"
);
const domBundle = Services.strings.createBundle(
"chrome://global/locale/dom/dom.properties"
);
function PromptFactory() {
this.wrappedJSObject = this;
}
@ -1127,8 +1131,68 @@ ColorPickerDelegate.prototype = {
},
};
function ShareDelegate() {}
ShareDelegate.prototype = {
classID: Components.ID("{1201d357-8417-4926-a694-e6408fbedcf8}"),
QueryInterface: ChromeUtils.generateQI([Ci.nsISharePicker]),
init: function(aParent) {
this._openerWindow = aParent;
},
get openerWindow() {
return this._openerWindow;
},
async share(aTitle, aText, aUri) {
const ABORT = 2;
const FAILURE = 1;
const SUCCESS = 0;
const msg = {
type: "share",
title: aTitle,
text: aText,
uri: aUri ? aUri.displaySpec : null,
};
const prompt = new PromptDelegate(this._openerWindow);
const result = await new Promise(resolve => {
prompt.asyncShowPrompt(msg, resolve);
});
if (!result) {
// A null result is treated as a dismissal in PromptDelegate.
throw new DOMException(
domBundle.GetStringFromName("WebShareAPI_Aborted"),
"AbortError"
);
}
const res = result && result.response;
switch (res) {
case FAILURE:
throw new DOMException(
domBundle.GetStringFromName("WebShareAPI_Failed"),
"DataError"
);
case ABORT: // Handle aborted attempt and invalid responses the same.
throw new DOMException(
domBundle.GetStringFromName("WebShareAPI_Aborted"),
"AbortError"
);
case SUCCESS:
return;
default:
throw new DOMException("Unknown error.", "UnknownError");
}
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
ColorPickerDelegate,
FilePickerDelegate,
PromptFactory,
ShareDelegate,
]);

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

@ -809,6 +809,7 @@ package org.mozilla.geckoview {
method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onDateTimePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.DateTimePrompt);
method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onFilePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.FilePrompt);
method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onPopupPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.PopupPrompt);
method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onSharePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.SharePrompt);
method @UiThread @Nullable default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onTextPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.TextPrompt);
}
@ -954,6 +955,20 @@ package org.mozilla.geckoview {
public static class GeckoSession.PromptDelegate.PromptResponse {
}
public static class GeckoSession.PromptDelegate.SharePrompt extends GeckoSession.PromptDelegate.BasePrompt {
ctor protected SharePrompt(@Nullable String, @Nullable String, @Nullable String);
method @UiThread @NonNull public GeckoSession.PromptDelegate.PromptResponse confirm(int);
field @Nullable public final String text;
field @Nullable public final String uri;
}
public static class GeckoSession.PromptDelegate.SharePrompt.Result {
ctor protected Result();
field public static final int ABORT = 2;
field public static final int FAILURE = 1;
field public static final int SUCCESS = 0;
}
public static class GeckoSession.PromptDelegate.TextPrompt extends GeckoSession.PromptDelegate.BasePrompt {
ctor protected TextPrompt(@Nullable String, @Nullable String, @Nullable String);
method @UiThread @NonNull public GeckoSession.PromptDelegate.PromptResponse confirm(@NonNull String);

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

@ -5,12 +5,14 @@ import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest
import org.mozilla.geckoview.GeckoSession.PromptDelegate
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
import org.mozilla.geckoview.test.util.Callbacks
import android.support.test.filters.MediumTest
import android.support.test.runner.AndroidJUnit4
import org.hamcrest.Matchers.*
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@ -254,5 +256,216 @@ class PromptDelegateTest : BaseSessionTest() {
}
})
}
}
@Test fun shareTextSucceeds() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
val shareText = "Example share text"
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 1)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
assertThat("Text field is not null", prompt.text, notNullValue())
assertThat("Title field is null", prompt.title, nullValue())
assertThat("Url field is null", prompt.uri, nullValue())
assertThat("Text field contains correct value", prompt.text, equalTo(shareText))
return GeckoResult.fromValue(prompt.confirm(PromptDelegate.SharePrompt.Result.SUCCESS))
}
})
try {
mainSession.waitForJS("""window.navigator.share({text: "${shareText}"})""")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
Assert.fail("Share must succeed." + e.reason as String)
}
}
@Test fun shareUrlSucceeds() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
val shareUrl = "https://example.com/"
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 1)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
assertThat("Text field is null", prompt.text, nullValue())
assertThat("Title field is null", prompt.title, nullValue())
assertThat("Url field is not null", prompt.uri, notNullValue())
assertThat("Text field contains correct value", prompt.uri, equalTo(shareUrl))
return GeckoResult.fromValue(prompt.confirm(PromptDelegate.SharePrompt.Result.SUCCESS))
}
})
try {
mainSession.waitForJS("""window.navigator.share({url: "${shareUrl}"})""")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
Assert.fail("Share must succeed." + e.reason as String)
}
}
@Test fun shareTitleSucceeds() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
val shareTitle = "Title!"
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 1)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
assertThat("Text field is null", prompt.text, nullValue())
assertThat("Title field is not null", prompt.title, notNullValue())
assertThat("Url field is null", prompt.uri, nullValue())
assertThat("Text field contains correct value", prompt.title, equalTo(shareTitle))
return GeckoResult.fromValue(prompt.confirm(PromptDelegate.SharePrompt.Result.SUCCESS))
}
})
try {
mainSession.waitForJS("""window.navigator.share({title: "${shareTitle}"})""")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
Assert.fail("Share must succeed." + e.reason as String)
}
}
@Test fun failedShareReturnsDataError() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
val shareUrl = "https://www.example.com"
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 1)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
return GeckoResult.fromValue(prompt.confirm(PromptDelegate.SharePrompt.Result.FAILURE))
}
})
try {
mainSession.waitForJS("""window.navigator.share({url: "${shareUrl}"})""")
Assert.fail("Request should have failed")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
assertThat("Error should be correct",
e.reason as String, containsString("DataError"))
}
}
@Test fun abortedShareReturnsAbortError() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
val shareUrl = "https://www.example.com"
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 1)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
return GeckoResult.fromValue(prompt.confirm(PromptDelegate.SharePrompt.Result.ABORT))
}
})
try {
mainSession.waitForJS("""window.navigator.share({url: "${shareUrl}"})""")
Assert.fail("Request should have failed")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
assertThat("Error should be correct",
e.reason as String, containsString("AbortError"))
}
}
@Test fun dismissedShareReturnsAbortError() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
val shareUrl = "https://www.example.com"
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 1)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
return GeckoResult.fromValue(prompt.dismiss())
}
})
try {
mainSession.waitForJS("""window.navigator.share({url: "${shareUrl}"})""")
Assert.fail("Request should have failed")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
assertThat("Error should be correct",
e.reason as String, containsString("AbortError"))
}
}
@Test fun emptyShareReturnsTypeError() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 0)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
return GeckoResult.fromValue(prompt.dismiss())
}
})
try {
mainSession.waitForJS("""window.navigator.share({})""")
Assert.fail("Request should have failed")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
assertThat("Error should be correct",
e.reason as String, containsString("TypeError"))
}
}
@Test fun invalidShareUrlReturnsTypeError() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
// Invalid port should cause URL parser to fail.
val shareUrl = "http://www.example.com:123456"
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 0)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
return GeckoResult.fromValue(prompt.dismiss())
}
})
try {
mainSession.waitForJS("""window.navigator.share({url: "${shareUrl}"})""")
Assert.fail("Request should have failed")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
assertThat("Error should be correct",
e.reason as String, containsString("TypeError"))
}
}
@Test fun shareRequiresUserInteraction() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to true))
mainSession.loadTestPath(HELLO_HTML_PATH)
mainSession.waitForPageStop()
val shareUrl = "https://www.example.com"
sessionRule.delegateDuringNextWait(object : Callbacks.PromptDelegate {
@AssertCalled(count = 0)
override fun onSharePrompt(session: GeckoSession, prompt: PromptDelegate.SharePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
return GeckoResult.fromValue(prompt.dismiss())
}
})
try {
mainSession.waitForJS("""window.navigator.share({url: "${shareUrl}"})""")
Assert.fail("Request should have failed")
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
assertThat("Error should be correct",
e.reason as String, containsString("NotAllowedError"))
}
}
}

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

@ -2738,6 +2738,14 @@ public class GeckoSession implements Parcelable {
res = delegate.onPopupPrompt(session, prompt);
break;
}
case "share": {
final String text = message.getString("text");
final String uri = message.getString("uri");
final PromptDelegate.SharePrompt prompt =
new PromptDelegate.SharePrompt(title, text, uri);
res = delegate.onSharePrompt(session, prompt);
break;
}
default: {
callback.sendError("Invalid type");
return;
@ -4413,6 +4421,82 @@ public class GeckoSession implements Parcelable {
}
}
/**
* SharePrompt contains the information necessary to represent a (v1) WebShare request.
*/
public class SharePrompt extends BasePrompt {
@Retention(RetentionPolicy.SOURCE)
@IntDef({Result.SUCCESS, Result.FAILURE, Result.ABORT})
/* package */ @interface ShareResult {}
/**
* Possible results to a {@link SharePrompt}.
*/
public static class Result {
/**
* The user shared with another app successfully.
*/
public static final int SUCCESS = 0;
/**
* The user attempted to share with another app, but it failed.
*/
public static final int FAILURE = 1;
/**
* The user aborted the share.
*/
public static final int ABORT = 2;
protected Result() {}
}
/**
* The text for the share request.
*/
public final @Nullable String text;
/**
* The uri for the share request.
*/
public final @Nullable String uri;
protected SharePrompt(@Nullable final String title,
@Nullable final String text,
@Nullable final String uri) {
super(title);
this.text = text;
this.uri = uri;
}
/**
* Confirms the prompt and either blocks or allows the share request.
*
* @param response One of {@link Result} specifying the outcome of the
* share attempt.
*
* @return A {@link PromptResponse} which can be used to complete the
* {@link GeckoResult} associated with this prompt.
*/
@UiThread
public @NonNull PromptResponse confirm(@ShareResult final int response) {
ensureResult().putInt("response", response);
return super.confirm();
}
/**
* Dismisses the prompt and returns {@link Result#ABORT} to web content.
*
* @return A {@link PromptResponse} which can be used to complete the
* {@link GeckoResult} associated with this prompt.
*/
@UiThread
public @NonNull PromptResponse dismiss() {
ensureResult().putInt("response", Result.ABORT);
return super.dismiss();
}
}
// Delegate functions.
/**
* Display an alert prompt.
@ -4549,6 +4633,23 @@ public class GeckoSession implements Parcelable {
@NonNull final PopupPrompt prompt) {
return null;
}
/**
* Display a share request prompt; this occurs when content attempts to use the
* WebShare API.
* See: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
*
* @param session GeckoSession that triggered the prompt.
* @param prompt The {@link SharePrompt} that describes the prompt.
*
* @return A {@link GeckoResult} resolving to a {@link PromptResponse} which
* includes all necessary information to resolve the prompt.
*/
@UiThread
default @Nullable GeckoResult<PromptResponse> onSharePrompt(@NonNull final GeckoSession session,
@NonNull final SharePrompt prompt) {
return null;
}
}
/**

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

@ -56,6 +56,8 @@ exclude: true
to the session it holds.
- Changed [`AutofillElement.children`][71.20] interface to `Collection` to provide
an efficient way to pre-allocate memory when filling `ViewStructure`.
- Added [`GeckoSession.PromptDelegate.onSharePrompt`][71.22] to support the WebShare API.
([bug 1402369]({{bugzilla}}1402369))
[71.1]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html#onBooleanScalar-org.mozilla.geckoview.RuntimeTelemetry.Metric-
[71.2]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html#onLongScalar-org.mozilla.geckoview.RuntimeTelemetry.Metric-
@ -77,6 +79,7 @@ exclude: true
[71.19]: {{javadoc_uri}}/GeckoSession.html#getAutofillElements--
[71.20]: {{javadoc_uri}}/AutofillElement.html
[71.21]: {{javadoc_uri}}/GeckoView.html#setAutofillEnabled-boolean-
[71.22]: {{javadoc_uri}}/GeckoSession.PromptDelegate.html#onSharePrompt-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.PromptDelegate.SharePrompt-
## v70
- Added API for session context assignment
@ -399,4 +402,4 @@ exclude: true
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: ee3ceb65db78c3a801f525465ff3c6e9eca22ae9
[api-version]: 6a71a9226b15eb40fb47f5da7400915f29fb4986

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

@ -114,6 +114,12 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
return res;
}
@Override
public GeckoResult<PromptResponse> onSharePrompt(final GeckoSession session,
final SharePrompt prompt) {
return GeckoResult.fromValue(prompt.dismiss());
}
private int getViewPadding(final AlertDialog.Builder builder) {
final TypedArray attr = builder.getContext().obtainStyledAttributes(
new int[] { android.R.attr.listPreferredItemPaddingLeft });

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

@ -2726,6 +2726,12 @@
value: false
mirror: always
# WebShare API - allows WebShare without user interaction (for tests only).
- name: dom.webshare.requireinteraction
type: bool
value: true
mirror: always
#---------------------------------------------------------------------------
# Prefs starting with "editor"
#---------------------------------------------------------------------------