Bug 1656336: Implement Web Extension downloads.download() - GV API r=geckoview-reviewers,robwu,agi

Differential Revision: https://phabricator.services.mozilla.com/D99573
This commit is contained in:
owlishDeveloper 2020-12-23 01:56:37 +00:00
parent a5467fbd19
commit df49be0905
12 changed files with 511 additions and 46 deletions

View File

@ -1636,9 +1636,11 @@ package org.mozilla.geckoview {
public class WebExtension {
method @Nullable @UiThread public WebExtension.BrowsingDataDelegate getBrowsingDataDelegate();
method @Nullable @UiThread public WebExtension.DownloadDelegate getDownloadDelegate();
method @Nullable @UiThread public WebExtension.TabDelegate getTabDelegate();
method @AnyThread public void setActionDelegate(@Nullable WebExtension.ActionDelegate);
method @UiThread public void setBrowsingDataDelegate(@Nullable WebExtension.BrowsingDataDelegate);
method @UiThread public void setDownloadDelegate(@Nullable WebExtension.DownloadDelegate);
method @UiThread public void setMessageDelegate(@Nullable WebExtension.MessageDelegate, @NonNull String);
method @UiThread public void setTabDelegate(@Nullable WebExtension.TabDelegate);
field public final long flags;
@ -1721,6 +1723,28 @@ package org.mozilla.geckoview {
field public static final int USER = 2;
}
public static class WebExtension.Download {
ctor protected Download(int);
field @NonNull public final int id;
}
public static interface WebExtension.DownloadDelegate {
method @AnyThread @Nullable default public GeckoResult<WebExtension.Download> onDownload(@NonNull WebExtension, @NonNull WebExtension.DownloadRequest);
}
public static class WebExtension.DownloadRequest {
ctor protected DownloadRequest(WebExtension.DownloadRequest.Builder);
field public static final int CONFLICT_ACTION_OVERWRITE = 1;
field public static final int CONFLICT_ACTION_PROMPT = 2;
field public static final int CONFLICT_ACTION_UNIQUIFY = 0;
field public final boolean allowHttpErrors;
field public final int conflictActionFlag;
field public final int downloadFlags;
field @Nullable public final String filename;
field @NonNull public final WebRequest request;
field public final boolean saveAs;
}
public static class WebExtension.Flags {
ctor protected Flags();
field public static final long ALLOW_CONTENT_MESSAGING = 1L;
@ -1838,6 +1862,7 @@ package org.mozilla.geckoview {
}
public class WebExtensionController {
method @Nullable @UiThread public WebExtension.Download createDownload(int);
method @AnyThread @NonNull public GeckoResult<WebExtension> disable(@NonNull WebExtension, int);
method @AnyThread @NonNull public GeckoResult<WebExtension> enable(@NonNull WebExtension, int);
method @AnyThread @NonNull public GeckoResult<WebExtension> ensureBuiltIn(@NonNull String, @Nullable String);
@ -1938,6 +1963,7 @@ package org.mozilla.geckoview {
@AnyThread public static class WebRequest.Builder extends WebMessage.Builder {
ctor public Builder(@NonNull String);
method @NonNull public WebRequest.Builder body(@Nullable ByteBuffer);
method @NonNull public WebRequest.Builder body(@Nullable String);
method @NonNull public WebRequest build();
method @NonNull public WebRequest.Builder cacheMode(int);
method @NonNull public WebRequest.Builder method(@NonNull String);

View File

@ -46,6 +46,14 @@ addons = {
"page.html",
"manifest.json",
],
"download-flags-true": [
"download.js",
"manifest.json",
],
"download-flags-false": [
"download.js",
"manifest.json",
],
}
for addon, files in addons.items():

View File

@ -0,0 +1,3 @@
browser.downloads.download({
url: "http://localhost:4245/assets/www/images/test.gif",
});

View File

@ -0,0 +1,17 @@
{
"manifest_version": 2,
"name": "Download",
"version": "1.0",
"applications": {
"gecko": {
"id": "download-flags-false@tests.mozilla.org"
}
},
"description": "Downloads a file",
"background": {
"scripts": ["download.js"]
},
"permissions": [
"downloads"
]
}

View File

@ -0,0 +1,16 @@
browser.downloads.download({
url: "http://localhost:4245/assets/www/images/test.gif",
filename: "banana.gif",
method: "POST",
body: "postbody",
headers: [
{
name: "User-Agent",
value: "Mozilla Firefox",
},
],
allowHttpErrors: true,
conflictAction: "overwrite",
saveAs: true,
incognito: true,
});

View File

@ -0,0 +1,17 @@
{
"manifest_version": 2,
"name": "Download",
"version": "1.0",
"applications": {
"gecko": {
"id": "download-flags-true@tests.mozilla.org"
}
},
"description": "Downloads a file",
"background": {
"scripts": ["download.js"]
},
"permissions": [
"downloads"
]
}

View File

@ -32,7 +32,6 @@ import java.lang.IllegalStateException
import java.math.BigInteger
import java.net.UnknownHostException
import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.nio.charset.Charset
import java.security.MessageDigest
import java.util.*
@ -77,14 +76,6 @@ class WebExecutorTest {
return executor.fetch(request, flags).pollDefault()!!
}
fun String.toDirectByteBuffer(): ByteBuffer {
val chars = CharBuffer.wrap(this)
val buffer = ByteBuffer.allocateDirect(this.length)
Charset.forName("UTF-8").newEncoder().encode(chars, buffer, true)
return buffer
}
fun WebResponse.getBodyBytes(): ByteBuffer {
body!!.use {
return ByteBuffer.wrap(it.readBytes())
@ -123,7 +114,7 @@ class WebExecutorTest {
.addHeader("Header2", "Value2")
.referrer(referrer)
.header("Content-Type", "text/plain")
.body(bodyString.toDirectByteBuffer())
.body(bodyString)
.build()
val response = fetch(request)

View File

@ -4,12 +4,10 @@
package org.mozilla.geckoview.test
import androidx.test.filters.MediumTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import java.util.concurrent.CancellationException;
import org.hamcrest.core.StringEndsWith.endsWith
import androidx.test.filters.MediumTest
import org.hamcrest.core.IsEqual.equalTo
import org.hamcrest.core.IsNull.nullValue
import org.hamcrest.core.StringEndsWith.endsWith
import org.json.JSONObject
import org.junit.Assert.*
import org.junit.Assume.assumeThat
@ -17,14 +15,18 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.*
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
import org.mozilla.geckoview.test.util.Callbacks
import org.mozilla.geckoview.WebExtension.DisabledFlags
import org.mozilla.geckoview.WebExtensionController.EnableSource
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.*
import org.mozilla.geckoview.test.util.RuntimeCreator
import org.mozilla.geckoview.WebExtension.*
import org.mozilla.geckoview.WebExtension.BrowsingDataDelegate.Type.*
import org.mozilla.geckoview.WebExtensionController.EnableSource
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.Setting
import org.mozilla.geckoview.test.util.Callbacks
import org.mozilla.geckoview.test.util.RuntimeCreator
import org.mozilla.geckoview.test.util.UiThreadUtils
import java.nio.charset.Charset
import java.util.*
import java.util.concurrent.CancellationException
import kotlin.collections.HashMap
@RunWith(AndroidJUnit4::class)
@ -2132,4 +2134,124 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.waitForResult(controller.uninstall(optionsExtension))
}
// This test checks if the request from Web Extension is processed correctly in Java
// the Boolean flags are true, other options have non-default values
@Test
fun testDownloadsFlagsTrue() {
val uri = createTestUrl("/assets/www/images/test.gif")
sessionRule.setPrefsUntilTestEnd(mapOf(
"xpinstall.signatures.required" to false,
"extensions.install.requireBuiltInCerts" to false,
"extensions.update.requireBuiltInCerts" to false
))
mainSession.loadUri("example.com")
sessionRule.waitForPageStop()
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
}
})
val webExtension = sessionRule.waitForResult(
controller.install("https://example.org/tests/junit/download-flags-true.xpi"))
val assertOnDownloadCalled = GeckoResult<WebExtension.Download>()
val downloadDelegate = object : DownloadDelegate {
override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult<WebExtension.Download>? {
assertEquals(webExtension!!.id, source.id)
assertEquals(uri, request.request.uri)
assertEquals("POST", request.request.method)
request.request.body?.rewind()
val result = Charset.forName("UTF-8").decode(request.request.body!!).toString()
assertEquals("postbody", result)
assertEquals("Mozilla Firefox", request.request.headers.get("User-Agent"))
assertEquals("banana.gif", request.filename)
assertTrue(request.allowHttpErrors)
assertTrue(request.saveAs)
assertEquals(GeckoWebExecutor.FETCH_FLAGS_PRIVATE, request.downloadFlags)
assertEquals(DownloadRequest.CONFLICT_ACTION_OVERWRITE, request.conflictActionFlag)
val download = controller.createDownload(1)
assertOnDownloadCalled.complete(download)
return GeckoResult.fromValue(download)
}
}
webExtension.setDownloadDelegate(downloadDelegate)
mainSession.reload()
sessionRule.waitForPageStop()
try {
sessionRule.waitForResult(assertOnDownloadCalled)
} catch (exception: UiThreadUtils.TimeoutException) {
controller.setAllowedInPrivateBrowsing(webExtension, true)
val downloadCreated = sessionRule.waitForResult(assertOnDownloadCalled)
assertNotNull(downloadCreated.id)
sessionRule.waitForResult(controller.uninstall(webExtension))
}
}
// This test checks if the request from Web Extension is processed correctly in Java
// the Boolean flags are absent/false, other options have default values
@Test
fun testDownloadsFlagsFalse() {
val uri = createTestUrl("/assets/www/images/test.gif")
sessionRule.setPrefsUntilTestEnd(mapOf(
"xpinstall.signatures.required" to false,
"extensions.install.requireBuiltInCerts" to false,
"extensions.update.requireBuiltInCerts" to false
))
mainSession.loadUri("example.com")
sessionRule.waitForPageStop()
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
}
})
val webExtension = sessionRule.waitForResult(
controller.install("https://example.org/tests/junit/download-flags-false.xpi"))
val assertOnDownloadCalled = GeckoResult<WebExtension.Download>()
val downloadDelegate = object : DownloadDelegate {
override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult<WebExtension.Download>? {
assertEquals(webExtension!!.id, source.id)
assertEquals(uri, request.request.uri)
assertEquals("GET", request.request.method)
assertNull(request.request.body)
assertEquals(0, request.request.headers.size)
assertNull(request.filename)
assertFalse(request.allowHttpErrors)
assertFalse(request.saveAs)
assertEquals(GeckoWebExecutor.FETCH_FLAGS_NONE, request.downloadFlags)
assertEquals(DownloadRequest.CONFLICT_ACTION_UNIQUIFY, request.conflictActionFlag)
val download = controller.createDownload(2)
assertOnDownloadCalled.complete(download)
return GeckoResult.fromValue(download)
}
}
webExtension.setDownloadDelegate(downloadDelegate)
mainSession.reload()
sessionRule.waitForPageStop()
val downloadCreated = sessionRule.waitForResult(assertOnDownloadCalled)
assertNotNull(downloadCreated.id)
sessionRule.waitForResult(controller.uninstall(webExtension))
}
}

