summaryrefslogtreecommitdiffstats
path: root/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java')
-rw-r--r--mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java653
1 files changed, 653 insertions, 0 deletions
diff --git a/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java b/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java
new file mode 100644
index 0000000000..f95efeab1b
--- /dev/null
+++ b/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java
@@ -0,0 +1,653 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.geckoview.test_runner;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.SurfaceTexture;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Surface;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.mozilla.geckoview.AllowOrDeny;
+import org.mozilla.geckoview.ContentBlocking;
+import org.mozilla.geckoview.GeckoDisplay;
+import org.mozilla.geckoview.GeckoResult;
+import org.mozilla.geckoview.GeckoRuntime;
+import org.mozilla.geckoview.GeckoRuntimeSettings;
+import org.mozilla.geckoview.GeckoSession;
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission;
+import org.mozilla.geckoview.GeckoSessionSettings;
+import org.mozilla.geckoview.GeckoView;
+import org.mozilla.geckoview.OrientationController;
+import org.mozilla.geckoview.WebExtension;
+import org.mozilla.geckoview.WebExtensionController;
+import org.mozilla.geckoview.WebRequestError;
+
+public class TestRunnerActivity extends Activity {
+ private static final String LOGTAG = "TestRunnerActivity";
+ private static final String ERROR_PAGE =
+ "<!DOCTYPE html><head><title>Error</title></head><body>Error!</body></html>";
+
+ static GeckoRuntime sRuntime;
+
+ private GeckoSession mPopupSession;
+ private GeckoSession mSession;
+ private GeckoView mView;
+ private boolean mKillProcessOnDestroy;
+
+ private HashMap<GeckoSession, Display> mDisplays = new HashMap<>();
+ private HashMap<String, ExtensionWrapper> mExtensions = new HashMap<>();
+
+ private static class ExtensionWrapper {
+ public WebExtension extension;
+ public HashMap<GeckoSession, WebExtension.Action> browserActions;
+ public HashMap<GeckoSession, WebExtension.Action> pageActions;
+
+ public ExtensionWrapper(final WebExtension extension) {
+ this.extension = extension;
+ browserActions = new HashMap<>();
+ pageActions = new HashMap<>();
+ }
+ }
+
+ private static class Display {
+ public final SurfaceTexture texture;
+ public final Surface surface;
+
+ private final int width;
+ private final int height;
+ private GeckoDisplay sessionDisplay;
+
+ public Display(final int width, final int height) {
+ this.width = width;
+ this.height = height;
+ texture = new SurfaceTexture(0);
+ texture.setDefaultBufferSize(width, height);
+ surface = new Surface(texture);
+ }
+
+ public void attach(final GeckoSession session) {
+ sessionDisplay = session.acquireDisplay();
+ sessionDisplay.surfaceChanged(
+ new GeckoDisplay.SurfaceInfo.Builder(surface).size(width, height).build());
+ }
+
+ public void release(final GeckoSession session) {
+ sessionDisplay.surfaceDestroyed();
+ session.releaseDisplay(sessionDisplay);
+ }
+ }
+
+ private static WebExtensionController webExtensionController() {
+ return sRuntime.getWebExtensionController();
+ }
+
+ private static OrientationController orientationController() {
+ return sRuntime.getOrientationController();
+ }
+
+ // Keeps track of all sessions for this test runner. The top session in the deque is the
+ // current active session for extension purposes.
+ private ArrayDeque<GeckoSession> mOwnedSessions = new ArrayDeque<>();
+
+ private GeckoSession.PermissionDelegate mPermissionDelegate =
+ new GeckoSession.PermissionDelegate() {
+ @Override
+ public GeckoResult<Integer> onContentPermissionRequest(
+ @NonNull final GeckoSession session, @NonNull ContentPermission perm) {
+ return GeckoResult.fromValue(ContentPermission.VALUE_ALLOW);
+ }
+
+ @Override
+ public void onAndroidPermissionsRequest(
+ @NonNull final GeckoSession session,
+ @Nullable final String[] permissions,
+ @NonNull final Callback callback) {
+ callback.grant();
+ }
+ };
+
+ private GeckoSession.PromptDelegate mPromptDelegate =
+ new GeckoSession.PromptDelegate() {
+ Map<BasePrompt, GeckoResult<PromptResponse>> mPromptResults = new HashMap<>();
+ public GeckoSession.PromptDelegate.PromptInstanceDelegate mPromptInstanceDelegate =
+ new GeckoSession.PromptDelegate.PromptInstanceDelegate() {
+ @Override
+ public void onPromptDismiss(
+ final @NonNull GeckoSession.PromptDelegate.BasePrompt prompt) {
+ mPromptResults.get(prompt).complete(prompt.dismiss());
+ }
+ };
+
+ @Override
+ public GeckoResult<PromptResponse> onAlertPrompt(
+ @NonNull final GeckoSession session, @NonNull final AlertPrompt prompt) {
+ mPromptResults.put(prompt, new GeckoResult<>());
+ prompt.setDelegate(mPromptInstanceDelegate);
+ return mPromptResults.get(prompt);
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onButtonPrompt(
+ @NonNull final GeckoSession session, @NonNull final ButtonPrompt prompt) {
+ mPromptResults.put(prompt, new GeckoResult<>());
+ prompt.setDelegate(mPromptInstanceDelegate);
+ return mPromptResults.get(prompt);
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onTextPrompt(
+ @NonNull final GeckoSession session, @NonNull final TextPrompt prompt) {
+ mPromptResults.put(prompt, new GeckoResult<>());
+ prompt.setDelegate(mPromptInstanceDelegate);
+ return mPromptResults.get(prompt);
+ }
+ };
+
+ private GeckoSession.NavigationDelegate mNavigationDelegate =
+ new GeckoSession.NavigationDelegate() {
+ @Override
+ public void onLocationChange(
+ final GeckoSession session, final String url, final List<ContentPermission> perms) {
+ getActionBar().setSubtitle(url);
+ }
+
+ @Override
+ public GeckoResult<AllowOrDeny> onLoadRequest(
+ final GeckoSession session, final LoadRequest request) {
+ // Allow Gecko to load all URIs
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+
+ @Override
+ public GeckoResult<GeckoSession> onNewSession(
+ final GeckoSession session, final String uri) {
+ webExtensionController().setTabActive(mOwnedSessions.peek(), false);
+ final GeckoSession newSession =
+ createBackgroundSession(session.getSettings(), /* active */ true);
+ webExtensionController().setTabActive(newSession, true);
+ return GeckoResult.fromValue(newSession);
+ }
+
+ @Override
+ public GeckoResult<String> onLoadError(
+ final GeckoSession session, final String uri, final WebRequestError error) {
+
+ return GeckoResult.fromValue("data:text/html," + ERROR_PAGE);
+ }
+ };
+
+ private GeckoSession.ContentDelegate mContentDelegate =
+ new GeckoSession.ContentDelegate() {
+ private void onContentProcessGone() {
+ if (System.getenv("MOZ_CRASHREPORTER_SHUTDOWN") != null) {
+ sRuntime.shutdown();
+ }
+ }
+
+ @Override
+ public void onCloseRequest(final GeckoSession session) {
+ closeSession(session);
+ }
+
+ @Override
+ public void onCrash(final GeckoSession session) {
+ onContentProcessGone();
+ }
+
+ @Override
+ public void onKill(final GeckoSession session) {
+ onContentProcessGone();
+ }
+ };
+
+ private WebExtension.TabDelegate mTabDelegate =
+ new WebExtension.TabDelegate() {
+ @Override
+ public GeckoResult<GeckoSession> onNewTab(
+ final WebExtension source, final WebExtension.CreateTabDetails details) {
+ GeckoSessionSettings settings = null;
+ if (details.cookieStoreId != null) {
+ settings = new GeckoSessionSettings.Builder().contextId(details.cookieStoreId).build();
+ }
+
+ if (details.active == Boolean.TRUE) {
+ webExtensionController().setTabActive(mOwnedSessions.peek(), false);
+ }
+ final GeckoSession newSession = createSession(settings, details.active == Boolean.TRUE);
+ return GeckoResult.fromValue(newSession);
+ }
+ };
+
+ private WebExtension.ActionDelegate mActionDelegate =
+ new WebExtension.ActionDelegate() {
+ private GeckoResult<GeckoSession> togglePopup(
+ final WebExtension extension, final boolean forceOpen) {
+ if (mPopupSession != null) {
+ mPopupSession.close();
+ if (!forceOpen) {
+ return null;
+ }
+ }
+
+ mPopupSession = createBackgroundSession(null, /* active */ false);
+ mPopupSession.open(sRuntime);
+
+ // Set the progress delegate in case there is an observer to the popup being loaded.
+ mTestApiImpl.setCurrentPopupExtension(extension);
+ mPopupSession.setProgressDelegate(mTestApiImpl);
+
+ return GeckoResult.fromValue(mPopupSession);
+ }
+
+ @Nullable
+ @Override
+ public GeckoResult<GeckoSession> onOpenPopup(
+ final WebExtension extension, final WebExtension.Action action) {
+ return togglePopup(extension, true);
+ }
+
+ @Nullable
+ @Override
+ public GeckoResult<GeckoSession> onTogglePopup(
+ final WebExtension extension, final WebExtension.Action action) {
+ return togglePopup(extension, false);
+ }
+
+ @Override
+ public void onBrowserAction(
+ final WebExtension extension,
+ final GeckoSession session,
+ final WebExtension.Action action) {
+ mExtensions.get(extension.id).browserActions.put(session, action);
+ }
+
+ @Override
+ public void onPageAction(
+ final WebExtension extension,
+ final GeckoSession session,
+ final WebExtension.Action action) {
+ mExtensions.get(extension.id).pageActions.put(session, action);
+ }
+ };
+
+ private WebExtension.SessionTabDelegate mSessionTabDelegate =
+ new WebExtension.SessionTabDelegate() {
+ @NonNull
+ @Override
+ public GeckoResult<AllowOrDeny> onCloseTab(
+ @Nullable final WebExtension source, @NonNull final GeckoSession session) {
+ closeSession(session);
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+
+ @Override
+ public GeckoResult<AllowOrDeny> onUpdateTab(
+ @NonNull final WebExtension source,
+ @NonNull final GeckoSession session,
+ @NonNull final WebExtension.UpdateTabDetails updateDetails) {
+ if (updateDetails.active == Boolean.TRUE) {
+ // Move session to the top since it's now the active tab
+ mOwnedSessions.remove(session);
+ mOwnedSessions.addFirst(session);
+ }
+
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+ };
+
+ private class TestRunnerActivityDelegate implements GeckoView.ActivityContextDelegate {
+ public Context getActivityContext() {
+ return TestRunnerActivity.this;
+ }
+ }
+
+ private TestRunnerActivityDelegate mActivityDelegate = new TestRunnerActivityDelegate();
+
+ /**
+ * Creates a session and adds it to the owned sessions deque.
+ *
+ * @param active Whether this session is the "active" session for extension purposes. The active
+ * session always sit at the top of the owned sessions deque.
+ * @return the newly created session.
+ */
+ private GeckoSession createSession(final boolean active) {
+ return createSession(null, active);
+ }
+
+ /**
+ * Creates a session and adds it to the owned sessions deque.
+ *
+ * @param settings settings for the newly created {@link GeckoSession}, could be null if no extra
+ * settings need to be added.
+ * @param active Whether this session is the "active" session for extension purposes. The active
+ * session always sit at the top of the owned sessions deque.
+ * @return the newly created session.
+ */
+ private GeckoSession createSession(GeckoSessionSettings settings, final boolean active) {
+ if (settings == null) {
+ settings = new GeckoSessionSettings();
+ }
+
+ final GeckoSession session = new GeckoSession(settings);
+ session.setNavigationDelegate(mNavigationDelegate);
+ session.setContentDelegate(mContentDelegate);
+ session.setPermissionDelegate(mPermissionDelegate);
+ session.setPromptDelegate(mPromptDelegate);
+
+ final WebExtension.SessionController sessionController = session.getWebExtensionController();
+ for (final ExtensionWrapper wrapper : mExtensions.values()) {
+ sessionController.setActionDelegate(wrapper.extension, mActionDelegate);
+ sessionController.setTabDelegate(wrapper.extension, mSessionTabDelegate);
+ }
+
+ if (active) {
+ mOwnedSessions.addFirst(session);
+ } else {
+ mOwnedSessions.addLast(session);
+ }
+ return session;
+ }
+
+ /**
+ * Creates a session with a display attached.
+ *
+ * @param settings settings for the newly created {@link GeckoSession}, could be null if no extra
+ * settings need to be added.
+ * @param active Whether this session is the "active" session for extension purposes. The active
+ * session always sit at the top of the owned sessions deque.
+ * @return the newly created session.
+ */
+ private GeckoSession createBackgroundSession(
+ final GeckoSessionSettings settings, final boolean active) {
+ final GeckoSession session = createSession(settings, active);
+
+ final Display display = new Display(mView.getWidth(), mView.getHeight());
+ display.attach(session);
+
+ mDisplays.put(session, display);
+
+ return session;
+ }
+
+ private void closeSession(final GeckoSession session) {
+ if (session == mOwnedSessions.peek()) {
+ webExtensionController().setTabActive(session, false);
+ }
+ if (mDisplays.containsKey(session)) {
+ final Display display = mDisplays.remove(session);
+ display.release(session);
+ }
+ mOwnedSessions.remove(session);
+ session.close();
+ if (!mOwnedSessions.isEmpty()) {
+ // Pick the top session as the current active
+ webExtensionController().setTabActive(mOwnedSessions.peek(), true);
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final Intent intent = getIntent();
+
+ if ("org.mozilla.geckoview.test.XPCSHELL_TEST_MAIN".equals(intent.getAction())) {
+ // This activity is just a stub to run xpcshell tests in a service
+ return;
+ }
+
+ if (sRuntime == null) {
+ final GeckoRuntimeSettings.Builder runtimeSettingsBuilder =
+ new GeckoRuntimeSettings.Builder();
+
+ // Mochitest and reftest encounter rounding errors if we have a
+ // a window.devicePixelRation like 3.625, so simplify that here.
+ runtimeSettingsBuilder
+ .arguments(new String[] {"-purgecaches"})
+ .displayDpiOverride(160)
+ .displayDensityOverride(1.0f)
+ .remoteDebuggingEnabled(true);
+
+ final Bundle extras = intent.getExtras();
+ if (extras != null) {
+ runtimeSettingsBuilder.extras(extras);
+ }
+
+ final ContentBlocking.SafeBrowsingProvider googleLegacy =
+ ContentBlocking.SafeBrowsingProvider.from(
+ ContentBlocking.GOOGLE_LEGACY_SAFE_BROWSING_PROVIDER)
+ .getHashUrl("http://mochi.test:8888/safebrowsing-dummy/gethash")
+ .updateUrl("http://mochi.test:8888/safebrowsing-dummy/update")
+ .build();
+
+ final ContentBlocking.SafeBrowsingProvider google =
+ ContentBlocking.SafeBrowsingProvider.from(ContentBlocking.GOOGLE_SAFE_BROWSING_PROVIDER)
+ .getHashUrl("http://mochi.test:8888/safebrowsing4-dummy/gethash")
+ .updateUrl("http://mochi.test:8888/safebrowsing4-dummy/update")
+ .build();
+
+ runtimeSettingsBuilder
+ .consoleOutput(true)
+ .contentBlocking(
+ new ContentBlocking.Settings.Builder()
+ .safeBrowsingProviders(google, googleLegacy)
+ .build());
+
+ sRuntime = GeckoRuntime.create(this, runtimeSettingsBuilder.build());
+
+ webExtensionController()
+ .setDebuggerDelegate(
+ new WebExtensionController.DebuggerDelegate() {
+ @Override
+ public void onExtensionListUpdated() {
+ refreshExtensionList();
+ }
+ });
+
+ webExtensionController()
+ .installBuiltIn("resource://android/assets/test-runner-support/")
+ .accept(
+ extension -> {
+ extension.setMessageDelegate(mApiEngine, "test-runner-support");
+ extension.setTabDelegate(mTabDelegate);
+ });
+
+ webExtensionController()
+ .setAddonManagerDelegate(new WebExtensionController.AddonManagerDelegate() {});
+
+ sRuntime.setDelegate(
+ () -> {
+ mKillProcessOnDestroy = true;
+ finish();
+ });
+ }
+
+ orientationController()
+ .setDelegate(
+ new OrientationController.OrientationDelegate() {
+ @Override
+ public GeckoResult<AllowOrDeny> onOrientationLock(int aOrientation) {
+ setRequestedOrientation(aOrientation);
+ return GeckoResult.allow();
+ }
+ });
+
+ mSession = createSession(/* active */ true);
+ webExtensionController().setTabActive(mOwnedSessions.peek(), true);
+ mSession.open(sRuntime);
+
+ // If we were passed a URI in the Intent, open it
+ final Uri uri = intent.getData();
+ if (uri != null) {
+ mSession.loadUri(uri.toString());
+ }
+
+ mView = new GeckoView(this);
+ mView.setSession(mSession);
+ setContentView(mView);
+ mView.setActivityContextDelegate(mActivityDelegate);
+
+ sRuntime.setServiceWorkerDelegate(
+ new GeckoRuntime.ServiceWorkerDelegate() {
+ @NonNull
+ @Override
+ public GeckoResult<GeckoSession> onOpenWindow(@NonNull String url) {
+ return mNavigationDelegate.onNewSession(mSession, url);
+ }
+ });
+ }
+
+ private final TestApiImpl mTestApiImpl = new TestApiImpl();
+ private final TestRunnerApiEngine mApiEngine = new TestRunnerApiEngine(mTestApiImpl);
+
+ private class TestApiImpl implements TestRunnerApiEngine.Api, GeckoSession.ProgressDelegate {
+ private GeckoResult<WebExtension> mOnPopupLoaded;
+ // Stores which extension opened the current popup
+ private WebExtension mCurrentPopupExtension;
+
+ @Override
+ public void onPageStop(final GeckoSession session, final boolean success) {
+ if (mOnPopupLoaded != null) {
+ mOnPopupLoaded.complete(mCurrentPopupExtension);
+ mOnPopupLoaded = null;
+ mCurrentPopupExtension = null;
+ }
+ session.setProgressDelegate(null);
+ }
+
+ public void setCurrentPopupExtension(final WebExtension extension) {
+ mCurrentPopupExtension = extension;
+ }
+
+ private GeckoResult<Void> clickAction(
+ final String extensionId, final HashMap<GeckoSession, WebExtension.Action> actions) {
+ final GeckoSession active = mOwnedSessions.peek();
+
+ WebExtension.Action action = actions.get(active);
+ if (action == null) {
+ // Get default action if there's no specific one
+ action = actions.get(null);
+ }
+
+ if (action == null) {
+ return GeckoResult.fromException(
+ new RuntimeException("No browser action for " + extensionId));
+ }
+
+ action.click();
+ return GeckoResult.fromValue(null);
+ }
+
+ @Override
+ public GeckoResult<Void> clickPageAction(final String extensionId) {
+ if (!mExtensions.containsKey(extensionId)) {
+ return GeckoResult.fromException(
+ new RuntimeException("Extension not found: " + extensionId));
+ }
+
+ return clickAction(extensionId, mExtensions.get(extensionId).pageActions);
+ }
+
+ @Override
+ public GeckoResult<Void> clickBrowserAction(final String extensionId) {
+ if (!mExtensions.containsKey(extensionId)) {
+ return GeckoResult.fromException(
+ new RuntimeException("Extension not found: " + extensionId));
+ }
+
+ return clickAction(extensionId, mExtensions.get(extensionId).browserActions);
+ }
+
+ @Override
+ public GeckoResult<Void> closePopup() {
+ if (mPopupSession != null) {
+ mPopupSession.close();
+ mPopupSession = null;
+ }
+ return null;
+ }
+
+ @Override
+ public GeckoResult<Void> awaitExtensionPopup(final String extensionId) {
+ mOnPopupLoaded = new GeckoResult<>();
+ return mOnPopupLoaded.accept(
+ extension -> {
+ if (!extension.id.equals(extensionId)) {
+ throw new IllegalStateException(
+ "Expecting panel from extension: "
+ + extensionId
+ + " found "
+ + extension.id
+ + " instead.");
+ }
+ });
+ }
+ }
+
+ // Random start timestamp for the BrowsingDataDelegate API.
+ private static final int CLEAR_DATA_START_TIMESTAMP = 1234;
+
+ private void refreshExtensionList() {
+ webExtensionController()
+ .list()
+ .accept(
+ extensions -> {
+ mExtensions.clear();
+ for (final WebExtension extension : extensions) {
+ mExtensions.put(extension.id, new ExtensionWrapper(extension));
+ extension.setActionDelegate(mActionDelegate);
+ extension.setTabDelegate(mTabDelegate);
+
+ extension.setBrowsingDataDelegate(
+ new WebExtension.BrowsingDataDelegate() {
+ @Nullable
+ @Override
+ public GeckoResult<Settings> onGetSettings() {
+ final long types =
+ Type.CACHE
+ | Type.COOKIES
+ | Type.HISTORY
+ | Type.FORM_DATA
+ | Type.DOWNLOADS;
+ return GeckoResult.fromValue(
+ new Settings(CLEAR_DATA_START_TIMESTAMP, types, types));
+ }
+ });
+
+ for (final GeckoSession session : mOwnedSessions) {
+ final WebExtension.SessionController controller =
+ session.getWebExtensionController();
+ controller.setActionDelegate(extension, mActionDelegate);
+ controller.setTabDelegate(extension, mSessionTabDelegate);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ mSession.close();
+ super.onDestroy();
+
+ if (mKillProcessOnDestroy) {
+ android.os.Process.killProcess(android.os.Process.myPid());
+ }
+ }
+
+ public GeckoView getGeckoView() {
+ return mView;
+ }
+
+ public GeckoSession getGeckoSession() {
+ return mSession;
+ }
+}