Bug 1612097 - Add ability to cancel the GeckoResult returned by WebExtensionControll.install(BuiltIn); r=snorp,agi

Make the GeckoResult<WebExtension> returned by WebExtensionControll.install(BuiltIn) cancellable

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Thomas Wisniewski 2020-03-12 19:03:46 +00:00
parent 25b913bc5c
commit c5824ee72d
5 changed files with 164 additions and 19 deletions

View File

@ -88,6 +88,7 @@ GeckoViewStartup.prototype = {
"GeckoView:WebExtension:Get", "GeckoView:WebExtension:Get",
"GeckoView:WebExtension:Disable", "GeckoView:WebExtension:Disable",
"GeckoView:WebExtension:Enable", "GeckoView:WebExtension:Enable",
"GeckoView:WebExtension:CancelInstall",
"GeckoView:WebExtension:Install", "GeckoView:WebExtension:Install",
"GeckoView:WebExtension:InstallBuiltIn", "GeckoView:WebExtension:InstallBuiltIn",
"GeckoView:WebExtension:List", "GeckoView:WebExtension:List",

View File

@ -6,6 +6,7 @@ package org.mozilla.geckoview.test
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import java.util.concurrent.CancellationException;
import org.hamcrest.core.StringEndsWith.endsWith import org.hamcrest.core.StringEndsWith.endsWith
import org.hamcrest.core.IsEqual.equalTo import org.hamcrest.core.IsEqual.equalTo
import org.json.JSONObject import org.json.JSONObject
@ -1338,6 +1339,33 @@ class WebExtensionTest : BaseSessionTest() {
assertBodyBorderEqualTo("") assertBodyBorderEqualTo("")
} }
@Test(expected = CancellationException::class)
fun cancelInstall() {
val install = controller.install("$TEST_ENDPOINT/stall/test.xpi")
val cancel = sessionRule.waitForResult(install.cancel())
assertTrue(cancel)
sessionRule.waitForResult(install)
}
@Test
fun cancelInstallFailsAfterInstalled() {
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
}
})
var install = controller.install("resource://android/assets/web_extensions/borderify.xpi");
val borderify = sessionRule.waitForResult(install)
val cancel = sessionRule.waitForResult(install.cancel())
assertFalse(cancel)
sessionRule.waitForResult(controller.uninstall(borderify))
}
@Test @Test
fun updatePostpone() { fun updatePostpone() {
sessionRule.setPrefsUntilTestEnd(mapOf( sessionRule.setPrefsUntilTestEnd(mapOf(

View File

@ -18,6 +18,7 @@ import java.util.*
class TestServer { class TestServer {
private val server = AsyncHttpServer() private val server = AsyncHttpServer()
private val assets: AssetManager private val assets: AssetManager
private val stallingResponses = Vector<AsyncHttpServerResponse>()
constructor(context: Context) { constructor(context: Context) {
assets = context.resources.assets assets = context.resources.assets
@ -132,6 +133,25 @@ class TestServer {
response.end() response.end()
} }
server.get("/stall/.*") { _, response ->
// keep trickling data for a long time (until we are stopped)
stallingResponses.add(response)
val count = 100
response.setContentType("InstallException")
response.headers.set("Content-Length", "${count}")
response.writeHead()
val payload = byteArrayOf(1)
for (i in 1..count - 1) {
response.write(ByteBufferList(payload))
SystemClock.sleep(250)
}
stallingResponses.remove(response)
response.end()
}
} }
fun start(port: Int) { fun start(port: Int) {
@ -139,6 +159,9 @@ class TestServer {
} }
fun stop() { fun stop() {
for (response in stallingResponses) {
response.end()
}
server.stop() server.stop()
} }
} }

View File

@ -21,6 +21,7 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED; import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED;
import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED; import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED;
@ -440,6 +441,67 @@ public class WebExtensionController {
} }
} }
private static class WebExtensionInstallResult extends WebExtensionResult {
private static class InstallCanceller implements GeckoResult.CancellationDelegate {
private static class CancelResult extends GeckoResult<Boolean>
implements EventCallback {
@Override
public void sendSuccess(final Object response) {
final boolean result = ((GeckoBundle) response).getBoolean("cancelled");
complete(result);
}
@Override
public void sendError(final Object response) {
completeExceptionally(new Exception(response.toString()));
}
}
private final String mInstallId;
private boolean mCancelled;
public InstallCanceller(@NonNull final String aInstallId) {
mInstallId = aInstallId;
mCancelled = false;
}
@Override
public GeckoResult<Boolean> cancel() {
CancelResult result = new CancelResult();
final GeckoBundle bundle = new GeckoBundle(1);
bundle.putString("installId", mInstallId);
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:CancelInstall",
bundle, result);
return result.then(wasCancelled -> {
mCancelled = wasCancelled;
return GeckoResult.fromValue(wasCancelled);
});
}
}
/* package */ final @NonNull String installId;
private final InstallCanceller mInstallCanceller;
public WebExtensionInstallResult() {
super("extension");
installId = UUID.randomUUID().toString();
mInstallCanceller = new InstallCanceller(installId);
setCancellationDelegate(mInstallCanceller);
}
@Override
public void sendError(final Object response) {
if (!mInstallCanceller.mCancelled) {
super.sendError(response);
}
}
}
/** /**
* Install an extension. * Install an extension.
* *
@ -478,14 +540,12 @@ public class WebExtensionController {
@NonNull @NonNull
@AnyThread @AnyThread
public GeckoResult<WebExtension> install(final @NonNull String uri) { public GeckoResult<WebExtension> install(final @NonNull String uri) {
final WebExtensionResult result = new WebExtensionResult("extension"); WebExtensionInstallResult result = new WebExtensionInstallResult();
final GeckoBundle bundle = new GeckoBundle(2);
final GeckoBundle bundle = new GeckoBundle(1);
bundle.putString("locationUri", uri); bundle.putString("locationUri", uri);
bundle.putString("installId", result.installId);
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Install", EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Install",
bundle, result); bundle, result);
return result.then(extension -> { return result.then(extension -> {
registerWebExtension(extension); registerWebExtension(extension);
return GeckoResult.fromValue(extension); return GeckoResult.fromValue(extension);
@ -523,14 +583,12 @@ public class WebExtensionController {
// TODO: Bug 1601067 make public // TODO: Bug 1601067 make public
GeckoResult<WebExtension> installBuiltIn(final String uri) { GeckoResult<WebExtension> installBuiltIn(final String uri) {
final WebExtensionResult result = new WebExtensionResult("extension"); WebExtensionInstallResult result = new WebExtensionInstallResult();
final GeckoBundle bundle = new GeckoBundle(2);
final GeckoBundle bundle = new GeckoBundle(1);
bundle.putString("locationUri", uri); bundle.putString("locationUri", uri);
bundle.putString("installId", result.installId);
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:InstallBuiltIn", EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:InstallBuiltIn",
bundle, result); bundle, result);
return result.then(extension -> { return result.then(extension -> {
registerWebExtension(extension); registerWebExtension(extension);
return GeckoResult.fromValue(extension); return GeckoResult.fromValue(extension);

View File

@ -308,8 +308,40 @@ function exportExtension(aAddon, aPermissions, aSourceURI) {
} }
class ExtensionInstallListener { class ExtensionInstallListener {
constructor(aResolve) { constructor(aResolve, aInstall, aInstallId) {
this.resolve = aResolve; this.install = aInstall;
this.installId = aInstallId;
this.resolve = result => {
aResolve(result);
EventDispatcher.instance.unregisterListener(this, [
"GeckoView:WebExtension:CancelInstall",
]);
};
EventDispatcher.instance.registerListener(this, [
"GeckoView:WebExtension:CancelInstall",
]);
}
async onEvent(aEvent, aData, aCallback) {
debug`onEvent ${aEvent} ${aData}`;
switch (aEvent) {
case "GeckoView:WebExtension:CancelInstall": {
const { installId } = aData;
if (this.installId !== installId) {
return;
}
let cancelled = false;
try {
this.install.cancel();
cancelled = true;
} catch (_) {
// install may have already failed or been cancelled
}
aCallback.onSuccess({ cancelled });
break;
}
}
} }
onDownloadCancelled(aInstall) { onDownloadCancelled(aInstall) {
@ -534,14 +566,16 @@ var GeckoViewWebExtension = {
return scope.extension; return scope.extension;
}, },
async installWebExtension(aUri) { async installWebExtension(aInstallId, aUri) {
const install = await AddonManager.getInstallForURL(aUri.spec, { const install = await AddonManager.getInstallForURL(aUri.spec, {
telemetryInfo: { telemetryInfo: {
source: "geckoview-app", source: "geckoview-app",
}, },
}); });
const promise = new Promise(resolve => { const promise = new Promise(resolve => {
install.addListener(new ExtensionInstallListener(resolve)); install.addListener(
new ExtensionInstallListener(resolve, install, aInstallId)
);
}); });
const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
@ -794,14 +828,15 @@ var GeckoViewWebExtension = {
} }
case "GeckoView:WebExtension:Install": { case "GeckoView:WebExtension:Install": {
const uri = Services.io.newURI(aData.locationUri); const { locationUri, installId } = aData;
const uri = Services.io.newURI(locationUri);
if (uri == null) { if (uri == null) {
aCallback.onError(`Could not parse uri: ${uri}`); aCallback.onError(`Could not parse uri: ${locationUri}`);
return; return;
} }
try { try {
const result = await this.installWebExtension(uri); const result = await this.installWebExtension(installId, uri);
if (result.extension) { if (result.extension) {
aCallback.onSuccess(result); aCallback.onSuccess(result);
} else { } else {