| Index: components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionBase.java
|
| diff --git a/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionBase.java b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionBase.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..db793b01a07c9229c333c597760d911fe673dfb9
|
| --- /dev/null
|
| +++ b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionBase.java
|
| @@ -0,0 +1,431 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +package org.chromium.components.devtools_bridge;
|
| +
|
| +import java.nio.ByteBuffer;
|
| +import java.util.ArrayList;
|
| +import java.util.HashMap;
|
| +import java.util.List;
|
| +import java.util.Map;
|
| +
|
| +/**
|
| + * Base class for ServerSession and ClientSession. Both opens a control channel and a default
|
| + * tunnel. Control channel designated to exchange messages defined in SessionControlMessages.
|
| + *
|
| + * Signaling communication between client and server works in request/response manner. It's more
|
| + * restrictive than traditional bidirectional signaling channel but give more freedom in
|
| + * implementing signaling. Main motivation is that GCD provides API what works in that way.
|
| + *
|
| + * Session is initiated by a client. It creates an offer and sends it along with RTC configuration.
|
| + * Server sends an answer in response. Once session negotiated client starts ICE candidates
|
| + * exchange. It periodically sends own candidates and peeks server's ones. Periodic ICE exchange
|
| + * stops when control channel opens. It resumes if connections state turns to DISCONNECTED (because
|
| + * server may generate ICE candidates to recover connectivity but may not notify through
|
| + * control channel). ICE exchange in CONNECTED state designated to let improve connection
|
| + * when network configuration changed.
|
| + *
|
| + * If session is not started (or resumed) after mAutoCloseTimeoutMs it closes itself.
|
| + *
|
| + * Only default tunnel is supported at the moment. It designated for DevTools UNIX socket.
|
| + * Additional tunnels may be useful for: 1) reverse port forwarding and 2) tunneling
|
| + * WebView DevTools sockets of other applications. Additional tunnels negotiation should
|
| + * be implemented by adding new types of control messages. Dynamic tunnel configuration
|
| + * will need support for session renegotiation.
|
| + *
|
| + * Session is a single threaded object. Until started owner is responsible to synchronizing access
|
| + * to it. When started it must be called on the thread of SessionBase.Executor.
|
| + * All WebRTC callbacks are forwarded on this thread.
|
| + */
|
| +public abstract class SessionBase {
|
| + private static final int CONTROL_CHANNEL_ID = 0;
|
| + private static final int DEFAULT_TUNNEL_CHANNEL_ID = 1;
|
| +
|
| + private final Executor mExecutor;
|
| + private final SessionDependencyFactory mFactory;
|
| + private AbstractPeerConnection mConnection;
|
| + private AbstractDataChannel mControlChannel;
|
| + private List<String> mCandidates = new ArrayList<String>();
|
| + private boolean mControlChannelOpened = false;
|
| + private boolean mConnected = false;
|
| + private Cancellable mAutoCloseTask;
|
| + private SessionControlMessages.MessageHandler mControlMessageHandler;
|
| + private final Map<Integer, SocketTunnelBase> mTunnels =
|
| + new HashMap<Integer, SocketTunnelBase>();
|
| + private EventListener mEventListener;
|
| +
|
| + protected int mAutoCloseTimeoutMs = 30000;
|
| +
|
| + /**
|
| + * Allows to post tasks on the thread where the sessions lives.
|
| + */
|
| + public interface Executor {
|
| + Cancellable postOnSessionThread(int delayMs, Runnable runnable);
|
| + boolean isCalledOnSessionThread();
|
| + }
|
| +
|
| + /**
|
| + * Interface for cancelling scheduled tasks.
|
| + */
|
| + public interface Cancellable {
|
| + void cancel();
|
| + }
|
| +
|
| + /**
|
| + * Representation of server session. All methods are delivered through
|
| + * signaling channel (except test configurations). Server session is accessible
|
| + * in request/response manner.
|
| + */
|
| + public interface ServerSessionInterface {
|
| + /**
|
| + * Starts session with specified RTC configuration and offer.
|
| + */
|
| + void startSession(RTCConfiguration config,
|
| + String offer,
|
| + NegotiationCallback callback);
|
| +
|
| + /**
|
| + * Renegoteates session. Needed when tunnels added/removed on the fly.
|
| + */
|
| + void renegotiate(String offer, NegotiationCallback callback);
|
| +
|
| + /**
|
| + * Sends client's ICE candidates to the server and peeks server's ICE candidates.
|
| + */
|
| + void iceExchange(List<String> clientCandidates, IceExchangeCallback callback);
|
| + }
|
| +
|
| + /**
|
| + * Base interface for server callbacks.
|
| + */
|
| + public interface ServerCallback {
|
| + void onFailure(String errorMessage);
|
| + }
|
| +
|
| + /**
|
| + * Server's response to startSession and renegotiate methods.
|
| + */
|
| + public interface NegotiationCallback extends ServerCallback {
|
| + void onSuccess(String answer);
|
| + }
|
| +
|
| + /**
|
| + * Server's response on iceExchange method.
|
| + */
|
| + public interface IceExchangeCallback extends ServerCallback {
|
| + void onSuccess(List<String> serverCandidates);
|
| + }
|
| +
|
| + /**
|
| + * Listener of session's events.
|
| + */
|
| + public interface EventListener {
|
| + void onCloseSelf();
|
| + }
|
| +
|
| + protected SessionBase(SessionDependencyFactory factory,
|
| + Executor executor,
|
| + SocketTunnelBase defaultTunnel) {
|
| + mExecutor = executor;
|
| + mFactory = factory;
|
| + addTunnel(DEFAULT_TUNNEL_CHANNEL_ID, defaultTunnel);
|
| + }
|
| +
|
| + public final void dispose() {
|
| + checkCalledOnSessionThread();
|
| +
|
| + if (isStarted()) stop();
|
| + }
|
| +
|
| + public void setEventListener(EventListener listener) {
|
| + checkCalledOnSessionThread();
|
| +
|
| + mEventListener = listener;
|
| + }
|
| +
|
| + protected AbstractPeerConnection connection() {
|
| + return mConnection;
|
| + }
|
| +
|
| + protected boolean doesTunnelExist(int channelId) {
|
| + return mTunnels.containsKey(channelId);
|
| + }
|
| +
|
| + private final void addTunnel(int channelId, SocketTunnelBase tunnel) {
|
| + assert !mTunnels.containsKey(channelId);
|
| + assert !tunnel.isBound();
|
| + // Tunnel renegotiation not implemented.
|
| + assert channelId == DEFAULT_TUNNEL_CHANNEL_ID && !isStarted();
|
| +
|
| + mTunnels.put(channelId, tunnel);
|
| + }
|
| +
|
| + protected void removeTunnel(int channelId) {
|
| + assert mTunnels.containsKey(channelId);
|
| + mTunnels.get(channelId).unbind().dispose();
|
| + mTunnels.remove(channelId);
|
| + }
|
| +
|
| + protected final boolean isControlChannelOpened() {
|
| + return mControlChannelOpened;
|
| + }
|
| +
|
| + protected final boolean isConnected() {
|
| + return mConnected;
|
| + }
|
| +
|
| + protected final void postOnSessionThread(Runnable runnable) {
|
| + postOnSessionThread(0, runnable);
|
| + }
|
| +
|
| + protected final Cancellable postOnSessionThread(int delayMs, Runnable runnable) {
|
| + return mExecutor.postOnSessionThread(delayMs, runnable);
|
| + }
|
| +
|
| + protected final void checkCalledOnSessionThread() {
|
| + assert mExecutor.isCalledOnSessionThread();
|
| + }
|
| +
|
| + public final boolean isStarted() {
|
| + return mConnection != null;
|
| + }
|
| +
|
| + /**
|
| + * Creates and configures peer connection and sets a control message handler.
|
| + */
|
| + protected void start(RTCConfiguration config,
|
| + SessionControlMessages.MessageHandler handler) {
|
| + assert !isStarted();
|
| +
|
| + mConnection = mFactory.createPeerConnection(config, new ConnectionObserver());
|
| + mControlChannel = mConnection.createDataChannel(CONTROL_CHANNEL_ID);
|
| + mControlMessageHandler = handler;
|
| + mControlChannel.registerObserver(new ControlChannelObserver());
|
| +
|
| + for (Map.Entry<Integer, SocketTunnelBase> entry : mTunnels.entrySet()) {
|
| + int channelId = entry.getKey();
|
| + SocketTunnelBase tunnel = entry.getValue();
|
| + tunnel.bind(connection().createDataChannel(channelId));
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Disposed objects created in |start|.
|
| + */
|
| + public void stop() {
|
| + checkCalledOnSessionThread();
|
| +
|
| + assert isStarted();
|
| +
|
| + stopAutoCloseTimer();
|
| +
|
| + for (SocketTunnelBase tunnel : mTunnels.values()) {
|
| + tunnel.unbind().dispose();
|
| + }
|
| +
|
| + AbstractPeerConnection connection = mConnection;
|
| + mConnection = null;
|
| + assert !isStarted();
|
| +
|
| + mControlChannel.unregisterObserver();
|
| + mControlMessageHandler = null;
|
| + mControlChannel.dispose();
|
| + mControlChannel = null;
|
| +
|
| + // Dispose connection after data channels.
|
| + connection.dispose();
|
| + }
|
| +
|
| + protected abstract void onRemoteDescriptionSet();
|
| + protected abstract void onLocalDescriptionCreatedAndSet(
|
| + AbstractPeerConnection.SessionDescriptionType type, String description);
|
| + protected abstract void onControlChannelOpened();
|
| +
|
| + protected void onControlChannelClosed() {
|
| + closeSelf();
|
| + }
|
| +
|
| + protected void onIceConnectionChange() {}
|
| +
|
| + private void handleFailureOnSignalingThread(final String message) {
|
| + postOnSessionThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (isStarted())
|
| + onFailure(message);
|
| + }
|
| + });
|
| + }
|
| +
|
| + protected final void startAutoCloseTimer() {
|
| + assert mAutoCloseTask == null;
|
| + assert isStarted();
|
| + mAutoCloseTask = postOnSessionThread(mAutoCloseTimeoutMs, new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + assert isStarted();
|
| +
|
| + mAutoCloseTask = null;
|
| + closeSelf();
|
| + }
|
| + });
|
| + }
|
| +
|
| + protected final void stopAutoCloseTimer() {
|
| + if (mAutoCloseTask != null) {
|
| + mAutoCloseTask.cancel();
|
| + mAutoCloseTask = null;
|
| + }
|
| + }
|
| +
|
| + protected void closeSelf() {
|
| + stop();
|
| + if (mEventListener != null) {
|
| + mEventListener.onCloseSelf();
|
| + }
|
| + }
|
| +
|
| + // Returns collected candidates (for sending to the remote session) and removes them.
|
| + protected List<String> takeIceCandidates() {
|
| + List<String> result = new ArrayList<String>();
|
| + result.addAll(mCandidates);
|
| + mCandidates.clear();
|
| + return result;
|
| + }
|
| +
|
| + protected void addIceCandidates(List<String> candidates) {
|
| + for (String candidate : candidates) {
|
| + mConnection.addIceCandidate(candidate);
|
| + }
|
| + }
|
| +
|
| + protected void onFailure(String message) {
|
| + closeSelf();
|
| + }
|
| +
|
| + protected void onIceCandidate(String candidate) {
|
| + mCandidates.add(candidate);
|
| + }
|
| +
|
| + /**
|
| + * Receives callbacks from the peer connection on the signaling thread. Forwards them
|
| + * on the session thread. All session event handling methods assume session started (prevents
|
| + * disposed objects). It drops callbacks it closed.
|
| + */
|
| + private final class ConnectionObserver implements AbstractPeerConnection.Observer {
|
| + @Override
|
| + public void onFailure(final String description) {
|
| + postOnSessionThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (!isStarted()) return;
|
| + SessionBase.this.onFailure(description);
|
| + }
|
| + });
|
| + }
|
| +
|
| + @Override
|
| + public void onLocalDescriptionCreatedAndSet(
|
| + final AbstractPeerConnection.SessionDescriptionType type,
|
| + final String description) {
|
| + postOnSessionThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (!isStarted()) return;
|
| + SessionBase.this.onLocalDescriptionCreatedAndSet(type, description);
|
| + }
|
| + });
|
| + }
|
| +
|
| + @Override
|
| + public void onRemoteDescriptionSet() {
|
| + postOnSessionThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (!isStarted()) return;
|
| + SessionBase.this.onRemoteDescriptionSet();
|
| + }
|
| + });
|
| + }
|
| +
|
| + @Override
|
| + public void onIceCandidate(final String candidate) {
|
| + postOnSessionThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (!isStarted()) return;
|
| + SessionBase.this.onIceCandidate(candidate);
|
| + }
|
| + });
|
| + }
|
| +
|
| + @Override
|
| + public void onIceConnectionChange(final boolean connected) {
|
| + postOnSessionThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (!isStarted()) return;
|
| + mConnected = connected;
|
| + SessionBase.this.onIceConnectionChange();
|
| + }
|
| + });
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Receives callbacks from the control channel. Forwards them on the session thread.
|
| + */
|
| + private final class ControlChannelObserver implements AbstractDataChannel.Observer {
|
| + @Override
|
| + public void onStateChange(final AbstractDataChannel.State state) {
|
| + postOnSessionThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (!isStarted()) return;
|
| + mControlChannelOpened = state == AbstractDataChannel.State.OPEN;
|
| +
|
| + if (mControlChannelOpened) {
|
| + onControlChannelOpened();
|
| + } else {
|
| + onControlChannelClosed();
|
| + }
|
| + }
|
| + });
|
| + }
|
| +
|
| + @Override
|
| + public void onMessage(ByteBuffer message) {
|
| + final byte[] bytes = new byte[message.remaining()];
|
| + message.get(bytes);
|
| + postOnSessionThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (!isStarted() || mControlMessageHandler == null) return;
|
| +
|
| + try {
|
| + mControlMessageHandler.readMessage(bytes);
|
| + } catch (SessionControlMessages.InvalidFormatException e) {
|
| + // TODO(serya): handle
|
| + }
|
| + }
|
| + });
|
| + }
|
| + }
|
| +
|
| + protected void sendControlMessage(SessionControlMessages.Message<?> message) {
|
| + assert mControlChannelOpened;
|
| +
|
| + byte[] bytes = SessionControlMessages.toByteArray(message);
|
| + ByteBuffer rawMessage = ByteBuffer.allocateDirect(bytes.length);
|
| + rawMessage.put(bytes);
|
| +
|
| + sendControlMessage(rawMessage);
|
| + }
|
| +
|
| + private void sendControlMessage(ByteBuffer rawMessage) {
|
| + rawMessage.limit(rawMessage.position());
|
| + rawMessage.position(0);
|
| + mControlChannel.send(rawMessage, AbstractDataChannel.MessageType.TEXT);
|
| + }
|
| +}
|
|
|