diff options
author | Sergey Ryazanov <serya@chromium.org> | 2014-10-16 13:32:59 +0400 |
---|---|---|
committer | Sergey Ryazanov <serya@chromium.org> | 2014-10-16 09:35:58 +0000 |
commit | a25a26c53da3c9ee6bce24cf5ce9a60e81d97c39 (patch) | |
tree | 43745514cbc7286dea4a161224236c493c239744 | |
parent | 243100c408f479f1deb7f318b49032f8be5c9cf9 (diff) | |
download | chromium_src-a25a26c53da3c9ee6bce24cf5ce9a60e81d97c39.zip chromium_src-a25a26c53da3c9ee6bce24cf5ce9a60e81d97c39.tar.gz chromium_src-a25a26c53da3c9ee6bce24cf5ce9a60e81d97c39.tar.bz2 |
Implementing RTC debugging session objects (client and server parts).
Pair of ClientSession and ServerSession holds WebRTC connection which tunnels DevTools UNIX socket.
Once cloud based signaling channel doesn't exist at the moment connections work on a single android device. For manual testing the testing APK has launch activity which lets to start service which tunnels Chrome Shell socket to another local socket (only works when Chrome Shell signed with the same certificate).
BUG=383418
TEST=Automatic: LocalSessionBridgeTest, SessionControlMessagesTest. See description for manual testing.
R=mnaganov@chromium.org
Review URL: https://codereview.chromium.org/537253003
Cr-Commit-Position: refs/heads/master@{#299871}
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. |