mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 08:12:05 +00:00
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:
parent
a5467fbd19
commit
df49be0905
@ -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);
|
||||
|
@ -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():
|
||||
|
@ -0,0 +1,3 @@
|
||||
browser.downloads.download({
|
||||
url: "http://localhost:4245/assets/www/images/test.gif",
|
||||
});
|
@ -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"
|
||||
]
|
||||
}
|
@ -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,
|
||||
});
|
@ -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"
|
||||
]
|
||||
}
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user