summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build/all.gyp15
-rw-r--r--build/android/findbugs_filter/findbugs_known_bugs.txt2
-rw-r--r--components/devtools_bridge.gyp5
-rw-r--r--components/devtools_bridge/DEPS4
-rw-r--r--components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/AbstractPeerConnection.java106
-rw-r--r--components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/RTCConfiguration.java62
-rw-r--r--components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/ServerSession.java178
-rw-r--r--components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionBase.java431
-rw-r--r--components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionControlMessages.java250
-rw-r--r--components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionDependencyFactory.java370
-rw-r--r--components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SocketTunnelBase.java2
-rw-r--r--components/devtools_bridge/android/javatests/AndroidManifest.xml1
-rw-r--r--components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/LocalSessionBridgeTest.java87
-rw-r--r--components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/SessionControlMessagesTest.java94
-rw-r--r--components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugActivity.java19
-rw-r--r--components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugService.java116
-rw-r--r--components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/ClientSession.java248
-rw-r--r--components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/LocalSessionBridge.java432
-rw-r--r--components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/SignalingThreadMock.java2
19 files changed, 2386 insertions, 38 deletions
diff --git a/build/all.gyp b/build/all.gyp
index f562537..b469a22 100644
--- a/build/all.gyp
+++ b/build/all.gyp
@@ -806,7 +806,6 @@
'../chrome/chrome.gyp:chrome_shell_uiautomator_tests',
'../chrome/chrome.gyp:unit_tests_apk',
'../components/components_tests.gyp:components_unittests_apk',
- '../components/devtools_bridge.gyp:devtools_bridge_tests_apk',
'../content/content_shell_and_tests.gyp:content_browsertests_apk',
'../content/content_shell_and_tests.gyp:content_gl_tests_apk',
'../content/content_shell_and_tests.gyp:content_unittests_apk',
@@ -825,6 +824,13 @@
'../ui/events/events.gyp:events_unittests_apk',
'../ui/gfx/gfx_tests.gyp:gfx_unittests_apk',
],
+ 'conditions': [
+ ['"<(libpeer_target_type)"=="static_library"', {
+ 'dependencies': [
+ '../components/devtools_bridge.gyp:devtools_bridge_tests_apk',
+ ],
+ }],
+ ],
},
{
# WebRTC Chromium tests to run on Android.
@@ -838,13 +844,6 @@
# Unit test bundles packaged as an apk.
'../content/content_shell_and_tests.gyp:content_browsertests_apk',
],
- 'conditions': [
- ['"<(libpeer_target_type)"=="static_library"', {
- 'dependencies': [
- '../third_party/libjingle/libjingle.gyp:libjingle_peerconnection_javalib',
- ],
- }],
- ],
}, # target_name: android_builder_chromium_webrtc
], # targets
}], # OS="android"
diff --git a/build/android/findbugs_filter/findbugs_known_bugs.txt b/build/android/findbugs_filter/findbugs_known_bugs.txt
index c82e62b..f641e15 100644
--- a/build/android/findbugs_filter/findbugs_known_bugs.txt
+++ b/build/android/findbugs_filter/findbugs_known_bugs.txt
@@ -22,3 +22,5 @@ M V EI: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.thumbnail
M M LI: Incorrect lazy initialization of static field org.chromium.chrome.browser.sync.ProfileSyncService.sSyncSetupManager in org.chromium.chrome.browser.sync.ProfileSyncService.get(Context) At ProfileSyncService.java
M V EI2: org.chromium.content_public.browser.LoadUrlParams.setPostData(byte[]) may expose internal representation by storing an externally mutable object into LoadUrlParams.mPostData At LoadUrlParams.java
M V EI: org.chromium.content_public.browser.LoadUrlParams.getPostData() may expose internal representation by returning LoadUrlParams.mPostData At LoadUrlParams.java
+M D NP: Read of unwritten public or protected field data in org.chromium.components.devtools_bridge.SessionDependencyFactory$DataChannelObserverAdapter.onMessage(DataChannel$Buffer) At SessionDependencyFactory.java
+M D NP: Read of unwritten public or protected field mandatory in org.chromium.components.devtools_bridge.SessionDependencyFactory.createPeerConnection(RTCConfiguration, AbstractPeerConnection$Observer) At SessionDependencyFactory.java
diff --git a/components/devtools_bridge.gyp b/components/devtools_bridge.gyp
index 9df9527..a77e31a 100644
--- a/components/devtools_bridge.gyp
+++ b/components/devtools_bridge.gyp
@@ -11,6 +11,9 @@
'java_in_dir': 'devtools_bridge/android/java',
},
'includes': [ '../build/java.gypi' ],
+ 'dependencies': [
+ '../third_party/libjingle/libjingle.gyp:libjingle_peerconnection_javalib',
+ ],
},
{
'target_name': 'devtools_bridge_testutils',
@@ -20,6 +23,7 @@
},
'includes': [ '../build/java.gypi' ],
'dependencies': [
+ '../third_party/libjingle/libjingle.gyp:libjingle_peerconnection_javalib',
'devtools_bridge_javalib',
],
},
@@ -34,6 +38,7 @@
'apk_name': 'DevToolsBridgeTest',
'test_suite_name': 'devtools_bridge_tests',
'java_in_dir': 'devtools_bridge/android/javatests',
+ 'native_lib_target': 'libjingle_peerconnection_so',
'is_test_apk': 1,
},
'includes': [ '../build/java_apk.gypi' ],
diff --git a/components/devtools_bridge/DEPS b/components/devtools_bridge/DEPS
new file mode 100644
index 0000000..9fe3283
--- /dev/null
+++ b/components/devtools_bridge/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+ "-chrome",
+ "-content",
+]
diff --git a/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/AbstractPeerConnection.java b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/AbstractPeerConnection.java
new file mode 100644
index 0000000..3e1fb6a
--- /dev/null
+++ b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/AbstractPeerConnection.java
@@ -0,0 +1,106 @@
+// 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;
+
+/**
+ * Limited view on org.webrtc.PeerConnection. Abstraction layer helps with:
+ * 1. Allows both native and Java API implementation.
+ * 2. Hides unused features.
+ * Should be accessed on a single thread.
+ */
+public abstract class AbstractPeerConnection {
+ /**
+ * All methods are callen on WebRTC signaling thread.
+ */
+ public interface Observer {
+ /**
+ * Called when createAndSetLocalDescription or setRemoteDescription failed.
+ */
+ void onFailure(String description);
+
+ /**
+ * Called when createAndSetLocalDescription succeeded.
+ */
+ void onLocalDescriptionCreatedAndSet(SessionDescriptionType type, String description);
+
+ /**
+ * Called when setRemoteDescription succeeded.
+ */
+ void onRemoteDescriptionSet();
+
+ /**
+ * New ICE candidate available. String representation defined in the IceCandidate class.
+ * To be sent to the remote peer connection.
+ */
+ void onIceCandidate(String iceCandidate);
+
+ /**
+ * Called when connected or disconnected. In disconnected state recovery procedure
+ * should only rely on signaling channel.
+ */
+ void onIceConnectionChange(boolean connected);
+ }
+
+ /**
+ * Type of session description.
+ */
+ public enum SessionDescriptionType {
+ OFFER, ANSWER
+ }
+
+ /**
+ * The result of this method will be invocation onLocalDescriptionCreatedAndSet
+ * or onFailure on the observer. Should not be called when waiting result of
+ * setRemoteDescription.
+ */
+ public abstract void createAndSetLocalDescription(SessionDescriptionType type);
+
+ /**
+ * Result of this method will be invocation onRemoteDescriptionSet or onFailure on the observer.
+ */
+ public abstract void setRemoteDescription(SessionDescriptionType type, String description);
+
+ /**
+ * Adds a remote ICE candidate.
+ */
+ public abstract void addIceCandidate(String candidate);
+
+ /**
+ * Destroys native objects. Synchronized with the signaling thread
+ * (no observer method called when the connection disposed)
+ */
+ public abstract void dispose();
+
+ /**
+ * Creates prenegotiated SCTP data channel.
+ */
+ public abstract AbstractDataChannel createDataChannel(int channelId);
+
+ /**
+ * Helper class which enforces string representation of an ICE candidate.
+ */
+ static class IceCandidate {
+ public final String sdpMid;
+ public final int sdpMLineIndex;
+ public final String sdp;
+
+ public IceCandidate(String sdpMid, int sdpMLineIndex, String sdp) {
+ this.sdpMid = sdpMid;
+ this.sdpMLineIndex = sdpMLineIndex;
+ this.sdp = sdp;
+ }
+
+ public String toString() {
+ return sdpMid + ":" + sdpMLineIndex + ":" + sdp;
+ }
+
+ public static IceCandidate fromString(String candidate) throws IllegalArgumentException {
+ String[] parts = candidate.split(":", 3);
+ if (parts.length != 3)
+ throw new IllegalArgumentException("Expected column separated list.");
+ return new IceCandidate(parts[0], Integer.parseInt(parts[1]), parts[2]);
+ }
+ }
+}
diff --git a/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/RTCConfiguration.java b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/RTCConfiguration.java
new file mode 100644
index 0000000..bb2dbb0
--- /dev/null
+++ b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/RTCConfiguration.java
@@ -0,0 +1,62 @@
+// 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.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents RTCConfiguration (http://www.w3.org/TR/webrtc/#rtcconfiguration-type).
+ * Replacement for List<PeerConnection.IceServer> in Java WebRTC API.
+ * Transferable through signaling channel.
+ * Immutable.
+ */
+public class RTCConfiguration {
+ /**
+ * Single ICE server description.
+ */
+ public static class IceServer {
+ public final String uri;
+ public final String username;
+ public final String credential;
+
+ public IceServer(String uri, String username, String credential) {
+ this.uri = uri;
+ this.username = username;
+ this.credential = credential;
+ }
+ }
+
+ public final List<IceServer> iceServers;
+
+ private RTCConfiguration(List<IceServer> iceServers) {
+ this.iceServers = Collections.unmodifiableList(new ArrayList<IceServer>(iceServers));
+ }
+
+ public RTCConfiguration() {
+ this(Collections.<IceServer>emptyList());
+ }
+
+ /**
+ * Builder for RTCConfiguration.
+ */
+ public static class Builder {
+ private final List<IceServer> mIceServers = new ArrayList<IceServer>();
+
+ public RTCConfiguration build() {
+ return new RTCConfiguration(mIceServers);
+ }
+
+ public Builder addIceServer(String uri, String username, String credential) {
+ mIceServers.add(new IceServer(uri, username, credential));
+ return this;
+ }
+
+ public Builder addIceServer(String uri) {
+ return addIceServer(uri, "", "");
+ }
+ }
+}
diff --git a/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/ServerSession.java b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/ServerSession.java
new file mode 100644
index 0000000..90c8b34
--- /dev/null
+++ b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/ServerSession.java
@@ -0,0 +1,178 @@
+// 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.util.List;
+
+/**
+ * DevTools Bridge server session. Handles connection with a ClientSession.
+ * See SessionBase description for more detais.
+ */
+public class ServerSession extends SessionBase implements SessionBase.ServerSessionInterface {
+ private NegotiationCallback mNegotiationCallback;
+ private IceExchangeCallback mIceExchangeCallback;
+ private boolean mIceEchangeRequested = false;
+
+ protected int mGatheringDelayMs = 200;
+
+ public ServerSession(SessionDependencyFactory factory,
+ Executor executor,
+ String defaultSocketName) {
+ super(factory, executor, new SocketTunnelServer(defaultSocketName));
+ }
+
+ @Override
+ public void stop() {
+ super.stop();
+ if (mNegotiationCallback != null) {
+ mNegotiationCallback.onFailure("Session stopped");
+ mNegotiationCallback = null;
+ }
+ if (mIceExchangeCallback != null) {
+ mIceExchangeCallback.onFailure("Session stopped");
+ mIceExchangeCallback = null;
+ }
+ }
+
+ @Override
+ public void startSession(RTCConfiguration config,
+ String offer,
+ NegotiationCallback callback) {
+ checkCalledOnSessionThread();
+ if (isStarted()) {
+ callback.onFailure("Session already started");
+ return;
+ }
+
+ ClientMessageHandler handler = new ClientMessageHandler();
+ start(config, handler);
+
+ negotiate(offer, callback);
+ }
+
+ @Override
+ public void renegotiate(String offer, NegotiationCallback callback) {
+ checkCalledOnSessionThread();
+ if (!isStarted()) {
+ callback.onFailure("Session is not started");
+ return;
+ }
+
+ callback.onFailure("Not implemented");
+ }
+
+ private void negotiate(String offer, NegotiationCallback callback) {
+ if (mNegotiationCallback != null) {
+ callback.onFailure("Negotiation already in progress");
+ return;
+ }
+
+ mNegotiationCallback = callback;
+ // If success will call onRemoteDescriptionSet.
+ connection().setRemoteDescription(
+ AbstractPeerConnection.SessionDescriptionType.OFFER, offer);
+ }
+
+ protected void onRemoteDescriptionSet() {
+ // If success will call onLocalDescriptionCreatedAndSet.
+ connection().createAndSetLocalDescription(
+ AbstractPeerConnection.SessionDescriptionType.ANSWER);
+ }
+
+ @Override
+ protected void onLocalDescriptionCreatedAndSet(
+ AbstractPeerConnection.SessionDescriptionType type, String description) {
+ assert type == AbstractPeerConnection.SessionDescriptionType.ANSWER;
+
+ mNegotiationCallback.onSuccess(description);
+ mNegotiationCallback = null;
+ onSessionNegotiated();
+ }
+
+ protected void onSessionNegotiated() {
+ if (!isControlChannelOpened())
+ startAutoCloseTimer();
+ }
+
+ @Override
+ public void iceExchange(List<String> clientCandidates,
+ IceExchangeCallback callback) {
+ checkCalledOnSessionThread();
+ if (!isStarted()) {
+ callback.onFailure("Session disposed");
+ return;
+ }
+
+ if (mNegotiationCallback != null || mIceExchangeCallback != null) {
+ callback.onFailure("Concurrent requests detected");
+ return;
+ }
+
+ mIceExchangeCallback = callback;
+ addIceCandidates(clientCandidates);
+
+ // Give libjingle some time for gathering ice candidates.
+ postOnSessionThread(mGatheringDelayMs, new Runnable() {
+ @Override
+ public void run() {
+ if (isStarted())
+ sendIceCandidatesBack();
+ }
+ });
+ }
+
+ private void sendIceCandidatesBack() {
+ mIceExchangeCallback.onSuccess(takeIceCandidates());
+ mIceExchangeCallback = null;
+ mIceEchangeRequested = false;
+ }
+
+ @Override
+ protected void onControlChannelOpened() {
+ stopAutoCloseTimer();
+ }
+
+ @Override
+ protected void onFailure(String message) {
+ if (mNegotiationCallback != null) {
+ mNegotiationCallback.onFailure(message);
+ mNegotiationCallback = null;
+ }
+ super.onFailure(message);
+ }
+
+ @Override
+ protected void onIceCandidate(String candidate) {
+ super.onIceCandidate(candidate);
+ if (isControlChannelOpened() && !mIceEchangeRequested) {
+ // New ICE candidate may improve connection even if control channel operable.
+ // If control channel closed client will exchange candidates anyway.
+ sendControlMessage(new SessionControlMessages.IceExchangeMessage());
+ mIceEchangeRequested = true;
+ }
+ }
+
+ protected SocketTunnelServer createSocketTunnelServer(String serverSocketName) {
+ return new SocketTunnelServer(serverSocketName);
+ }
+
+ private final class ClientMessageHandler extends SessionControlMessages.ClientMessageHandler {
+ @Override
+ protected void onMessage(SessionControlMessages.ClientMessage message) {
+ switch (message.type) {
+ case UNKNOWN_REQUEST:
+ sendControlMessage(((SessionControlMessages.UnknownRequestMessage) message)
+ .createResponse());
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void sendControlMessage(SessionControlMessages.Message<?> message) {
+ assert message instanceof SessionControlMessages.ServerMessage;
+ super.sendControlMessage(message);
+ }
+}
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 0000000..db793b0
--- /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);
+ }
+}
diff --git a/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionControlMessages.java b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionControlMessages.java
new file mode 100644
index 0000000..028af60
--- /dev/null
+++ b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionControlMessages.java
@@ -0,0 +1,250 @@
+// 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 android.util.JsonReader;
+import android.util.JsonWriter;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+/**
+ * Defines protocol of control channel of SessionBase. Messages are JSON serializable
+ * and transferred through AbstractDataChannel.
+ */
+public final class SessionControlMessages {
+ private SessionControlMessages() {
+ throw new RuntimeException("Class not intended to instantiate");
+ }
+
+ /**
+ * Types of messages that client sends to server.
+ */
+ enum ClientMessageType {
+ UNKNOWN_REQUEST
+ }
+
+ /**
+ * Types of messages that servers sends to client.
+ */
+ enum ServerMessageType {
+ ICE_EXCHANGE,
+ UNKNOWN_RESPONSE
+ }
+
+ /**
+ * Base class for all messages.
+ */
+ public abstract static class Message<T extends Enum> {
+ public final T type;
+
+ protected Message(T type) {
+ this.type = type;
+ }
+
+ public void write(JsonWriter writer) throws IOException {
+ writer.name("type");
+ writer.value(type.toString());
+ }
+ }
+
+ /**
+ * Base calss for messages that client sends to server.
+ */
+ public abstract static class ClientMessage extends Message<ClientMessageType> {
+ protected ClientMessage(ClientMessageType type) {
+ super(type);
+ }
+ }
+
+ /**
+ * Base class for messages that server sends to client.
+ */
+ public abstract static class ServerMessage extends Message<ServerMessageType> {
+ protected ServerMessage(ServerMessageType type) {
+ super(type);
+ }
+ }
+
+ /**
+ * Server sends this message when it has ICE candidates to exchange. Client initiates
+ * ICE exchange over signaling channel.
+ */
+ public static final class IceExchangeMessage extends ServerMessage {
+ public IceExchangeMessage() {
+ super(ServerMessageType.ICE_EXCHANGE);
+ }
+ }
+
+ /**
+ * Server response on unrecognized client message.
+ */
+ public static final class UnknownResponseMessage extends ServerMessage {
+ public final String rawRequestType;
+
+ public UnknownResponseMessage(String rawRequestType) {
+ super(ServerMessageType.UNKNOWN_RESPONSE);
+ this.rawRequestType = rawRequestType;
+ }
+
+ public void write(JsonWriter writer) throws IOException {
+ super.write(writer);
+ writer.name("rawRequestType");
+ writer.value(rawRequestType.toString());
+ }
+ }
+
+ /**
+ * Helper class to represent message of unknown type. Should not be sent.
+ */
+ public static final class UnknownRequestMessage extends ClientMessage {
+ public final String rawType;
+
+ public UnknownRequestMessage(String rawType) {
+ super(ClientMessageType.UNKNOWN_REQUEST);
+ this.rawType = rawType;
+ }
+
+ @Override
+ public void write(JsonWriter writer) throws IOException {
+ throw new RuntimeException("Should not be serialized");
+ }
+
+ public UnknownResponseMessage createResponse() {
+ return new UnknownResponseMessage(rawType);
+ }
+ }
+
+ private static <T extends Enum<T>> T getMessageType(
+ Class<T> enumType, String rawType, T defaultType) throws IOException {
+ try {
+ return Enum.valueOf(enumType, rawType);
+ } catch (IllegalArgumentException e) {
+ if (defaultType != null)
+ return defaultType;
+ throw new IOException("Invalid message type " + rawType);
+ }
+ }
+
+ public static void write(JsonWriter writer, Message<?> message) throws IOException {
+ writer.beginObject();
+ message.write(writer);
+ writer.endObject();
+ }
+
+ public static ClientMessage readClientMessage(JsonReader reader) throws IOException {
+ String rawType = "";
+ boolean success = false;
+
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if ("type".equals(name)) {
+ rawType = reader.nextString();
+ }
+ }
+ reader.endObject();
+
+ switch (getMessageType(ClientMessageType.class,
+ rawType,
+ ClientMessageType.UNKNOWN_REQUEST)) {
+ case UNKNOWN_REQUEST:
+ return new UnknownRequestMessage(rawType);
+ }
+ throw new IOException("Invalid message");
+ }
+
+ public static ServerMessage readServerMessage(JsonReader reader) throws IOException {
+ String rawType = "";
+ String rawRequestType = null;
+
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if ("type".equals(name)) {
+ rawType = reader.nextString();
+ } else if ("rawRequestType".equals(name)) {
+ rawRequestType = reader.nextString();
+ }
+ }
+ reader.endObject();
+
+ switch (getMessageType(ServerMessageType.class, rawType, null)) {
+ case ICE_EXCHANGE:
+ return new IceExchangeMessage();
+ case UNKNOWN_RESPONSE:
+ return new UnknownResponseMessage(rawRequestType);
+ }
+ throw new IOException("Invalid message");
+ }
+
+ /**
+ * Base class for client and server message handlers.
+ */
+ public abstract static class MessageHandler {
+ protected abstract void readMessage(JsonReader reader) throws IOException;
+
+ public boolean readMessage(byte[] bytes) throws InvalidFormatException {
+ try {
+ readMessage(new JsonReader(new InputStreamReader(new ByteArrayInputStream(bytes))));
+ return true;
+ } catch (IOException e) {
+ throw new InvalidFormatException(e);
+ }
+ }
+ }
+
+ /**
+ * Exception when parsing or handling message.
+ */
+ public static class InvalidFormatException extends IOException {
+ public InvalidFormatException(IOException e) {
+ super(e);
+ }
+
+ public InvalidFormatException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Base class for handler of server messages (to be created on client).
+ */
+ public abstract static class ServerMessageHandler extends MessageHandler {
+ @Override
+ protected void readMessage(JsonReader reader) throws IOException {
+ onMessage(readServerMessage(reader));
+ }
+
+ protected abstract void onMessage(ServerMessage message);
+ }
+
+ /**
+ * Base class for handler of client messages (to be created on server).
+ */
+ public abstract static class ClientMessageHandler extends MessageHandler {
+ @Override
+ public void readMessage(JsonReader reader) throws IOException {
+ onMessage(readClientMessage(reader));
+ }
+
+ protected abstract void onMessage(ClientMessage message);
+ }
+
+ public static byte[] toByteArray(Message<?> message) {
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ JsonWriter writer = new JsonWriter(new OutputStreamWriter(byteStream));
+ try {
+ write(writer, message);
+ writer.close();
+ return byteStream.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionDependencyFactory.java b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionDependencyFactory.java
new file mode 100644
index 0000000..c39de43
--- /dev/null
+++ b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SessionDependencyFactory.java
@@ -0,0 +1,370 @@
+// 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 org.webrtc.DataChannel;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements AbstractDataChannel and AbstractPeerConnection on top of org.webrtc.* API.
+ * Isolation is needed because some configuration of DevTools bridge may not be based on
+ * Java API. Native implementation of SessionDependencyFactory will be added for this case.
+ * In addition abstraction layer isolates SessionBase from complexity of underlying API
+ * beside used features.
+ */
+public class SessionDependencyFactory {
+ private final PeerConnectionFactory mFactory = new PeerConnectionFactory();
+
+ public AbstractPeerConnection createPeerConnection(
+ RTCConfiguration config, AbstractPeerConnection.Observer observer) {
+ MediaConstraints constraints = new MediaConstraints();
+ constraints.mandatory.add(
+ new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
+ return new PeerConnectionAdapter(
+ mFactory.createPeerConnection(convert(config), constraints,
+ new PeerConnnectionObserverAdapter(observer)), observer);
+ }
+
+ public void dispose() {
+ mFactory.dispose();
+ }
+
+ private static AbstractPeerConnection.SessionDescriptionType convertType(
+ SessionDescription.Type type) {
+ switch (type) {
+ case OFFER:
+ return AbstractPeerConnection.SessionDescriptionType.OFFER;
+ case ANSWER:
+ return AbstractPeerConnection.SessionDescriptionType.ANSWER;
+ default:
+ throw new IllegalArgumentException(type.toString());
+ }
+ }
+
+ private static SessionDescription.Type convertType(
+ AbstractPeerConnection.SessionDescriptionType type) {
+ switch (type) {
+ case OFFER:
+ return SessionDescription.Type.OFFER;
+ case ANSWER:
+ return SessionDescription.Type.ANSWER;
+ default:
+ throw new IllegalArgumentException(type.toString());
+ }
+ }
+
+ private static AbstractPeerConnection.IceCandidate convert(IceCandidate candidate) {
+ return new AbstractPeerConnection.IceCandidate(
+ candidate.sdpMid, candidate.sdpMLineIndex, candidate.sdp);
+ }
+
+ private static IceCandidate convert(AbstractPeerConnection.IceCandidate candidate) {
+ return new IceCandidate(candidate.sdpMid, candidate.sdpMLineIndex, candidate.sdp);
+ }
+
+ private static List<PeerConnection.IceServer> convert(RTCConfiguration config) {
+ List<PeerConnection.IceServer> result = new ArrayList<PeerConnection.IceServer>();
+ for (RTCConfiguration.IceServer server : config.iceServers) {
+ result.add(new PeerConnection.IceServer(
+ server.uri, server.username, server.credential));
+ }
+ return result;
+ }
+
+ public static DataChannelAdapter createDataChannel(PeerConnection connection, int channelId) {
+ DataChannel.Init init = new DataChannel.Init();
+ init.ordered = true;
+ init.negotiated = true;
+ init.id = channelId;
+ return new DataChannelAdapter(connection.createDataChannel("", init));
+ }
+
+ private static final class DataChannelAdapter extends AbstractDataChannel {
+ private final DataChannel mAdaptee;
+
+ public DataChannelAdapter(DataChannel adaptee) {
+ mAdaptee = adaptee;
+ }
+
+ @Override
+ public void dispose() {
+ mAdaptee.dispose();
+ }
+
+ @Override
+ public void close() {
+ mAdaptee.close();
+ }
+
+ @Override
+ public void send(ByteBuffer message, AbstractDataChannel.MessageType type) {
+ assert message.remaining() > 0;
+ mAdaptee.send(new DataChannel.Buffer(
+ message, type == AbstractDataChannel.MessageType.BINARY));
+ }
+
+ @Override
+ public void registerObserver(Observer observer) {
+ mAdaptee.registerObserver(new DataChannelObserverAdapter(observer, mAdaptee));
+ }
+
+ @Override
+ public void unregisterObserver() {
+ mAdaptee.unregisterObserver();
+ }
+ }
+
+ private static final class DataChannelObserverAdapter implements DataChannel.Observer {
+ private final AbstractDataChannel.Observer mAdaptee;
+ private final DataChannel mDataChannel;
+ private AbstractDataChannel.State mState = AbstractDataChannel.State.CLOSED;
+
+ public DataChannelObserverAdapter(
+ AbstractDataChannel.Observer adaptee, DataChannel dataChannel) {
+ mAdaptee = adaptee;
+ mDataChannel = dataChannel;
+ }
+
+ @Override
+ public void onStateChange() {
+ AbstractDataChannel.State state = mDataChannel.state() == DataChannel.State.OPEN ?
+ AbstractDataChannel.State.OPEN : AbstractDataChannel.State.CLOSED;
+ if (mState != state) {
+ mState = state;
+ mAdaptee.onStateChange(state);
+ }
+ }
+
+ @Override
+ public void onMessage(DataChannel.Buffer buffer) {
+ assert buffer.data.remaining() > 0;
+ mAdaptee.onMessage(buffer.data);
+ }
+ }
+
+ private abstract static class SetHandler implements SdpObserver {
+ @Override
+ public final void onCreateSuccess(SessionDescription description) {
+ assert false;
+ }
+
+ @Override
+ public final void onCreateFailure(String error) {
+ assert false;
+ }
+ }
+
+ private abstract static class CreateHandler implements SdpObserver {
+ @Override
+ public final void onSetSuccess() {
+ assert false;
+ }
+
+ @Override
+ public final void onSetFailure(String error) {
+ assert false;
+ }
+ }
+
+ private static final class CreateAndSetHandler extends CreateHandler {
+ private final PeerConnectionAdapter mConnection;
+ private final AbstractPeerConnection.Observer mObserver;
+
+ public CreateAndSetHandler(PeerConnectionAdapter connection,
+ AbstractPeerConnection.Observer observer) {
+ mConnection = connection;
+ mObserver = observer;
+ }
+
+ @Override
+ public void onCreateSuccess(final SessionDescription localDescription) {
+ mConnection.setLocalDescriptionOnSignalingThread(localDescription);
+ }
+
+ @Override
+ public void onCreateFailure(String description) {
+ mObserver.onFailure(description);
+ }
+ }
+
+ private static final class LocalSetHandler extends SetHandler {
+ private final SessionDescription mLocalDescription;
+ private final AbstractPeerConnection.Observer mObserver;
+
+ public LocalSetHandler(SessionDescription localDescription,
+ AbstractPeerConnection.Observer observer) {
+ mLocalDescription = localDescription;
+ mObserver = observer;
+ }
+
+ @Override
+ public void onSetSuccess() {
+ mObserver.onLocalDescriptionCreatedAndSet(
+ convertType(mLocalDescription.type), mLocalDescription.description);
+ }
+
+ @Override
+ public void onSetFailure(String description) {
+ mObserver.onFailure(description);
+ }
+ }
+
+ private static final class SetRemoteDescriptionHandler extends SetHandler {
+ private final AbstractPeerConnection.Observer mObserver;
+
+ public SetRemoteDescriptionHandler(AbstractPeerConnection.Observer observer) {
+ mObserver = observer;
+ }
+
+ @Override
+ public void onSetSuccess() {
+ mObserver.onRemoteDescriptionSet();
+ }
+
+ @Override
+ public void onSetFailure(String description) {
+ mObserver.onFailure(description);
+ }
+ }
+
+ private static final class PeerConnectionAdapter extends AbstractPeerConnection {
+ private PeerConnection mAdaptee;
+ private final Observer mObserver;
+
+ // Only access from signaling thread and disposing need synchronization.
+ private final Object mDisposeLock = new Object();
+
+ public PeerConnectionAdapter(PeerConnection adaptee, Observer observer) {
+ mAdaptee = adaptee;
+ mObserver = observer;
+ }
+
+ public void setLocalDescriptionOnSignalingThread(SessionDescription description) {
+ synchronized (mDisposeLock) {
+ if (mAdaptee == null)
+ return;
+
+ mAdaptee.setLocalDescription(
+ new LocalSetHandler(description, mObserver), description);
+ }
+ }
+
+ @Override
+ public void createAndSetLocalDescription(SessionDescriptionType type) {
+ CreateAndSetHandler handler = new CreateAndSetHandler(this, mObserver);
+ switch (type) {
+ case OFFER:
+ mAdaptee.createOffer(handler, new MediaConstraints());
+ break;
+
+ case ANSWER:
+ mAdaptee.createAnswer(handler, new MediaConstraints());
+ break;
+
+ default:
+ assert false;
+ }
+ }
+
+ @Override
+ public void setRemoteDescription(SessionDescriptionType type, String description) {
+ mAdaptee.setRemoteDescription(new SetRemoteDescriptionHandler(mObserver),
+ new SessionDescription(convertType(type), description));
+ }
+
+ @Override
+ public void addIceCandidate(String candidate) {
+ mAdaptee.addIceCandidate(convert(
+ AbstractPeerConnection.IceCandidate.fromString(candidate)));
+ }
+
+ @Override
+ public void dispose() {
+ synchronized (mDisposeLock) {
+ mAdaptee.dispose();
+ mAdaptee = null;
+ }
+ }
+
+ @Override
+ public AbstractDataChannel createDataChannel(int channelId) {
+ DataChannel.Init init = new DataChannel.Init();
+ init.ordered = true;
+ init.negotiated = true;
+ init.id = channelId;
+ return new DataChannelAdapter(mAdaptee.createDataChannel("", init));
+ }
+ }
+
+ private static final class PeerConnnectionObserverAdapter implements PeerConnection.Observer {
+ private final AbstractPeerConnection.Observer mAdaptee;
+ private boolean mConnected = false;
+
+ public PeerConnnectionObserverAdapter(AbstractPeerConnection.Observer adaptee) {
+ mAdaptee = adaptee;
+ }
+
+ @Override
+ public void onIceCandidate(IceCandidate candidate) {
+ mAdaptee.onIceCandidate(convert(candidate).toString());
+ }
+
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState newState) {}
+
+ @Override
+ public void onIceConnectionChange(PeerConnection.IceConnectionState newState) {
+ boolean connected = isConnected(newState);
+ if (mConnected != connected) {
+ mConnected = connected;
+ mAdaptee.onIceConnectionChange(connected);
+ }
+ }
+
+ private static boolean isConnected(PeerConnection.IceConnectionState newState) {
+ switch (newState) {
+ case CONNECTED:
+ case COMPLETED:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onIceGatheringChange(PeerConnection.IceGatheringState newState) {}
+
+ @Override
+ public void onDataChannel(DataChannel dataChannel) {
+ // Remote peer added non-prenegotiated data channel. It's not supported.
+ dataChannel.dispose();
+ }
+
+ @Override
+ public void onAddStream(MediaStream stream) {}
+
+ @Override
+ public void onRemoveStream(MediaStream stream) {}
+
+ @Override
+ public void onRenegotiationNeeded() {
+ }
+
+ @Override
+ public void onError() {
+ assert false; // TODO(serya): add meaningful handling strategy.
+ }
+ }
+}
diff --git a/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SocketTunnelBase.java b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SocketTunnelBase.java
index 7c1439a5..e0ca732 100644
--- a/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SocketTunnelBase.java
+++ b/components/devtools_bridge/android/java/src/org/chromium/components/devtools_bridge/SocketTunnelBase.java
@@ -40,7 +40,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
* ID is safe to be reused.
*/
public abstract class SocketTunnelBase {
- // Data channel is threadsafe but access to the reference needs synchromization.
+ // Data channel is threadsafe but access to the reference needs synchronization.
private final ReadWriteLock mDataChanneliReferenceLock = new ReentrantReadWriteLock();
private volatile AbstractDataChannel mDataChannel;
diff --git a/components/devtools_bridge/android/javatests/AndroidManifest.xml b/components/devtools_bridge/android/javatests/AndroidManifest.xml
index 2478af1..1b2bbeb 100644
--- a/components/devtools_bridge/android/javatests/AndroidManifest.xml
+++ b/components/devtools_bridge/android/javatests/AndroidManifest.xml
@@ -27,6 +27,7 @@
<uses-permission android:name="android.permission.RUN_INSTRUMENTATION" />
<uses-permission android:name="android.permission.INJECT_EVENTS" />
+ <uses-permission android:name="android.permission.INTERNET" />
<!-- For manual testing with Chrome Shell -->
<uses-permission android:name="org.chromium.chrome.shell.permission.DEBUG" />
diff --git a/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/LocalSessionBridgeTest.java b/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/LocalSessionBridgeTest.java
new file mode 100644
index 0000000..222c285
--- /dev/null
+++ b/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/LocalSessionBridgeTest.java
@@ -0,0 +1,87 @@
+// 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 android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.Future;
+
+/**
+ * Tests for both client and server sessions bound with {@link LocalSessionBridge}.
+ */
+public class LocalSessionBridgeTest extends InstrumentationTestCase {
+ private static final String SERVER_SOCKET_NAME =
+ "org.chromium.components.devtools_bridge.LocalSessionBridgeTest.SERVER_SOCKET";
+ private static final String CLIENT_SOCKET_NAME =
+ "org.chromium.components.devtools_bridge.LocalSessionBridgeTest.CLIENT_SOCKET";
+
+ private static final String REQUEST = "Request";
+ private static final String RESPONSE = "Response";
+
+ private LocalSessionBridge mBridge;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mBridge = new LocalSessionBridge(SERVER_SOCKET_NAME, CLIENT_SOCKET_NAME);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mBridge.dispose();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testDisposeAfeterStart() {
+ mBridge.start();
+ }
+
+ @MediumTest
+ public void testNegotiating() throws InterruptedException {
+ mBridge.start();
+ mBridge.awaitNegotiated();
+ }
+
+ @MediumTest
+ public void testOpenControlChannel() throws InterruptedException {
+ mBridge.start();
+ mBridge.awaitControlChannelOpened();
+ }
+
+ @MediumTest
+ public void testClientAutocloseTimeout() throws InterruptedException {
+ mBridge.setMessageDeliveryDelayMs(1000);
+ mBridge.setClientAutoCloseTimeoutMs(100);
+ mBridge.start();
+ mBridge.awaitClientAutoClosed();
+ }
+
+ @MediumTest
+ public void testServerAutocloseTimeout() throws InterruptedException {
+ mBridge.setMessageDeliveryDelayMs(1000);
+ mBridge.setServerAutoCloseTimeoutMs(100);
+ mBridge.start();
+ mBridge.awaitServerAutoClosed();
+ }
+
+ @MediumTest
+ public void testRequestResponse() throws Exception {
+ mBridge.start();
+
+ LocalServerSocket serverListeningSocket = new LocalServerSocket(SERVER_SOCKET_NAME);
+ Future<String> response = TestUtils.asyncRequest(CLIENT_SOCKET_NAME, REQUEST);
+ LocalSocket serverSocket = serverListeningSocket.accept();
+ String request = TestUtils.readAll(serverSocket);
+ Assert.assertEquals(REQUEST, request);
+ TestUtils.writeAndShutdown(serverSocket, RESPONSE);
+ Assert.assertEquals(RESPONSE, response.get());
+ }
+}
diff --git a/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/SessionControlMessagesTest.java b/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/SessionControlMessagesTest.java
new file mode 100644
index 0000000..a318168
--- /dev/null
+++ b/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/SessionControlMessagesTest.java
@@ -0,0 +1,94 @@
+// 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 android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.Assert;
+
+import org.chromium.components.devtools_bridge.SessionControlMessages.ClientMessage;
+import org.chromium.components.devtools_bridge.SessionControlMessages.ClientMessageHandler;
+import org.chromium.components.devtools_bridge.SessionControlMessages.IceExchangeMessage;
+import org.chromium.components.devtools_bridge.SessionControlMessages.InvalidFormatException;
+import org.chromium.components.devtools_bridge.SessionControlMessages.Message;
+import org.chromium.components.devtools_bridge.SessionControlMessages.MessageHandler;
+import org.chromium.components.devtools_bridge.SessionControlMessages.ServerMessage;
+import org.chromium.components.devtools_bridge.SessionControlMessages.ServerMessageHandler;
+import org.chromium.components.devtools_bridge.SessionControlMessages.UnknownRequestMessage;
+import org.chromium.components.devtools_bridge.SessionControlMessages.UnknownResponseMessage;
+
+/**
+ * Tests for {@link SessionControlMessages}
+ */
+public class SessionControlMessagesTest extends InstrumentationTestCase {
+ private static final String UNKNOWN_REQUEST_TYPE = "@unknown request@";
+ private static final String UNKNOWN_REQUEST = "{\"type\": \"" + UNKNOWN_REQUEST_TYPE + "\"}";
+
+ @SmallTest
+ public void testIceExchangeMessage() {
+ recode(new IceExchangeMessage());
+ }
+
+ @SmallTest
+ public void testUnknownRequest() throws InvalidFormatException {
+ UnknownRequestMessage request =
+ (UnknownRequestMessage) ClientMessageReader.readMessage(UNKNOWN_REQUEST);
+ UnknownResponseMessage response = ServerMessageReader.recode(request.createResponse());
+ Assert.assertEquals(UNKNOWN_REQUEST_TYPE, response.rawRequestType);
+ }
+
+ private <T extends ServerMessage> T recode(T message) {
+ assert message != null;
+ return ServerMessageReader.recode(message);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> T cast(T prototype, Object object) {
+ Assert.assertNotNull(object);
+ if (prototype.getClass() == object.getClass())
+ return (T) object;
+ else
+ throw new ClassCastException();
+ }
+
+ private static void checkedRead(MessageHandler handler, Message<?> message) {
+ try {
+ handler.readMessage(SessionControlMessages.toByteArray(message));
+ } catch (InvalidFormatException e) {
+ Assert.fail(e.toString());
+ }
+ }
+
+ private static class ServerMessageReader extends ServerMessageHandler {
+ private ServerMessage mLastMessage;
+
+ @Override
+ protected void onMessage(ServerMessage message) {
+ mLastMessage = message;
+ }
+
+ public static <T extends ServerMessage> T recode(T message) {
+ ServerMessageReader handler = new ServerMessageReader();
+ checkedRead(handler, message);
+ return cast(message, handler.mLastMessage);
+ }
+ }
+
+ private static class ClientMessageReader extends ClientMessageHandler {
+ private ClientMessage mLastMessage;
+
+ @Override
+ protected void onMessage(ClientMessage message) {
+ mLastMessage = message;
+ }
+
+ public static ClientMessage readMessage(String json) throws InvalidFormatException {
+ ClientMessageReader reader = new ClientMessageReader();
+ reader.readMessage(json.getBytes());
+ return reader.mLastMessage;
+ }
+ }
+}
diff --git a/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugActivity.java b/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugActivity.java
index 4e769ec..7d3bb83 100644
--- a/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugActivity.java
+++ b/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugActivity.java
@@ -38,21 +38,22 @@ public class DebugActivity extends Activity {
textView.setText(intro);
mLayout.addView(textView);
- Button startButton = new Button(this);
- startButton.setText("Start LocalTunnelBridge");
- startButton.setOnClickListener(new SendActionOnClickListener(DebugService.START_ACTION));
- mLayout.addView(startButton);
-
- Button stopButton = new Button(this);
- stopButton.setText("Stop");
- stopButton.setOnClickListener(new SendActionOnClickListener(DebugService.STOP_ACTION));
- mLayout.addView(stopButton);
+ addActionButton("Start LocalTunnelBridge", DebugService.START_TUNNEL_BRIDGE_ACTION);
+ addActionButton("Start LocalSessionBridge", DebugService.START_SESSION_BRIDGE_ACTION);
+ addActionButton("Stop", DebugService.STOP_ACTION);
LayoutParams layoutParam = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
setContentView(mLayout, layoutParam);
}
+ private void addActionButton(String text, String action) {
+ Button button = new Button(this);
+ button.setText(text);
+ button.setOnClickListener(new SendActionOnClickListener(action));
+ mLayout.addView(button);
+ }
+
private class SendActionOnClickListener implements View.OnClickListener {
private final String mAction;
diff --git a/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugService.java b/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugService.java
index 8d23016..57f93f84 100644
--- a/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugService.java
+++ b/components/devtools_bridge/android/javatests/src/org/chromium/components/devtools_bridge/tests/DebugService.java
@@ -13,6 +13,7 @@ import android.os.IBinder;
import android.os.Process;
import android.widget.Toast;
+import org.chromium.components.devtools_bridge.LocalSessionBridge;
import org.chromium.components.devtools_bridge.LocalTunnelBridge;
import java.io.IOException;
@@ -22,15 +23,86 @@ import java.io.IOException;
*/
public class DebugService extends Service {
private static final String PACKAGE = "org.chromium.components.devtools_bridge.tests";
- public static final String START_ACTION = PACKAGE + ".START_ACTION";
+ public static final String START_TUNNEL_BRIDGE_ACTION =
+ PACKAGE + ".START_TUNNEL_BRIDGE_ACTION";
+ public static final String START_SESSION_BRIDGE_ACTION =
+ PACKAGE + ".START_SESSION_BRIDGE_ACTION";
public static final String STOP_ACTION = PACKAGE + ".STOP_ACTION";
private static final int NOTIFICATION_ID = 1;
- private LocalTunnelBridge mBridge;
+ private Controller mRunningController;
- private LocalTunnelBridge createBridge() throws IOException {
- String exposingSocketName = "webview_devtools_remote_" + Integer.valueOf(Process.myPid());
- return new LocalTunnelBridge("chrome_shell_devtools_remote", exposingSocketName);
+ private interface Controller {
+ void create() throws IOException;
+ void start() throws Exception;
+ void stop();
+ void dispose();
+ }
+
+ private String replicatingSocketName() {
+ return "chrome_shell_devtools_remote";
+ }
+
+ private String exposingSocketName() {
+ return "webview_devtools_remote_" + Integer.valueOf(Process.myPid());
+ }
+
+ private class LocalTunnelBridgeController implements Controller {
+ private LocalTunnelBridge mBridge;
+
+ @Override
+ public void create() throws IOException {
+ mBridge = new LocalTunnelBridge(replicatingSocketName(), exposingSocketName());
+ }
+
+ @Override
+ public void start() throws Exception {
+ mBridge.start();
+ }
+
+ @Override
+ public void stop() {
+ mBridge.stop();
+ }
+
+ @Override
+ public void dispose() {
+ mBridge.dispose();
+ }
+
+ @Override
+ public String toString() {
+ return "LocalTunnelBridge";
+ }
+ }
+
+ private class LocalSessionBridgeController implements Controller {
+ private LocalSessionBridge mBridge;
+
+ @Override
+ public void create() throws IOException {
+ mBridge = new LocalSessionBridge(replicatingSocketName(), exposingSocketName());
+ }
+
+ @Override
+ public void start() {
+ mBridge.start();
+ }
+
+ @Override
+ public void stop() {
+ mBridge.stop();
+ }
+
+ @Override
+ public void dispose() {
+ mBridge.dispose();
+ }
+
+ @Override
+ public String toString() {
+ return "LocalSessionBridge";
+ }
}
@Override
@@ -38,42 +110,48 @@ public class DebugService extends Service {
if (intent == null) return START_NOT_STICKY;
String action = intent.getAction();
- if (START_ACTION.equals(action)) {
- return start();
+ if (START_TUNNEL_BRIDGE_ACTION.equals(action)) {
+ return start(new LocalTunnelBridgeController());
+ } else if (START_SESSION_BRIDGE_ACTION.equals(action)) {
+ return start(new LocalSessionBridgeController());
} else if (STOP_ACTION.equals(action)) {
return stop();
}
return START_NOT_STICKY;
}
- private int start() {
- if (mBridge != null)
+ private int start(Controller controller) {
+ if (mRunningController != null) {
+ Toast.makeText(this, "Already started", Toast.LENGTH_SHORT).show();
return START_NOT_STICKY;
+ }
try {
- mBridge = createBridge();
- mBridge.start();
+ controller.create();
+ controller.start();
} catch (Exception e) {
+ e.printStackTrace();
Toast.makeText(this, "Failed to start", Toast.LENGTH_SHORT).show();
- mBridge.dispose();
- mBridge = null;
+ controller.dispose();
return START_NOT_STICKY;
}
+ mRunningController = controller;
startForeground(NOTIFICATION_ID, makeForegroundServiceNotification());
- Toast.makeText(this, "Service started", Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, controller.toString() + " started", Toast.LENGTH_SHORT).show();
return START_STICKY;
}
private int stop() {
- if (mBridge == null)
+ if (mRunningController == null)
return START_NOT_STICKY;
- mBridge.stop();
- mBridge.dispose();
- mBridge = null;
+ String name = mRunningController.toString();
+ mRunningController.stop();
+ mRunningController.dispose();
+ mRunningController = null;
stopSelf();
- Toast.makeText(this, "Service stopped", Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, name + " stopped", Toast.LENGTH_SHORT).show();
return START_NOT_STICKY;
}
diff --git a/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/ClientSession.java b/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/ClientSession.java
new file mode 100644
index 0000000..dfc1648
--- /dev/null
+++ b/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/ClientSession.java
@@ -0,0 +1,248 @@
+// 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.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Client session. Creates client socket tunnel for clientSocketName as a default tunnel.
+ * See SessionBase for details.
+ */
+public class ClientSession extends SessionBase {
+ private final ServerSessionInterface mServer;
+ private RTCConfiguration mConfig;
+ private Cancellable mIceExchangeTask;
+ private boolean mIceExchangeRequested = false;
+ private IceExchangeHandler mPendingIceExchangeRequest;
+
+ private int mExchangeDelayMs = -1;
+
+ protected int mInitialIceExchangeDelayMs = 200;
+ protected int mMaxIceExchangeDelayMs = 5000;
+
+ private final Map<Integer, SocketTunnelClient> mPendingTunnel =
+ new HashMap<Integer, SocketTunnelClient>();
+
+ public ClientSession(SessionDependencyFactory factory,
+ Executor executor,
+ ServerSessionInterface server,
+ String clientSocketName) throws IOException {
+ super(factory, executor, new SocketTunnelClient(clientSocketName));
+ mServer = server;
+ }
+
+ public void start(RTCConfiguration config) {
+ checkCalledOnSessionThread();
+
+ super.start(config, new ServerMessageHandler());
+ mConfig = config;
+
+ for (Map.Entry<Integer, SocketTunnelClient> entry : mPendingTunnel.entrySet()) {
+ int channelId = entry.getKey();
+ entry.getValue().bind(connection().createDataChannel(channelId));
+ }
+
+ connection().createAndSetLocalDescription(
+ AbstractPeerConnection.SessionDescriptionType.OFFER);
+ }
+
+ @Override
+ public void stop() {
+ for (SocketTunnelClient tunnel : mPendingTunnel.values())
+ tunnel.unbind().dispose();
+
+ if (mIceExchangeTask != null)
+ mIceExchangeTask.cancel();
+
+ super.stop();
+ }
+
+ @Override
+ protected void onLocalDescriptionCreatedAndSet(
+ AbstractPeerConnection.SessionDescriptionType type, String offer) {
+ assert type == AbstractPeerConnection.SessionDescriptionType.OFFER;
+ mServer.startSession(mConfig, offer, new CreateSessionHandler());
+ mConfig = null;
+ }
+
+ private void onAnswerReceived(String answer) {
+ connection().setRemoteDescription(
+ AbstractPeerConnection.SessionDescriptionType.ANSWER, answer);
+ }
+
+ @Override
+ protected void onRemoteDescriptionSet() {
+ onSessionNegotiated();
+ }
+
+ protected void onSessionNegotiated() {
+ assert !isIceExchangeStarted();
+ updateIceExchangeStatus();
+ assert isIceExchangeStarted();
+ }
+
+ @Override
+ protected void onControlChannelOpened() {
+ assert isIceExchangeStarted();
+ updateIceExchangeStatus();
+ }
+
+ @Override
+ protected void onIceConnectionChange() {
+ super.onIceConnectionChange();
+ updateIceExchangeStatus();
+ }
+
+ private void updateIceExchangeStatus() {
+ boolean needed = !isConnected() || !isControlChannelOpened();
+ if (needed == isIceExchangeStarted())
+ return;
+ if (needed)
+ startIceExchange();
+ else
+ stopIceExchange();
+ }
+
+ private boolean isIceExchangeStarted() {
+ return mExchangeDelayMs >= 0;
+ }
+
+ private void startIceExchange() {
+ assert !isIceExchangeStarted();
+ mExchangeDelayMs = mInitialIceExchangeDelayMs;
+ startAutoCloseTimer();
+
+ if (!isIceExchangeScheduledOrPending()) {
+ scheduleIceExchange(mExchangeDelayMs);
+ }
+
+ assert isIceExchangeScheduledOrPending();
+ assert isIceExchangeStarted();
+ }
+
+ private void stopIceExchange() {
+ assert isIceExchangeStarted();
+ mExchangeDelayMs = -1;
+ stopAutoCloseTimer();
+
+ // Last exchange will happen, not more will be scheduled (unless mIceExchangeRequested).
+ assert isIceExchangeScheduledOrPending();
+ assert !isIceExchangeStarted();
+ }
+
+ private void scheduleIceExchange(int delay) {
+ assert mIceExchangeTask == null;
+ mIceExchangeTask = postOnSessionThread(delay, new Runnable() {
+ @Override
+ public void run() {
+ mIceExchangeTask = null;
+
+ mServer.iceExchange(takeIceCandidates(), new IceExchangeHandler());
+ mIceExchangeRequested = false;
+ }
+ });
+ }
+
+ private boolean isIceExchangeScheduledOrPending() {
+ return mIceExchangeTask != null || mPendingIceExchangeRequest != null;
+ }
+
+ private void onServerCandidates(List<String> serverCandidates) {
+ addIceCandidates(serverCandidates);
+
+ if (isIceExchangeStarted()) {
+ mExchangeDelayMs *= 2;
+ if (mExchangeDelayMs > mMaxIceExchangeDelayMs) {
+ mExchangeDelayMs = mMaxIceExchangeDelayMs;
+ }
+
+ scheduleIceExchange(mExchangeDelayMs);
+ } else if (mIceExchangeRequested) {
+ scheduleIceExchange(mInitialIceExchangeDelayMs);
+ }
+ }
+
+ /**
+ * Queries single ICE eqchange cycle regardless of ICE exchange process.
+ */
+ private void queryIceExchange() {
+ mIceExchangeRequested = true;
+ if (mIceExchangeTask == null && mPendingIceExchangeRequest != null) {
+ assert !isIceExchangeStarted();
+ scheduleIceExchange(mInitialIceExchangeDelayMs);
+ }
+ }
+
+ private final class CreateSessionHandler implements NegotiationCallback {
+ @Override
+ public void onSuccess(String answer) {
+ checkCalledOnSessionThread();
+
+ onAnswerReceived(answer);
+ }
+
+ @Override
+ public void onFailure(String message) {
+ checkCalledOnSessionThread();
+
+ ClientSession.this.onFailure(message);
+ }
+ }
+
+ private final class IceExchangeHandler implements IceExchangeCallback {
+ public IceExchangeHandler() {
+ assert mPendingIceExchangeRequest == null;
+ mPendingIceExchangeRequest = this;
+ }
+
+ @Override
+ public void onSuccess(List<String> serverCandidates) {
+ checkCalledOnSessionThread();
+
+ mPendingIceExchangeRequest = null;
+ if (isStarted()) {
+ onServerCandidates(serverCandidates);
+ }
+ }
+
+ @Override
+ public void onFailure(String message) {
+ checkCalledOnSessionThread();
+
+ mPendingIceExchangeRequest = null;
+ if (isStarted()) {
+ ClientSession.this.onFailure(message);
+ }
+ }
+ }
+
+ private final class ServerMessageHandler extends SessionControlMessages.ServerMessageHandler {
+ @Override
+ protected void onMessage(SessionControlMessages.ServerMessage message) {
+ switch (message.type) {
+ case ICE_EXCHANGE:
+ queryIceExchange();
+ break;
+
+ case UNKNOWN_RESPONSE:
+ onUnknownResponse((SessionControlMessages.UnknownResponseMessage) message);
+ break;
+ }
+ }
+ }
+
+ private void onUnknownResponse(SessionControlMessages.UnknownResponseMessage message) {
+ // TODO(serya): Handle server version incompatibility.
+ }
+
+ @Override
+ protected void sendControlMessage(SessionControlMessages.Message<?> message) {
+ assert message instanceof SessionControlMessages.ClientMessage;
+ super.sendControlMessage(message);
+ }
+}
diff --git a/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/LocalSessionBridge.java b/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/LocalSessionBridge.java
new file mode 100644
index 0000000..d851abb
--- /dev/null
+++ b/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/LocalSessionBridge.java
@@ -0,0 +1,432 @@
+// 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 android.util.Log;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper class designated for automatic and manual testing. Creates a pair of ClientSession and
+ * ServerSession on separate and threads binds them through and adapters that makes call the calls
+ * on correct threads (no serialization needed for communication).
+ */
+public class LocalSessionBridge {
+ private static final String TAG = "LocalSessionBridge";
+
+ private volatile int mDelayMs = 0;
+
+ private final SessionDependencyFactory mFactory = new SessionDependencyFactory();
+
+ private final ThreadedExecutor mServerExecutor = new ThreadedExecutor();
+ private final ThreadedExecutor mClientExecutor = new ThreadedExecutor();
+
+ private final ServerSessionMock mServerSession;
+ private final ClientSessionMock mClientSession;
+
+ private boolean mStarted = false;
+
+ private final CountDownLatch mNegotiated = new CountDownLatch(2);
+ private final CountDownLatch mControlChannelOpened = new CountDownLatch(2);
+ private final CountDownLatch mClientAutoClosed = new CountDownLatch(1);
+ private final CountDownLatch mServerAutoClosed = new CountDownLatch(1);
+ private final CountDownLatch mTunnelConfirmed = new CountDownLatch(1);
+
+ private int mServerAutoCloseTimeoutMs = -1;
+ private int mClientAutoCloseTimeoutMs = -1;
+
+ public LocalSessionBridge(String serverSocketName, String clientSocketName) throws IOException {
+ mServerSession = new ServerSessionMock(serverSocketName);
+ mClientSession = new ClientSessionMock(mServerSession, clientSocketName);
+ }
+
+ public void setMessageDeliveryDelayMs(int value) {
+ mDelayMs = value;
+ }
+
+ void setClientAutoCloseTimeoutMs(int value) {
+ assert !isStarted();
+
+ mClientAutoCloseTimeoutMs = value;
+ }
+
+ void setServerAutoCloseTimeoutMs(int value) {
+ assert !isStarted();
+
+ mServerAutoCloseTimeoutMs = value;
+ }
+
+ public void dispose() {
+ if (isStarted()) stop();
+
+ mServerExecutor.dispose();
+ mClientExecutor.dispose();
+ mFactory.dispose();
+ }
+
+ public void start() {
+ start(new RTCConfiguration());
+ }
+
+ public void start(final RTCConfiguration config) {
+ if (mServerAutoCloseTimeoutMs >= 0)
+ mServerSession.setAutoCloseTimeoutMs(mServerAutoCloseTimeoutMs);
+ if (mClientAutoCloseTimeoutMs >= 0)
+ mClientSession.setAutoCloseTimeoutMs(mClientAutoCloseTimeoutMs);
+ mClientExecutor.runSynchronously(new Runnable() {
+ @Override
+ public void run() {
+ mClientSession.start(config);
+ }
+ });
+ mStarted = true;
+ }
+
+ private boolean isStarted() {
+ return mStarted;
+ }
+
+ public void stop() {
+ mServerExecutor.runSynchronously(new Runnable() {
+ @Override
+ public void run() {
+ mServerSession.dispose();
+ }
+ });
+ mClientExecutor.runSynchronously(new Runnable() {
+ @Override
+ public void run() {
+ mClientSession.dispose();
+ }
+ });
+ mStarted = false;
+ }
+
+ public void awaitNegotiated() throws InterruptedException {
+ mNegotiated.await();
+ }
+
+ public void awaitControlChannelOpened() throws InterruptedException {
+ mControlChannelOpened.await();
+ }
+
+ public void awaitClientAutoClosed() throws InterruptedException {
+ mClientAutoClosed.await();
+ }
+
+ public void awaitServerAutoClosed() throws InterruptedException {
+ mServerAutoClosed.await();
+ }
+
+ private class ServerSessionMock extends ServerSession {
+ public ServerSessionMock(String serverSocketName) {
+ super(mFactory, mServerExecutor, serverSocketName);
+ }
+
+ public void setAutoCloseTimeoutMs(int value) {
+ mAutoCloseTimeoutMs = value;
+ }
+
+ @Override
+ protected void onSessionNegotiated() {
+ Log.d(TAG, "Server negotiated");
+ mNegotiated.countDown();
+ super.onSessionNegotiated();
+ }
+
+ @Override
+ protected void onControlChannelOpened() {
+ Log.d(TAG, "Server's control channel opened");
+ super.onControlChannelOpened();
+ mControlChannelOpened.countDown();
+ }
+
+ @Override
+ protected void onIceCandidate(String candidate) {
+ Log.d(TAG, "Server's ICE candidate: " + candidate);
+ super.onIceCandidate(candidate);
+ }
+
+ @Override
+ protected void closeSelf() {
+ Log.d(TAG, "Server autoclosed");
+ super.closeSelf();
+ mServerAutoClosed.countDown();
+ }
+
+ @Override
+ protected SocketTunnelServer createSocketTunnelServer(String serverSocketName) {
+ SocketTunnelServer tunnel = super.createSocketTunnelServer(serverSocketName);
+ Log.d(TAG, "Server tunnel created on " + serverSocketName);
+ return tunnel;
+ }
+ }
+
+ private class ClientSessionMock extends ClientSession {
+ public ClientSessionMock(ServerSession serverSession, String clientSocketName)
+ throws IOException {
+ super(mFactory,
+ mClientExecutor,
+ createServerSessionProxy(serverSession),
+ clientSocketName);
+ }
+
+ public void setAutoCloseTimeoutMs(int value) {
+ mAutoCloseTimeoutMs = value;
+ }
+
+ @Override
+ protected void onSessionNegotiated() {
+ Log.d(TAG, "Client negotiated");
+ mNegotiated.countDown();
+ super.onSessionNegotiated();
+ }
+
+ @Override
+ protected void onControlChannelOpened() {
+ Log.d(TAG, "Client's control channel opened");
+ super.onControlChannelOpened();
+ mControlChannelOpened.countDown();
+ }
+
+ @Override
+ protected void onIceCandidate(String candidate) {
+ Log.d(TAG, "Client's ICE candidate: " + candidate);
+ super.onIceCandidate(candidate);
+ }
+
+ @Override
+ protected void closeSelf() {
+ Log.d(TAG, "Client autoclosed");
+ super.closeSelf();
+ mClientAutoClosed.countDown();
+ }
+ }
+
+ /**
+ * Implementation of SessionBase.Executor on top of ScheduledExecutorService.
+ */
+ public static final class ThreadedExecutor implements SessionBase.Executor {
+ private final ScheduledExecutorService mExecutor =
+ Executors.newSingleThreadScheduledExecutor();
+ private final AtomicReference<Thread> mSessionThread = new AtomicReference<Thread>();
+
+ @Override
+ public SessionBase.Cancellable postOnSessionThread(int delayMs, Runnable runnable) {
+ return new CancellableFuture(mExecutor.schedule(
+ new SessionThreadRunner(runnable), delayMs, TimeUnit.MILLISECONDS));
+ }
+
+ @Override
+ public boolean isCalledOnSessionThread() {
+ return Thread.currentThread() == mSessionThread.get();
+ }
+
+ public void runSynchronously(Runnable runnable) {
+ try {
+ mExecutor.submit(new SessionThreadRunner(runnable)).get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void dispose() {
+ mExecutor.shutdownNow();
+ }
+
+ private class SessionThreadRunner implements Runnable {
+ private final Runnable mRunnable;
+
+ public SessionThreadRunner(Runnable runnable) {
+ mRunnable = runnable;
+ }
+
+ @Override
+ public void run() {
+ Thread thread = mSessionThread.getAndSet(Thread.currentThread());
+ assert thread == null;
+ mRunnable.run();
+ thread = mSessionThread.getAndSet(null);
+ assert thread == Thread.currentThread();
+ }
+ }
+ }
+
+ private static final class CancellableFuture implements SessionBase.Cancellable {
+ private final ScheduledFuture<?> mFuture;
+
+ public CancellableFuture(ScheduledFuture<?> future) {
+ mFuture = future;
+ }
+
+ @Override
+ public void cancel() {
+ mFuture.cancel(false);
+ }
+ }
+
+ private ServerSessionProxy createServerSessionProxy(SessionBase.ServerSessionInterface proxee) {
+ return new ServerSessionProxy(mServerExecutor, mClientExecutor, proxee, mDelayMs);
+ }
+
+ /**
+ * Helper proxy that binds client and server sessions living on different executors.
+ * Exchange java objects instead of serialized messages.
+ */
+ public static final class ServerSessionProxy implements SessionBase.ServerSessionInterface {
+ private final SessionBase.ServerSessionInterface mProxee;
+ private final SessionBase.Executor mServerExecutor;
+ private final SessionBase.Executor mClientExecutor;
+ private final int mDelayMs;
+
+ public ServerSessionProxy(
+ SessionBase.Executor serverExecutor, SessionBase.Executor clientExecutor,
+ SessionBase.ServerSessionInterface proxee, int delayMs) {
+ mServerExecutor = serverExecutor;
+ mClientExecutor = clientExecutor;
+ mProxee = proxee;
+ mDelayMs = delayMs;
+ }
+
+ public SessionBase.Executor serverExecutor() {
+ return mServerExecutor;
+ }
+
+ public SessionBase.Executor clientExecutor() {
+ return mClientExecutor;
+ }
+
+ @Override
+ public void startSession(final RTCConfiguration config,
+ final String offer,
+ final SessionBase.NegotiationCallback callback) {
+ Log.d(TAG, "Starting session: " + offer);
+ mServerExecutor.postOnSessionThread(mDelayMs, new Runnable() {
+ @Override
+ public void run() {
+ mProxee.startSession(config, offer, wrap(callback));
+ }
+ });
+ }
+
+ @Override
+ public void renegotiate(final String offer,
+ final SessionBase.NegotiationCallback callback) {
+ Log.d(TAG, "Renegotiation: " + offer);
+ mServerExecutor.postOnSessionThread(mDelayMs, new Runnable() {
+ @Override
+ public void run() {
+ mProxee.renegotiate(offer, wrap(callback));
+ }
+ });
+ }
+
+ @Override
+ public void iceExchange(final List<String> clientCandidates,
+ final SessionBase.IceExchangeCallback callback) {
+ Log.d(TAG, "Client ice candidates " + Integer.toString(clientCandidates.size()));
+ mServerExecutor.postOnSessionThread(mDelayMs, new Runnable() {
+ @Override
+ public void run() {
+ mProxee.iceExchange(clientCandidates, wrap(callback));
+ }
+ });
+ }
+
+ private NegotiationCallbackProxy wrap(SessionBase.NegotiationCallback callback) {
+ return new NegotiationCallbackProxy(callback, mClientExecutor, mDelayMs);
+ }
+
+ private IceExchangeCallbackProxy wrap(SessionBase.IceExchangeCallback callback) {
+ return new IceExchangeCallbackProxy(callback, mClientExecutor, mDelayMs);
+ }
+ }
+
+ private static final class NegotiationCallbackProxy implements SessionBase.NegotiationCallback {
+ private final SessionBase.NegotiationCallback mProxee;
+ private final SessionBase.Executor mClientExecutor;
+ private final int mDelayMs;
+
+ public NegotiationCallbackProxy(SessionBase.NegotiationCallback callback,
+ SessionBase.Executor clientExecutor,
+ int delayMs) {
+ mProxee = callback;
+ mClientExecutor = clientExecutor;
+ mDelayMs = delayMs;
+ }
+
+ @Override
+ public void onSuccess(final String answer) {
+ Log.d(TAG, "Sending answer: " + answer);
+ mClientExecutor.postOnSessionThread(mDelayMs, new Runnable() {
+ @Override
+ public void run() {
+ mProxee.onSuccess(answer);
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(final String message) {
+ mClientExecutor.postOnSessionThread(mDelayMs, new Runnable() {
+ @Override
+ public void run() {
+ mProxee.onFailure(message);
+ }
+ });
+ }
+ }
+
+ private static final class IceExchangeCallbackProxy implements SessionBase.IceExchangeCallback {
+ private final SessionBase.IceExchangeCallback mProxee;
+ private final SessionBase.Executor mClientExecutor;
+ private final int mDelayMs;
+
+ public IceExchangeCallbackProxy(SessionBase.IceExchangeCallback callback,
+ SessionBase.Executor clientExecutor,
+ int delayMs) {
+ mProxee = callback;
+ mClientExecutor = clientExecutor;
+ mDelayMs = delayMs;
+ }
+
+ @Override
+ public void onSuccess(List<String> serverCandidates) {
+ Log.d(TAG, "Server ice candidates " + Integer.toString(serverCandidates.size()));
+
+ final List<String> serverCandidatesCopy = new ArrayList<String>();
+ serverCandidatesCopy.addAll(serverCandidates);
+
+ mClientExecutor.postOnSessionThread(mDelayMs, new Runnable() {
+ @Override
+ public void run() {
+ mProxee.onSuccess(serverCandidatesCopy);
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(final String message) {
+ Log.d(TAG, "Ice exchange falure: " + message);
+ mClientExecutor.postOnSessionThread(mDelayMs, new Runnable() {
+ @Override
+ public void run() {
+ mProxee.onFailure(message);
+ }
+ });
+ }
+ }
+}
diff --git a/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/SignalingThreadMock.java b/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/SignalingThreadMock.java
index 16e0973..8d62f78 100644
--- a/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/SignalingThreadMock.java
+++ b/components/devtools_bridge/test/android/javatests/src/org/chromium/components/devtools_bridge/SignalingThreadMock.java
@@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Convinience class for tests. Like WebRTC threads supports posts
- * and synchromous invokes.
+ * and synchronous invokes.
*/
class SignalingThreadMock {
// TODO: use scaleTimeout when natives for org.chromium.base get available.