diff options
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. |