diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /mobile/android/test_runner/src/main | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/test_runner/src/main')
24 files changed, 1098 insertions, 0 deletions
diff --git a/mobile/android/test_runner/src/main/AndroidManifest.xml b/mobile/android/test_runner/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6f55aa27ab --- /dev/null +++ b/mobile/android/test_runner/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + <uses-permission android:name="android.permission.RECORD_AUDIO"/> + <uses-permission android:name="android.permission.CAMERA"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> + + <application + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:usesCleartextTraffic="true" + android:theme="@style/AppTheme" + android:name="androidx.multidex.MultiDexApplication"> + <uses-library android:name="android.test.runner" android:required="false"/> + <activity android:name=".TestRunnerActivity" + android:configChanges="orientation|screenSize" + android:exported="true"/> + <activity-alias android:name=".App" android:targetActivity=".TestRunnerActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <action android:name="org.mozilla.geckoview.test_runner.XPCSHELL_TEST"/> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity-alias> + + <!-- This is used to run xpcshell tests --> + <service android:name=".XpcshellTestRunnerService$i0" android:enabled="true" android:exported="true" android:process=":xpcshell0"/> + <service android:name=".XpcshellTestRunnerService$i1" android:enabled="true" android:exported="true" android:process=":xpcshell1"/> + <service android:name=".XpcshellTestRunnerService$i2" android:enabled="true" android:exported="true" android:process=":xpcshell2"/> + <service android:name=".XpcshellTestRunnerService$i3" android:enabled="true" android:exported="true" android:process=":xpcshell3"/> + <service android:name=".XpcshellTestRunnerService$i4" android:enabled="true" android:exported="true" android:process=":xpcshell4"/> + <service android:name=".XpcshellTestRunnerService$i5" android:enabled="true" android:exported="true" android:process=":xpcshell5"/> + <service android:name=".XpcshellTestRunnerService$i6" android:enabled="true" android:exported="true" android:process=":xpcshell6"/> + <service android:name=".XpcshellTestRunnerService$i7" android:enabled="true" android:exported="true" android:process=":xpcshell7"/> + <service android:name=".XpcshellTestRunnerService$i8" android:enabled="true" android:exported="true" android:process=":xpcshell8"/> + <service android:name=".XpcshellTestRunnerService$i9" android:enabled="true" android:exported="true" android:process=":xpcshell9"/> + </application> +</manifest> diff --git a/mobile/android/test_runner/src/main/assets/test-runner-support/manifest.json b/mobile/android/test_runner/src/main/assets/test-runner-support/manifest.json new file mode 100644 index 0000000000..3d68643af9 --- /dev/null +++ b/mobile/android/test_runner/src/main/assets/test-runner-support/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 2, + "name": "Dummy test-runner-support", + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test-runner-support@tests.mozilla.org" + } + }, + "description": "This extension pretends to be the sender of native messages from tests. See mobile/android/modules/test/AppUiTestDelegate.sys.mjs for actual usage." +} 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..3930ab545c --- /dev/null +++ b/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java @@ -0,0 +1,715 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test_runner; + +import static org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_EXPERIMENT_SLUG_NOT_FOUND; +import static org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_FEATURE_NOT_FOUND; +import static org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_UNKNOWN; + +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.json.JSONException; +import org.json.JSONObject; +import org.mozilla.geckoview.AllowOrDeny; +import org.mozilla.geckoview.ContentBlocking; +import org.mozilla.geckoview.ExperimentDelegate; +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) + .experimentDelegate(new TestRunnerExperimentDelegate()); + + 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; + } + + class TestRunnerExperimentDelegate implements ExperimentDelegate { + @Override + public GeckoResult<JSONObject> onGetExperimentFeature(@NonNull String feature) { + GeckoResult<JSONObject> result = new GeckoResult<>(); + if (feature.equals("test")) { + try { + result.complete(new JSONObject().put("item-one", true).put("item-two", 5)); + } catch (JSONException e) { + result.completeExceptionally(new ExperimentException(ERROR_UNKNOWN)); + } + } else { + result.completeExceptionally(new ExperimentException(ERROR_FEATURE_NOT_FOUND)); + } + return result; + } + + @Override + public GeckoResult<Void> onRecordExposureEvent(@NonNull String feature) { + GeckoResult<Void> result = new GeckoResult<>(); + if (feature.equals("test")) { + result.complete(null); + } else { + result.completeExceptionally(new ExperimentException(ERROR_FEATURE_NOT_FOUND)); + } + return result; + } + + @Override + public GeckoResult<Void> onRecordExperimentExposureEvent( + @NonNull String feature, @NonNull String slug) { + GeckoResult<Void> result = new GeckoResult<>(); + if (feature.equals("test") && slug.equals("test")) { + result.complete(null); + } else if (!slug.equals("test") && feature.equals("test")) { + result.completeExceptionally(new ExperimentException(ERROR_EXPERIMENT_SLUG_NOT_FOUND)); + } else { + result.completeExceptionally(new ExperimentException(ERROR_FEATURE_NOT_FOUND)); + } + return result; + } + + @Override + public GeckoResult<Void> onRecordMalformedConfigurationEvent( + @NonNull String feature, @NonNull String part) { + GeckoResult<Void> result = new GeckoResult<>(); + if (feature.equals("test")) { + result.complete(null); + } else { + result.completeExceptionally(new ExperimentException(ERROR_FEATURE_NOT_FOUND)); + } + return result; + } + } +} diff --git a/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerApiEngine.java b/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerApiEngine.java new file mode 100644 index 0000000000..c6b8e797da --- /dev/null +++ b/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerApiEngine.java @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test_runner; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.geckoview.GeckoResult; +import org.mozilla.geckoview.WebExtension; + +// Receives API calls via mobile/android/modules/test/AppUiTestDelegate.sys.mjs +// and forwards the calls to the Api impl. +// +// This interface allows JS/HTML-based mochitests to invoke test-only logic +// that is implemented by the embedder, e.g. by TestRunnerActivity. +// +// There is no implementation for xpcshell tests because the underlying concepts +// are not available to xpcshell on desktop Firefox. If there is ever a desire +// to create an instance, do so from XpcshellTestRunnerService.java. +// +// The supported messages are documented in AppTestDelegateParent.sys.mjs. +public class TestRunnerApiEngine implements WebExtension.MessageDelegate { + private static final String LOGTAG = "TestRunnerAPI"; + + public interface Api { + GeckoResult<Void> clickBrowserAction(String extensionId); + + GeckoResult<Void> clickPageAction(String extensionId); + + GeckoResult<Void> closePopup(); + + GeckoResult<Void> awaitExtensionPopup(String extensionId); + } + + private final Api mImpl; + + public TestRunnerApiEngine(final Api impl) { + mImpl = impl; + } + + @SuppressWarnings("unchecked") + private GeckoResult<Object> handleMessage(final JSONObject message) throws JSONException { + final String type = message.getString("type"); + + Log.i(LOGTAG, "Test API: " + type); + + if ("clickBrowserAction".equals(type)) { + return (GeckoResult) mImpl.clickBrowserAction(message.getString("extensionId")); + } else if ("clickPageAction".equals(type)) { + return (GeckoResult) mImpl.clickPageAction(message.getString("extensionId")); + } else if ("closeBrowserAction".equals(type)) { + return (GeckoResult) mImpl.closePopup(); + } else if ("closePageAction".equals(type)) { + return (GeckoResult) mImpl.closePopup(); + } else if ("awaitExtensionPanel".equals(type)) { + return (GeckoResult) mImpl.awaitExtensionPopup(message.getString("extensionId")); + } + + return GeckoResult.fromException(new RuntimeException("Unrecognized command " + type)); + } + + @Nullable + @Override + public GeckoResult<Object> onMessage( + @NonNull final String nativeApp, + @NonNull final Object message, + @NonNull final WebExtension.MessageSender sender) { + try { + return handleMessage((JSONObject) message); + } catch (final JSONException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/XpcshellTestRunnerService.java b/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/XpcshellTestRunnerService.java new file mode 100644 index 0000000000..529b740c28 --- /dev/null +++ b/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/XpcshellTestRunnerService.java @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test_runner; + +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import androidx.annotation.Nullable; +import java.util.HashMap; +import org.mozilla.geckoview.ContentBlocking; +import org.mozilla.geckoview.GeckoResult; +import org.mozilla.geckoview.GeckoRuntime; +import org.mozilla.geckoview.GeckoRuntimeSettings; +import org.mozilla.geckoview.WebExtension; +import org.mozilla.geckoview.WebExtensionController; + +public class XpcshellTestRunnerService extends Service { + public static final class i0 extends XpcshellTestRunnerService {} + + public static final class i1 extends XpcshellTestRunnerService {} + + public static final class i2 extends XpcshellTestRunnerService {} + + public static final class i3 extends XpcshellTestRunnerService {} + + public static final class i4 extends XpcshellTestRunnerService {} + + public static final class i5 extends XpcshellTestRunnerService {} + + public static final class i6 extends XpcshellTestRunnerService {} + + public static final class i7 extends XpcshellTestRunnerService {} + + public static final class i8 extends XpcshellTestRunnerService {} + + public static final class i9 extends XpcshellTestRunnerService {} + + private static final String LOGTAG = "XpcshellTestRunner"; + static GeckoRuntime sRuntime; + + private HashMap<String, WebExtension> mExtensions = new HashMap<>(); + + private static WebExtensionController webExtensionController() { + return sRuntime.getWebExtensionController(); + } + + @Override + public synchronized int onStartCommand(Intent intent, int flags, int startId) { + if (sRuntime != null) { + // We don't support restarting GeckoRuntime + throw new RuntimeException("Cannot start more than once"); + } + + final Bundle extras = intent.getExtras(); + for (final String key : extras.keySet()) { + Log.i(LOGTAG, "Got extras " + key + "=" + extras.get(key)); + } + + 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(); + + final GeckoRuntimeSettings runtimeSettings = + new GeckoRuntimeSettings.Builder() + .arguments(new String[] {"-xpcshell"}) + .extras(extras) + .consoleOutput(true) + .contentBlocking( + new ContentBlocking.Settings.Builder() + .safeBrowsingProviders(google, googleLegacy) + .build()) + .build(); + + sRuntime = GeckoRuntime.create(this, runtimeSettings); + + webExtensionController() + .setDebuggerDelegate( + new WebExtensionController.DebuggerDelegate() { + @Override + public void onExtensionListUpdated() { + refreshExtensionList(); + } + }); + + webExtensionController() + .setAddonManagerDelegate(new WebExtensionController.AddonManagerDelegate() {}); + + sRuntime.setDelegate( + () -> { + stopSelf(); + System.exit(0); + }); + + return Service.START_NOT_STICKY; + } + + // 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, extension); + + 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)); + } + }); + } + }); + } + + @Override + public synchronized IBinder onBind(Intent intent) { + return null; + } +} diff --git a/mobile/android/test_runner/src/main/res/drawable-nodpi/colors.png b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors.png Binary files differnew file mode 100644 index 0000000000..c9a2788e53 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors.png diff --git a/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_br.png b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_br.png Binary files differnew file mode 100644 index 0000000000..da4eba73b3 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_br.png diff --git a/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_br_scaled.png b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_br_scaled.png Binary files differnew file mode 100644 index 0000000000..c402e73bb6 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_br_scaled.png diff --git a/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_tl.png b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_tl.png Binary files differnew file mode 100644 index 0000000000..eda5c5ebf0 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_tl.png diff --git a/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_tl_scaled.png b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_tl_scaled.png Binary files differnew file mode 100644 index 0000000000..0ce5a631c4 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/drawable-nodpi/colors_tl_scaled.png diff --git a/mobile/android/test_runner/src/main/res/drawable/ic_launcher_background.xml b/mobile/android/test_runner/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..cd75f1434a --- /dev/null +++ b/mobile/android/test_runner/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + android:height="108dp" + android:width="108dp" + android:viewportHeight="108" + android:viewportWidth="108"> + <path android:fillColor="#26A69A" + android:pathData="M0,0h108v108h-108z"/> + <path android:fillColor="#00000000" android:pathData="M9,0L9,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,0L19,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M29,0L29,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M39,0L39,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M49,0L49,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M59,0L59,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M69,0L69,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M79,0L79,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M89,0L89,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M99,0L99,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,9L108,9" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,19L108,19" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,29L108,29" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,39L108,39" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,49L108,49" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,59L108,59" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,69L108,69" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,79L108,79" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,89L108,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,99L108,99" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,29L89,29" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,39L89,39" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,49L89,49" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,59L89,59" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,69L89,69" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,79L89,79" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M29,19L29,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M39,19L39,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M49,19L49,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M59,19L59,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M69,19L69,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M79,19L79,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> +</vector> diff --git a/mobile/android/test_runner/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/test_runner/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..a2f5908281 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/test_runner/src/main/res/mipmap-hdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..1b52399808 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/test_runner/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..ff10afd6e1 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/test_runner/src/main/res/mipmap-mdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..115a4c768a --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/test_runner/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..dcd3cd8083 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/test_runner/src/main/res/mipmap-xhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..459ca609d3 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/test_runner/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..8ca12fe024 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/test_runner/src/main/res/mipmap-xxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..8e19b410a1 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/test_runner/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..b824ebdd48 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/test_runner/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/test_runner/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..4c19a13c23 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/mobile/android/test_runner/src/main/res/values/colors.xml b/mobile/android/test_runner/src/main/res/values/colors.xml new file mode 100644 index 0000000000..3a96673022 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + <color name="colorPrimary">#3F51B5</color> + <color name="colorPrimaryDark">#303F9F</color> + <color name="colorAccent">#FF4081</color> +</resources> diff --git a/mobile/android/test_runner/src/main/res/values/strings.xml b/mobile/android/test_runner/src/main/res/values/strings.xml new file mode 100644 index 0000000000..7831a536eb --- /dev/null +++ b/mobile/android/test_runner/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + <string name="app_name">GeckoView Test Runner</string> +</resources> diff --git a/mobile/android/test_runner/src/main/res/values/styles.xml b/mobile/android/test_runner/src/main/res/values/styles.xml new file mode 100644 index 0000000000..60abe4bf63 --- /dev/null +++ b/mobile/android/test_runner/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<resources> + <!-- Base application theme. --> + <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar"> + <!-- Customize your theme here. --> + </style> +</resources> |