diff options
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRuntimeService.java')
-rw-r--r-- | mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRuntimeService.java | 404 |
1 files changed, 404 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRuntimeService.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRuntimeService.java new file mode 100644 index 0000000000..90db5b88f2 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRuntimeService.java @@ -0,0 +1,404 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.mozilla.gecko.util.GeckoBundle; +import org.mozilla.geckoview.GeckoResult; +import org.mozilla.geckoview.GeckoRuntime; +import org.mozilla.geckoview.GeckoRuntimeSettings; +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule; + +public class TestRuntimeService extends Service + implements GeckoSession.ProgressDelegate, GeckoRuntime.Delegate { + // Used by the client to register themselves + public static final int MESSAGE_REGISTER = 1; + // Sent when the first page load completes + public static final int MESSAGE_INIT_COMPLETE = 2; + // Sent when GeckoRuntime exits + public static final int MESSAGE_QUIT = 3; + // Reload current session + public static final int MESSAGE_RELOAD = 4; + // Load URI in current session + public static final int MESSAGE_LOAD_URI = 5; + // Receive a reply for a message + public static final int MESSAGE_REPLY = 6; + // Execute action on the remote service + public static final int MESSAGE_PAGE_STOP = 7; + + // Used by clients to know the first safe ID that can be used + // for additional message types + public static final int FIRST_SAFE_MESSAGE = MESSAGE_PAGE_STOP + 1; + + // Generic service instances + public static final class instance0 extends TestRuntimeService {} + + public static final class instance1 extends TestRuntimeService {} + + protected GeckoRuntime mRuntime; + protected GeckoSession mSession; + protected GeckoBundle mTestData; + + private Messenger mClient; + + private class TestHandler extends Handler { + @Override + public void handleMessage(@NonNull final Message msg) { + final Bundle msgData = msg.getData(); + final GeckoBundle data = + msgData != null ? GeckoBundle.fromBundle(msgData.getBundle("data")) : null; + final String id = msgData != null ? msgData.getString("id") : null; + + switch (msg.what) { + case MESSAGE_REGISTER: + mClient = msg.replyTo; + return; + case MESSAGE_QUIT: + // Unceremoniously exit + System.exit(0); + return; + case MESSAGE_RELOAD: + mSession.reload(); + break; + case MESSAGE_LOAD_URI: + mSession.loadUri(data.getString("uri")); + break; + default: + { + final GeckoResult<GeckoBundle> result = + TestRuntimeService.this.handleMessage(msg.what, data); + if (result != null) { + result.accept( + bundle -> { + final GeckoBundle reply = new GeckoBundle(); + reply.putString("id", id); + reply.putBundle("data", bundle); + TestRuntimeService.this.sendMessage(MESSAGE_REPLY, reply); + }); + } + return; + } + } + } + } + + final Messenger mMessenger = new Messenger(new TestHandler()); + + @Override + public void onShutdown() { + sendMessage(MESSAGE_QUIT); + } + + protected void sendMessage(final int message) { + sendMessage(message, null); + } + + protected void sendMessage(final int message, final GeckoBundle bundle) { + if (mClient == null) { + throw new IllegalStateException("Service is not connected yet!"); + } + + Message msg = Message.obtain(null, message); + msg.replyTo = mMessenger; + if (bundle != null) { + msg.setData(bundle.toBundle()); + } + + try { + mClient.send(msg); + } catch (RemoteException ex) { + throw new RuntimeException(ex); + } + } + + private boolean mFirstPageStop = true; + + @Override + public void onPageStop(@NonNull final GeckoSession session, final boolean success) { + // Notify the subclass that the session is ready to use + if (success && mFirstPageStop) { + onSessionReady(session); + mFirstPageStop = false; + sendMessage(MESSAGE_INIT_COMPLETE); + } else { + sendMessage(MESSAGE_PAGE_STOP); + } + } + + protected void onSessionReady(final GeckoSession session) {} + + @Override + public void onDestroy() { + // Sometimes the service doesn't die on it's own so we need to kill it here. + System.exit(0); + } + + @Nullable + @Override + public IBinder onBind(final Intent intent) { + // Request to be killed as soon as the client unbinds. + stopSelf(); + + if (mRuntime != null) { + // We only expect one client + throw new RuntimeException("Multiple clients !?"); + } + + mRuntime = createRuntime(getApplicationContext(), intent); + mRuntime.setDelegate(this); + + if (intent.hasExtra("test-data")) { + mTestData = GeckoBundle.fromBundle(intent.getBundleExtra("test-data")); + } + + mSession = createSession(intent); + mSession.setProgressDelegate(this); + mSession.open(mRuntime); + + return mMessenger.getBinder(); + } + + /** Override this to handle custom messages. */ + protected GeckoResult<GeckoBundle> handleMessage(final int messageId, final GeckoBundle data) { + return null; + } + + /** Override this to change the default runtime */ + protected GeckoRuntime createRuntime( + final @NonNull Context context, final @NonNull Intent intent) { + return GeckoRuntime.create( + context, new GeckoRuntimeSettings.Builder().extras(intent.getExtras()).build()); + } + + /** Override this to change the default session */ + protected GeckoSession createSession(final Intent intent) { + return new GeckoSession(); + } + + /** + * Starts GeckoRuntime in the process given in input, and waits for the MESSAGE_INIT_COMPLETE + * event that's fired when the first GeckoSession receives the onPageStop event. + * + * <p>We wait for a page load to make sure that everything started up correctly (as opposed to + * quitting during the startup procedure). + */ + public static class RuntimeInstance<T> { + public boolean isConnected = false; + public GeckoResult<Void> disconnected = new GeckoResult<>(); + public GeckoResult<Void> started = new GeckoResult<>(); + public GeckoResult<Void> quitted = new GeckoResult<>(); + public final Context context; + public final Class<T> service; + + private final File mProfileFolder; + private final GeckoBundle mTestData; + private final ClientHandler mClientHandler = new ClientHandler(); + private Messenger mMessenger; + private Messenger mServiceMessenger; + private GeckoResult<Void> mPageStop = null; + + private Map<String, GeckoResult<GeckoBundle>> mPendingMessages = new HashMap<>(); + + protected RuntimeInstance( + final Context context, final Class<T> service, final File profileFolder) { + this(context, service, profileFolder, null); + } + + protected RuntimeInstance( + final Context context, + final Class<T> service, + final File profileFolder, + final GeckoBundle testData) { + this.context = context; + this.service = service; + mProfileFolder = profileFolder; + mTestData = testData; + } + + public static <T> RuntimeInstance<T> start( + final Context context, final Class<T> service, final File profileFolder) { + RuntimeInstance<T> instance = new RuntimeInstance<>(context, service, profileFolder); + instance.sendIntent(); + return instance; + } + + class ClientHandler extends Handler implements ServiceConnection { + @Override + public void handleMessage(@NonNull Message msg) { + switch (msg.what) { + case MESSAGE_INIT_COMPLETE: + started.complete(null); + break; + case MESSAGE_QUIT: + quitted.complete(null); + // No reason to keep the service around anymore + context.unbindService(mClientHandler); + break; + case MESSAGE_REPLY: + final String messageId = msg.getData().getString("id"); + final Bundle data = msg.getData().getBundle("data"); + mPendingMessages.remove(messageId).complete(GeckoBundle.fromBundle(data)); + break; + case MESSAGE_PAGE_STOP: + if (mPageStop != null) { + mPageStop.complete(null); + mPageStop = null; + } + break; + default: + RuntimeInstance.this.handleMessage(msg); + break; + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + mMessenger = new Messenger(mClientHandler); + mServiceMessenger = new Messenger(binder); + isConnected = true; + + RuntimeInstance.this.sendMessage(MESSAGE_REGISTER); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + isConnected = false; + context.unbindService(this); + disconnected.complete(null); + } + } + + /** Override this to handle additional messages. */ + protected void handleMessage(Message msg) {} + + /** Override to modify the intent sent to the service */ + protected Intent createIntent(final Context context) { + return new Intent(context, service); + } + + private GeckoResult<GeckoBundle> sendMessageInternal( + final int message, final GeckoBundle bundle, final GeckoResult<GeckoBundle> result) { + if (!isConnected) { + throw new IllegalStateException("Service is not connected yet!"); + } + + final String messageId = UUID.randomUUID().toString(); + GeckoBundle data = new GeckoBundle(); + data.putString("id", messageId); + if (bundle != null) { + data.putBundle("data", bundle); + } + + Message msg = Message.obtain(null, message); + msg.replyTo = mMessenger; + msg.setData(data.toBundle()); + + if (result != null) { + mPendingMessages.put(messageId, result); + } + + try { + mServiceMessenger.send(msg); + } catch (RemoteException ex) { + throw new RuntimeException(ex); + } + + return result; + } + + private GeckoResult<Void> waitForPageStop() { + if (mPageStop == null) { + mPageStop = new GeckoResult<>(); + } + return mPageStop; + } + + protected GeckoResult<GeckoBundle> query(final int message) { + return query(message, null); + } + + protected GeckoResult<GeckoBundle> query(final int message, final GeckoBundle bundle) { + final GeckoResult<GeckoBundle> result = new GeckoResult<>(); + return sendMessageInternal(message, bundle, result); + } + + protected void sendMessage(final int message) { + sendMessage(message, null); + } + + protected void sendMessage(final int message, final GeckoBundle bundle) { + sendMessageInternal(message, bundle, null); + } + + protected void sendIntent() { + final Intent intent = createIntent(context); + intent.putExtra("args", "-profile " + mProfileFolder.getAbsolutePath()); + if (mTestData != null) { + intent.putExtra("test-data", mTestData.toBundle()); + } + context.bindService(intent, mClientHandler, Context.BIND_AUTO_CREATE); + } + + /** + * Quits the current runtime. + * + * @return a {@link GeckoResult} that is resolved when the service fully disconnects. + */ + public GeckoResult<Void> quit() { + sendMessage(MESSAGE_QUIT); + return disconnected; + } + + /** + * Reloads the current session. + * + * @return A {@link GeckoResult} that is resolved when the page is fully reloaded. + */ + public GeckoResult<Void> reload() { + sendMessage(MESSAGE_RELOAD); + return waitForPageStop(); + } + + /** + * Load a test path in the current session. + * + * @return A {@link GeckoResult} that is resolved when the page is fully loaded. + */ + public GeckoResult<Void> loadTestPath(final String path) { + return loadUri(GeckoSessionTestRule.TEST_ENDPOINT + path); + } + + /** + * Load an arbitrary URI in the current session. + * + * @return A {@link GeckoResult} that is resolved when the page is fully loaded. + */ + public GeckoResult<Void> loadUri(final String uri) { + return started.then( + unused -> { + final GeckoBundle data = new GeckoBundle(1); + data.putString("uri", uri); + sendMessage(MESSAGE_LOAD_URI, data); + return waitForPageStop(); + }); + } + } +} |