View File

@ -65,9 +65,11 @@ public class WebExtension {
void onActionDelegate(final ActionDelegate delegate);
void onBrowsingDataDelegate(final BrowsingDataDelegate delegate);
void onTabDelegate(final TabDelegate delegate);
void onDownloadDelegate(final DownloadDelegate delegate);
ActionDelegate getActionDelegate();
BrowsingDataDelegate getBrowsingDataDelegate();
TabDelegate getTabDelegate();
DownloadDelegate getDownloadDelegate();
}
private DelegateController mDelegateController = null;
@ -988,6 +990,7 @@ public class WebExtension {
final private HashMap<String, ActionDelegate> mActionDelegates;
final private HashMap<String, BrowsingDataDelegate> mBrowsingDataDelegates;
final private HashMap<String, TabDelegate> mTabDelegates;
final private HashMap<String, DownloadDelegate> mDownloadDelegates;
final private GeckoSession mSession;
final private EventDispatcher mEventDispatcher;
@ -1022,6 +1025,7 @@ public class WebExtension {
mActionDelegates = new HashMap<>();
mBrowsingDataDelegates = new HashMap<>();
mTabDelegates = new HashMap<>();
mDownloadDelegates = new HashMap<>();
mEventDispatcher = session != null
? session.getEventDispatcher()
: EventDispatcher.getInstance();
@ -1036,7 +1040,8 @@ public class WebExtension {
"GeckoView:WebExtension:Connect",
"GeckoView:WebExtension:Disconnect",
"GeckoView:BrowsingData:GetSettings",
"GeckoView:BrowsingData:Clear");
"GeckoView:BrowsingData:Clear",
"GeckoView:WebExtension:Download");
}
public void unregisterWebExtension(final WebExtension extension) {
@ -1044,6 +1049,7 @@ public class WebExtension {
mActionDelegates.remove(extension.id);
mBrowsingDataDelegates.remove(extension.id);
mTabDelegates.remove(extension.id);
mDownloadDelegates.remove(extension.id);
}
public void setTabDelegate(final WebExtension webExtension,
@ -1118,6 +1124,15 @@ public class WebExtension {
runtime.getWebExtensionController().handleMessage(event, message, callback, mSession);
}
public void setDownloadDelegate(final @NonNull WebExtension extension,
final @Nullable DownloadDelegate delegate) {
mDownloadDelegates.put(extension.id, delegate);
}
public WebExtension.DownloadDelegate getDownloadDelegate(final WebExtension extension) {
return mDownloadDelegates.get(extension.id);
}
}
/**
@ -2194,23 +2209,68 @@ public class WebExtension {
}
}
// TODO: implement bug 1538348
/* package */ interface DownloadDelegate {
public interface DownloadDelegate {
/**
* Method that is called when Web Extension requests a download
* (when downloads.download() is called in Web Extension)
*
* @param source - Web Extension that requested the download
* @param request - contains the {@link WebRequest} and additional parameters for the request
* @return {@link Download} instance
*/
@AnyThread
default GeckoResult<WebExtension.Download> onDownload(WebExtension source, DownloadRequest request) {
@Nullable
default GeckoResult<WebExtension.Download> onDownload(@NonNull WebExtension source, @NonNull DownloadRequest request) {
return null;
}
}
// TODO: make public bug 1538348
/**
* Represents a download
* Set the download delegate for this extension. This delegate will be invoked whenever
* this extension tries to use the `downloads` WebExtension API.
*
* See also
* <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads">WebExtensions/API/downloads</a>.
*
* @param delegate the {@link DownloadDelegate} instance for this extension.
*/
@UiThread
public void setDownloadDelegate(final @Nullable DownloadDelegate delegate) {
if (mDelegateController != null) {
mDelegateController.onDownloadDelegate(delegate);
}
}
/**
* Get the download delegate for this extension.
*
* See also
* <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads">WebExtensions downloads API</a>.
*
* @return The {@link DownloadDelegate} instance for this extension.
*/
@UiThread
@Nullable
public DownloadDelegate getDownloadDelegate() {
return mDelegateController.getDownloadDelegate();
}
/**
* Represents a download for <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads">downloads API</a>
* Instantiate using {@link WebExtensionController#createDownload}
*/
static class Download {
/* package */ final String id;
public static class Download {
/**
* Represents a unique identifier for the downloaded item
* that is persistent across browser sessions
*/
public final @NonNull int id;
private Download(final String id) {
/**
* For testing.
* @param id - integer id for the download item
*/
protected Download(final int id) {
this.id = id;
}
@ -2335,46 +2395,148 @@ public class WebExtension {
}
}
// TODO: make public bug 1538348
/**
* Represents Web Extension API specific download request
*/
static final class DownloadRequest {
/* package */ final WebRequest request;
/* package */ final @GeckoWebExecutor.FetchFlags int downloadFlags;
/* package */ final String filename;
/* package */ final @ConflictActionFlags int conflictActionFlag;
public static class DownloadRequest {
/**
* Regular GeckoView {@link WebRequest} object
*/
public final @NonNull WebRequest request;
@IntDef(flag = true, value = { UNIQUIFY, OVERWRITE, PROMPT })
/**
* Optional fetch flags for {@link GeckoWebExecutor}
*/
public final @GeckoWebExecutor.FetchFlags int downloadFlags;
/**
* A file path relative to the default downloads directory
*/
public final @Nullable String filename;
/**
* The action you want taken if there is a filename conflict, as defined
* <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/FilenameConflictAction">here</a>
*/
public final @ConflictActionFlags int conflictActionFlag;
/**
* Specifies whether to provide a file chooser dialog to allow
* the user to select a filename (true), or not (false)
*/
public final boolean saveAs;
/**
* Flag that enables downloads to continue even if they encounter HTTP errors.
* When false, the download is canceled when it encounters an HTTP error.
* When true, the download continues when an HTTP error is encountered and
* the HTTP server error is not reported. However, if the download fails due to
* file-related, network-related, user-related, or other error, that error is reported.
*/
public final boolean allowHttpErrors;
@IntDef(flag = true, value = {CONFLICT_ACTION_UNIQUIFY, CONFLICT_ACTION_OVERWRITE, CONFLICT_ACTION_PROMPT})
/* package */ @interface ConflictActionFlags {}
/**
* The app should modify the filename to make it unique
*/
/* package */ static final int UNIQUIFY = 0;
public static final int CONFLICT_ACTION_UNIQUIFY = 0;
/**
* The app should overwrite the old file with the newly-downloaded file
*/
/* package */ static final int OVERWRITE = 1;
public static final int CONFLICT_ACTION_OVERWRITE = 1;
/**
* The app should prompt the user, asking them to choose whether to uniquify or overwrite
*/
/* package */ static final int PROMPT = 1 << 1;
public static final int CONFLICT_ACTION_PROMPT = 1 << 1;
private DownloadRequest(final DownloadRequest.Builder builder) {
protected DownloadRequest(final DownloadRequest.Builder builder) {
this.request = builder.mRequest;
this.downloadFlags = builder.mDownloadFlags;
this.filename = builder.mFilename;
this.conflictActionFlag = builder.mConflictActionFlag;
this.saveAs = builder.mSaveAs;
this.allowHttpErrors = builder.mAllowHttpErrors;
}
/* package */ class Builder {
/**
* Convenience method to convert a GeckoBundle to a DownloadRequest.
*
* @param optionsBundle - in the shape of the options object browser.downloads.download() accepts
* @return request - a DownloadRequest instance
*/
/* package */ static DownloadRequest fromBundle(final GeckoBundle optionsBundle) {
final String uri = optionsBundle.getString("url");
WebRequest.Builder mainRequestBuilder = new WebRequest.Builder(uri);
String method = optionsBundle.getString("method");
if (method != null) {
mainRequestBuilder.method(method);
if (method.equals("POST")) {
String body = optionsBundle.getString("body");
mainRequestBuilder.body(body);
}
}
GeckoBundle[] headers = optionsBundle.getBundleArray("headers");
if (headers != null) {
for (GeckoBundle header : headers) {
String value = header.getString("value");
if (value == null) {
value = header.getString("binaryValue");
}
mainRequestBuilder.addHeader(header.getString("name"), value);
}
}
WebRequest mainRequest = mainRequestBuilder.build();
int downloadFlags = GeckoWebExecutor.FETCH_FLAGS_NONE;
boolean incognito = optionsBundle.getBoolean("incognito");
if (incognito) {
downloadFlags |= GeckoWebExecutor.FETCH_FLAGS_PRIVATE;
}
boolean allowHttpErrors = optionsBundle.getBoolean("allowHttpErrors");
int conflictActionFlags = CONFLICT_ACTION_UNIQUIFY;
String conflictActionString = optionsBundle.getString("conflictAction");
if (conflictActionString != null) {
switch (conflictActionString.toLowerCase()) {
case "overwrite":
conflictActionFlags |= WebExtension.DownloadRequest.CONFLICT_ACTION_OVERWRITE;
break;
case "prompt":
conflictActionFlags |= WebExtension.DownloadRequest.CONFLICT_ACTION_PROMPT;
break;
}
}
boolean saveAs = optionsBundle.getBoolean("saveAs");
WebExtension.DownloadRequest request = new WebExtension.DownloadRequest.Builder(mainRequest)
.filename(optionsBundle.getString("filename"))
.downloadFlags(downloadFlags)
.conflictAction(conflictActionFlags)
.saveAs(saveAs)
.allowHttpErrors(allowHttpErrors)
.build();
return request;
}
/* package */ static class Builder {
private final WebRequest mRequest;
private @GeckoWebExecutor.FetchFlags int mDownloadFlags = 0;
private String mFilename = null;
private @ConflictActionFlags int mConflictActionFlag = UNIQUIFY;
private @ConflictActionFlags int mConflictActionFlag = CONFLICT_ACTION_UNIQUIFY;
private boolean mSaveAs = false;
private boolean mAllowHttpErrors = false;
/* package */ Builder(final WebRequest request) {
this.mRequest = request;
@ -2395,6 +2557,16 @@ public class WebExtension {
return this;
}
/* package */ Builder saveAs(final boolean saveAs) {
this.mSaveAs = saveAs;
return this;
}
/* package */ Builder allowHttpErrors(final boolean allowHttpErrors) {
this.mAllowHttpErrors = allowHttpErrors;
return this;
}
/* package */ DownloadRequest build() {
return new DownloadRequest(this);
}

View File

@ -40,6 +40,9 @@ public class WebExtensionController {
private final MultiMap<MessageRecipient, Message> mPendingMessages;
private final MultiMap<String, Message> mPendingNewTab;
private final MultiMap<String, Message> mPendingBrowsingData;
private final MultiMap<String, Message> mPendingDownload;
private final HashMap<Integer, WebExtension.Download> mDownloads;
private static class Message {
final GeckoBundle bundle;
@ -203,6 +206,23 @@ public class WebExtensionController {
public WebExtension.TabDelegate getTabDelegate() {
return mListener.getTabDelegate(mExtension);
}
@Override
public void onDownloadDelegate(final WebExtension.DownloadDelegate delegate) {
mListener.setDownloadDelegate(mExtension, delegate);
for (final Message message : mPendingDownload.get(mExtension.id)) {
WebExtensionController.this.handleMessage(message.event, message.bundle,
message.callback, message.session);
}
mPendingDownload.remove(mExtension.id);
}
@Override
public WebExtension.DownloadDelegate getDownloadDelegate() {
return mListener.getDownloadDelegate(mExtension);
}
}
/**
@ -789,7 +809,9 @@ public class WebExtensionController {
mPendingMessages = new MultiMap<>();
mPendingNewTab = new MultiMap<>();
mPendingBrowsingData = new MultiMap<>();
mPendingDownload = new MultiMap<>();
mExtensions.setObserver(mInternals);
mDownloads = new HashMap<>();
}
/* package */ void registerWebExtension(final WebExtension webExtension) {
@ -855,6 +877,9 @@ public class WebExtensionController {
} else if ("GeckoView:BrowsingData:Clear".equals(event)) {
browsingDataClear(message, extension);
return;
} else if ("GeckoView:WebExtension:Download".equals(event)) {
download(message, extension);
return;
}
final String nativeApp = bundle.getString("nativeApp");
if (nativeApp == null) {
@ -1008,6 +1033,38 @@ public class WebExtensionController {
}
/* package */ void download(final Message message, final WebExtension extension) {
final WebExtension.DownloadDelegate delegate = mListener.getDownloadDelegate(extension);
if (delegate == null) {
mPendingDownload.add(extension.id, message);
return;
}
final GeckoBundle optionsBundle = message.bundle.getBundle("options");
WebExtension.DownloadRequest request = WebExtension.DownloadRequest.fromBundle(optionsBundle);
GeckoResult<WebExtension.Download> result = delegate.onDownload(extension, request);
if (result == null) {
message.callback.sendError("downloads.download() is not supported");
return;
}
result.then(
value -> {
if (value != null) {
message.callback.sendSuccess(value.id);
} else {
message.callback.sendError("downloads.download is not supported");
Log.e(LOGTAG, "onDownload returned invalid null id");
}
return GeckoResult.fromValue(value);
},
error -> {
message.callback.sendError(error.getCause().getMessage());
return GeckoResult.fromException(error);
});
}
/* package */ void openOptionsPage(
final Message message,
final WebExtension extension) {
@ -1347,8 +1404,16 @@ public class WebExtensionController {
return null;
}
// TODO: implement bug 1538348
/* package */ WebExtension.Download createDownload(final String id) {
return null;
@Nullable
@UiThread
public WebExtension.Download createDownload(final int id) {
if (mDownloads.containsKey(id)) {
throw new IllegalArgumentException("Download with this id already exists");
} else {
WebExtension.Download download = new WebExtension.Download(id);
mDownloads.put(id, download);
return download;
}
}
}

View File

@ -17,6 +17,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
/**
* WebRequest represents an HTTP[S] request. The typical pattern is to create instances of this
@ -169,6 +171,25 @@ public class WebRequest extends WebMessage {
return this;
}
/**
* Set the body.
*
* @param bodyString A {@link String} with the data.
* @return This Builder instance.
*/
public @NonNull Builder body(final @Nullable String bodyString) {
if (bodyString == null) {
mBody = null;
return this;
}
CharBuffer chars = CharBuffer.wrap(bodyString);
ByteBuffer buffer = ByteBuffer.allocateDirect(bodyString.length());
Charset.forName("UTF-8").newEncoder().encode(chars, buffer, true);
mBody = buffer;
return this;
}
/**
* Set the HTTP method.
*

View File

@ -17,6 +17,13 @@ exclude: true
- Removed deprecated [`ContentDelegate#onExternalResponse(GeckoSession, WebResponseInfo)`].
Use [`ContentDelegate#onExternalResponse(GeckoSession, WebResponse)`][82.2] instead.
([bug 1665157]({{bugzilla}}1665157))
- Added [`WebExtension.DownloadDelegate`][86.1] and that can be used to
implement the WebExtension `downloads` API.
([bug 1656336]({{bugzilla}}1656336))
- Added [`WebRequest.Builder#body(@Nullable String)`][86.2] which converts a string to direct byte buffer.
[86.1]: {{javadoc_uri}}/WebExtension.DownloadDelegate.html
[86.2]: {{javadoc_uri}}/WebRequest.Builder#body-java.lang.String-
## v85
- Added [`WebExtension.BrowsingDataDelegate`][85.1] that can be used to
@ -860,4 +867,4 @@ to allow adding gecko profiler markers.
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: 71df230eba3e3e1bda47ea54767d81aaf592b115
[api-version]: 604cbae58ecf5cd250c1ee9e75dadf626d601817