/*
SignalingClient.java
Copyright (c) 2015 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.deviceplugin.webrtc.core;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.koushikdutta.async.ByteBufferList;
import com.koushikdutta.async.DataEmitter;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.callback.DataCallback;
import com.koushikdutta.async.callback.WritableCallback;
import com.koushikdutta.async.http.AsyncHttpClient;
import com.koushikdutta.async.http.AsyncHttpRequest;
import com.koushikdutta.async.http.AsyncHttpResponse;
import com.koushikdutta.async.http.WebSocket;
import org.deviceconnect.android.deviceplugin.webrtc.BuildConfig;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* シグナリングサーバーと通信を行うクライアント.
*
* @author NTT DOCOMO, INC.
*/
public final class SignalingClient {
/**
* Tag for debugging.
*/
private static final String TAG = "WEBRTC";
/**
* Defined the HTTP timeout.
*/
private static final int TIMEOUT_HTTP_CONNECTION = 25000;
/**
* Defined the WebSocket timeout.
*/
private static final int TIMEOUT_WEBSOCKET = 30000;
/**
* トークン.
*/
private final String mStrToken = PeerUtil.randomToken(16);
/**
* Peeのコンフィグ.
*/
private PeerConfig mConfig;
/**
* Peer Id.
*/
private String mId;
/**
* Credential.
*/
private String mCredential;
/**
* Disconnect flag.
*/
private boolean mDisconnectFlag;
/**
* WebSocket connected to peer server.
*/
private WebSocket mWebSocket;
/**
* Handler.
*/
private Handler mHandler = new Handler(Looper.getMainLooper());
/**
* Server options.
*/
private SignalingOption mOption;
/**
* Queue that stores messages that send to the server.
*/
private List<String> mQueueMessage = new ArrayList<>();
/**
* Instance of Runnable that will send message to the server.
*/
private Runnable mSendingRun;
/**
* Callbacks of SignalingClient.
*/
private OnSignalingCallback mSignalingCallback;
/**
* Constructor.
* @param config config of the peer server
*/
public SignalingClient(final PeerConfig config) {
mConfig = config;
mOption = new SignalingOption();
mDisconnectFlag = true;
retrieveId(null);
}
/**
* Disconnect from peer server.
*/
public void disconnect() {
if (!mDisconnectFlag) {
mDisconnectFlag = true;
stopWebSocket();
if (mSignalingCallback != null) {
mSignalingCallback.onDisconnect();
}
}
}
/**
* Destroys this instance.
*/
public void destroy() {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ SignalingClient::destroy");
}
disconnect();
}
/**
* Returns true if disconnected to the server.
* @return true if disconnected to the server, false otherwise
*/
public boolean isDisconnectFlag() {
return mDisconnectFlag;
}
/**
* Returns true if connected to the server.
* @return true if connected to the server, false otherwise
*/
public boolean isOpen() {
if (mWebSocket != null) {
return mWebSocket.isOpen();
}
return false;
}
/**
* Gets my id.
* @return id
*/
public String getId() {
return mId;
}
/**
* Sets a callback.
* @param callback callback of SignalingClient
*/
public void setOnSignalingCallback(OnSignalingCallback callback) {
mSignalingCallback = callback;
}
/**
* Retrieves the list of peer that can be connected.
* @param callback Callback to return the list of peer
*/
public void listAllPeers(final OnAllPeersCallback callback) {
Uri uri = Uri.parse(createDiscoveryUrl());
AsyncHttpRequest req = new AsyncHttpRequest(uri, "GET");
addConfig(req);
AsyncHttpClient client = AsyncHttpClient.getDefaultInstance();
client.executeJSONArray(req, new AsyncHttpClient.JSONArrayCallback() {
@Override
public void onCompleted(final Exception e, final AsyncHttpResponse source, final JSONArray result) {
if (e != null) {
if (callback != null) {
callback.onErrorCallback();
}
} else {
int statusCode = source.code();
if (statusCode == 200) {
if (callback != null) {
callback.onCallback(result);
}
} else {
if (callback != null) {
callback.onErrorCallback();
}
}
}
}
});
}
/**
* Retrieves my id from peer server.
* @param id old id
*/
private void retrieveId(final String id) {
Uri uri = Uri.parse(createRetrievedUrl(id));
AsyncHttpRequest req = new AsyncHttpRequest(uri, "GET");
req.setHeader("Content-Length", String.valueOf(0));
req.setTimeout(TIMEOUT_HTTP_CONNECTION);
addConfig(req);
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ retrieveId");
Log.d(TAG, "@@@ uri=" + req.toString());
}
AsyncHttpClient client = AsyncHttpClient.getDefaultInstance();
client.executeString(req, new AsyncHttpClient.StringCallback() {
@Override
public void onCompleted(final Exception e, final AsyncHttpResponse source, final String result) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ retrieveId response " + source);
Log.d(TAG, "@@@ result: " + result);
}
if (e != null) {
if (mSignalingCallback != null) {
mSignalingCallback.onError("Failed to connect a server.");
}
return;
}
if (result != null && result.length() > 0 && source != null) {
int resultCode = source.code();
source.close();
if (resultCode == 200) {
try {
JSONObject jsonResult = new JSONObject(result);
mId = jsonResult.getString("id");
mCredential = jsonResult.getString("credential");
} catch (Exception ex) {
mId = result;
}
if (mId != null) {
startXhrStream();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
startWebSocket();
}
}, 1000L);
if (mHandler != null && mSendingRun != null) {
mHandler.post(mSendingRun);
}
}
} else {
if (mSignalingCallback != null) {
mSignalingCallback.onError("Failed to connect a server.");
}
}
} else {
if (source != null) {
source.close();
}
if (mSignalingCallback != null) {
mSignalingCallback.onError("Retrieved PeerId is failed.");
}
}
}
});
}
/**
* Starts XHR stream.
*/
private void startXhrStream() {
Uri uri = Uri.parse(createSkyWayServer());
AsyncHttpRequest req = new AsyncHttpRequest(uri, "POST");
req.setHeader("Accept-Encoding", "gzip, deflate");
req.setHeader("Connection", "keep-alive");
req.setHeader("Accept", "*/*");
req.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
req.addHeader("_index", "1");
req.addHeader("_streamIndex", String.valueOf(0));
req.setTimeout(TIMEOUT_HTTP_CONNECTION);
addConfig(req);
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ startXhrStream");
Log.d(TAG, "@@@ request=" + req);
}
AsyncHttpClient client = AsyncHttpClient.getDefaultInstance();
client.executeString(req, new AsyncHttpClient.StringCallback() {
@Override
public void onCompleted(final Exception e, final AsyncHttpResponse source, final String result) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ startXhrStream response.");
Log.d(TAG, "@@@ result=" + result);
Log.d(TAG, "@@@ message=" + source.message());
Log.d(TAG, "@@@ source=" + source);
if (e != null) {
Log.e(TAG, "@@@ ", e);
}
}
String type = null;
if (result != null) {
int msgIndex = result.indexOf("{");
if (msgIndex != -1) {
try {
String error = result.substring(msgIndex);
JSONObject ex = new JSONObject(error);
type = ex.getString("type");
} catch (Exception e2) {
// do nothing
}
}
if ("OPEN".equalsIgnoreCase(type)) {
mDisconnectFlag = false;
if (mSignalingCallback != null) {
mSignalingCallback.onOpen(mId);
}
}
}
}
});
}
/**
* Starts a WebSocket.
*/
private void startWebSocket() {
Uri uri = Uri.parse(createWSString());
AsyncHttpRequest req = new AsyncHttpRequest(uri, "GET");
req.setHeader("User-Agent", PeerUtil.USER_AGENT);
req.setTimeout(TIMEOUT_WEBSOCKET);
addConfig(req);
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ startWebSocket: " + uri.toString());
}
AsyncHttpClient client = AsyncHttpClient.getDefaultInstance();
client.websocket(req, PeerUtil.SCHEME_HTTP, new AsyncHttpClient.WebSocketConnectCallback() {
@Override
public void onCompleted(final Exception ex, final WebSocket webSocket) {
if (ex != null) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "", ex);
}
mDisconnectFlag = true;
if (mSignalingCallback != null) {
mSignalingCallback.onError(ex.toString());
}
} else {
mWebSocket = webSocket;
mSendingRun = new Runnable() {
@Override
public void run() {
sendMessage();
}
};
webSocket.setPongCallback(new WebSocket.PongCallback() {
@Override
public void onPongReceived(String s) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ PongCallback");
}
}
});
webSocket.setWriteableCallback(new WritableCallback() {
@Override
public void onWriteable() {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ WriteableCallback");
}
}
});
webSocket.setDataCallback(new DataCallback() {
@Override
public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ DataCallback");
}
bb.recycle();
}
});
webSocket.setClosedCallback(new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ ClosedCallback");
Log.d(TAG, "@@@ ex=" + ex);
}
if (mSignalingCallback != null) {
mSignalingCallback.onClose();
}
}
});
webSocket.setEndCallback(new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ EndCallback");
Log.d(TAG, "@@@ ex=" + ex);
}
}
});
webSocket.setStringCallback(new com.koushikdutta.async.http.WebSocket.StringCallback() {
@Override
public void onStringAvailable(final String s) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ StringCallback");
Log.d(TAG, "@@@ string=" + s);
}
try {
handleMessage(new JSONObject(s));
} catch (Exception e) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "@@@ json error.", e);
}
}
}
});
if (webSocket.isOpen()) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "@@@ WebSocket is open.");
}
mDisconnectFlag = false;
if (mSignalingCallback != null) {
mSignalingCallback.onOpen(mId);
}
} else {
if (BuildConfig.DEBUG) {
Log.e(TAG, "@@@ WebSocket cannot open.");
}
mDisconnectFlag = true;
}
}
}
});
}
/**
* Stops a WebSocket.
*/
private void stopWebSocket() {
if (mWebSocket != null) {
mWebSocket.close();
mWebSocket = null;
}
}
/**
* Added a config to the request.
* @param req request
*/
private void addConfig(final AsyncHttpRequest req) {
if (mConfig.getDomain() != null && mConfig.getDomain().length() > 0) {
req.addHeader("Origin", mConfig.getDomain());
}
}
/**
* Handles a message.
* @param json message
*/
private void handleMessage(final JSONObject json) {
String type = PeerUtil.getJSONString(json, "type", "");
String src = PeerUtil.getJSONString(json, "src", "");
JSONObject payload = json.optJSONObject("payload");
String connectionId = PeerUtil.getJSONString(payload, "connectionId", null);;
String payloadType = PeerUtil.getJSONString(payload, "type", "");
if (type.equalsIgnoreCase("open")) {
if (mDisconnectFlag) {
mDisconnectFlag = false;
if (mSignalingCallback != null) {
mSignalingCallback.onOpen(mId);
}
}
} else if (type.equalsIgnoreCase("close")) {
if (!mDisconnectFlag) {
mDisconnectFlag = true;
if (mSignalingCallback != null) {
mSignalingCallback.onClose();
}
}
} else if (type.equalsIgnoreCase("error")) {
String msg = PeerUtil.getJSONString(payload, "msg", "");
if (mSignalingCallback != null) {
mSignalingCallback.onError(msg);
}
} else if (type.equalsIgnoreCase("id-taken")) {
mDisconnectFlag = true;
String msg = String.format("ID `%s` is taken.", mId);
if (mSignalingCallback != null) {
mSignalingCallback.onError(msg);
}
} else if (type.equalsIgnoreCase("invalid-key")) {
mDisconnectFlag = true;
String msg = String.format("API KEY `%s` is invalid.", mConfig.getApiKey());
if (mSignalingCallback != null) {
mSignalingCallback.onError(msg);
}
} else if (type.equalsIgnoreCase("ping")) {
queueMessage("{\"type\":\"PONG\"}");
} else if (type.equalsIgnoreCase("leave")) {
} else if (type.equalsIgnoreCase("expire")) {
} else if (type.equalsIgnoreCase("offer")) {
if (payloadType.equalsIgnoreCase("media")) {
} else if (payloadType.equalsIgnoreCase("data")) {
}
if (mSignalingCallback != null) {
mSignalingCallback.onOffer(json);
}
} else if (type.equalsIgnoreCase("answer")) {
if (mSignalingCallback != null) {
mSignalingCallback.onAnswer(json);
}
} else if (type.equalsIgnoreCase("candidate")) {
if (mSignalingCallback != null) {
mSignalingCallback.onCandidate(json);
}
} else {
if (BuildConfig.DEBUG) {
Log.e(TAG, "@@@ Unknown type." + type);
}
}
}
/**
* Gets a url that retrieve id.
* @param id old id
* @return url
*/
private String createRetrievedUrl(final String id) {
StringBuilder build = new StringBuilder();
build.append(mOption.mScheme);
build.append("://");
build.append(mOption.mHost);
build.append(":");
build.append(mOption.mPort);
build.append("/" + mConfig.getApiKey());
build.append("/id?");
String strTSValue = PeerUtil.getTSValue();
build.append(strTSValue);
if (id != null && id.length() > 0) {
build.append("&id=" + id);
}
return build.toString();
}
/**
* Gets a url that retrieve list of id.
* @return url
*/
private String createDiscoveryUrl() {
StringBuilder build = new StringBuilder();
build.append(mOption.mScheme);
build.append("://");
build.append(mOption.mHost);
build.append(":");
build.append(mOption.mPort);
build.append("/active/list");
build.append("/" + mConfig.getApiKey());
return build.toString();
}
/**
* Gets a url of WebSocket.
* @return url
*/
private String createWSString() {
StringBuilder build = new StringBuilder();
build.append(mOption.mScheme);
build.append("://");
build.append(mOption.mHost);
build.append("/peerjs");
build.append("?key=" + mConfig.getApiKey());
build.append("&id=" + mId);
build.append("&token=" + mStrToken);
return build.toString();
}
/**
* Gets a url of SkyWay server.
* @return url
*/
private String createSkyWayServer() {
StringBuilder build = new StringBuilder();
build.append(mOption.mScheme);
build.append("://");
build.append(mOption.mHost);
build.append(":");
build.append(mOption.mPort);
build.append("/" + mConfig.getApiKey());
build.append("/" + mId);
build.append("/" + mStrToken);
build.append("/id");
build.append("?i=0");
return build.toString();
}
/**
* Sends a message to the server.
*/
private void sendMessage() {
if (mWebSocket != null && mWebSocket.isOpen() && !mQueueMessage.isEmpty()) {
boolean continues = false;
String msg = null;
synchronized (mQueueMessage) {
if (!mQueueMessage.isEmpty()) {
msg = mQueueMessage.remove(0);
}
}
if (msg != null) {
mWebSocket.send(msg);
synchronized (mQueueMessage) {
continues = !mQueueMessage.isEmpty();
}
}
if (continues && mSendingRun != null) {
mHandler.postDelayed(mSendingRun, 100L);
}
}
}
/**
* Inserts the message into the queue.
* @param message message
*/
void queueMessage(final String message) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "@@@ " + message);
}
boolean firstTime = false;
synchronized (mQueueMessage) {
if (mQueueMessage.isEmpty()) {
firstTime = true;
}
mQueueMessage.add(message);
}
if (firstTime && mSendingRun != null) {
mHandler.postDelayed(mSendingRun, 100L);
}
}
/**
* SignalingOption.
*
* @author NTT DOCOMO, INC.
*/
public class SignalingOption {
String mScheme = PeerUtil.SCHEME_HTTPS;
String mHost = PeerUtil.SKYWAY_HOST;
int mPort = PeerUtil.SKYWAY_PORT;
}
/**
* This interface is used to implement {@link SignalingClient} callbacks.
*/
public interface OnAllPeersCallback {
void onCallback(JSONArray result);
void onErrorCallback();
}
/**
* This interface is used to implement {@link SignalingClient} callbacks.
*/
public interface OnSignalingCallback {
void onOpen(String peerId);
void onClose();
void onOffer(JSONObject jsonMsg);
void onAnswer(JSONObject json);
void onCandidate(JSONObject json);
void onDisconnect();
void onError(String message);
}
}