/*
DConnectWebSocketClient.java
Copyright (c) 2016 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.message;
import android.net.Uri;
import android.util.Log;
import org.deviceconnect.message.intent.message.IntentDConnectMessage;
import org.deviceconnect.sdk.BuildConfig;
import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_17;
import org.java_websocket.framing.Framedata;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* WebSocketクライアントを管理するクラス.
* @author NTT DOCOMO, INC.
*/
class DConnectWebSocketClient {
/**
* デバッグフラグ.
*/
private static final boolean DEBUG = BuildConfig.DEBUG;
/**
* デバッグ用タグ.
*/
private static final String TAG = "DConnectSDK";
/**
* Device Connect Managerのイベントを配送するリスナーを格納するマップ.
*/
private Map<String, HttpDConnectSDK.OnEventListener> mListenerMap = new HashMap<>();
/**
* WebSocketの接続状態を通知するリスナー.
*/
private HttpDConnectSDK.OnWebSocketListener mOnWebSocketListener;
/**
* WebSocketクライアント.
*/
private WebSocketClient mWebSocketClient;
/**
* WebSocketの接続が確率フラグ.
* <p>
* trueの場合は接続済み、falseの場合は接続していない
* </p>
*/
private boolean isEstablishedWebSocket;
/**
* WebSocketタイムアウト時間(ms).
*/
private int mTimeout = 30 * 1000;
/**
* WebSocketの接続状態を通知するリスナーを設定する.
* @param onWebSocketListener 接続状態を通知するリスナー
*/
void setOnWebSocketListener(final HttpDConnectSDK.OnWebSocketListener onWebSocketListener) {
mOnWebSocketListener = onWebSocketListener;
}
/**
* WebSocket接続タイムアウト時間(ms)を設定する.
* @param timeout タイムアウト時間(ms)
*/
void setTimeout(final int timeout) {
mTimeout = timeout;
}
/**
* Device Connect ManagerのWebSocketサーバに接続を行う.
* @param uri サーバへのURI
* @param origin オリジン
* @param accessToken アクセストークン
*/
void connect(final String uri, final String origin, final String accessToken) {
if (mWebSocketClient != null) {
if (DEBUG) {
Log.w(TAG, "WebSocketClient is already connected.");
}
return;
}
Map<String, String> headers = new HashMap<>();
if (origin != null) {
headers.put(IntentDConnectMessage.EXTRA_ORIGIN, origin);
}
mWebSocketClient = new WebSocketClient(URI.create(uri), new Draft_17(), headers, mTimeout) {
@Override
public void onMessage(final String message) {
try {
JSONObject json = new JSONObject(message);
if (!isEstablishedWebSocket) {
if (json.getInt(DConnectMessage.EXTRA_RESULT) == DConnectMessage.RESULT_OK) {
isEstablishedWebSocket = true;
if (mOnWebSocketListener != null) {
mOnWebSocketListener.onOpen();
}
} else {
if (mOnWebSocketListener != null) {
RuntimeException ex = new RuntimeException();
mOnWebSocketListener.onError(ex);
}
}
} else {
DConnectSDK.OnEventListener l = mListenerMap.get(createPath(json));
if (l != null) {
l.onMessage(new DConnectEventMessage(message));
}
}
} catch (JSONException e) {
if (DEBUG) {
Log.w(TAG, "The message from Device Connect Manager is invalid.", e);
}
if (mOnWebSocketListener != null) {
mOnWebSocketListener.onError(e);
}
}
}
@Override
public void onOpen(final ServerHandshake handshake) {
sendAccessToken(accessToken);
}
@Override
public void onClose(final int code, final String reason, final boolean remote) {
DConnectWebSocketClient.this.close();
if (mOnWebSocketListener != null) {
mOnWebSocketListener.onClose();
}
}
@Override
public void onWebsocketPing(final WebSocket conn, final Framedata f) {
super.onWebsocketPing(conn, f);
}
@Override
public void onWebsocketPong(final WebSocket conn, final Framedata f) {
super.onWebsocketPong(conn, f);
}
@Override
public void onError(final Exception ex) {
if (mOnWebSocketListener != null) {
mOnWebSocketListener.onError(ex);
}
}
};
if (uri.startsWith("wss")) {
try {
SSLSocketFactory factory = createSSLSocketFactory();
mWebSocketClient.setSocket(factory.createSocket());
} catch (NoSuchAlgorithmException | KeyManagementException | IOException e) {
if (mOnWebSocketListener != null) {
mOnWebSocketListener.onError(e);
}
return;
}
}
mWebSocketClient.connect();
}
/**
* WebSocketが接続中かを確認する.
* @return 接続中の場合はtrue、それ以外はfalse
*/
boolean isConnected() {
return isEstablishedWebSocket;
}
/**
* イベントのパスを作成する.
* @param json イベントメッセージ
* @return パス
*/
private String createPath(final JSONObject json) {
String uri = "/gotapi";
if (json.has(DConnectMessage.EXTRA_PROFILE)) {
uri += "/";
uri += json.optString(DConnectMessage.EXTRA_PROFILE);
}
if (json.has(DConnectMessage.EXTRA_INTERFACE)) {
uri += "/";
uri += json.optString(DConnectMessage.EXTRA_INTERFACE);
}
if (json.has(DConnectMessage.EXTRA_ATTRIBUTE)) {
uri += "/";
uri += json.optString(DConnectMessage.EXTRA_ATTRIBUTE);
}
return uri.toLowerCase();
}
/**
* URIからパスを抽出する.
* @param uri パスを抽出するURI
* @return パス
*/
private String convertUriToPath(final Uri uri) {
return uri.getPath().toLowerCase();
}
/**
* WebSocketを切断する.
*/
void close() {
if (mWebSocketClient != null) {
mWebSocketClient.close();
mWebSocketClient = null;
}
isEstablishedWebSocket = false;
}
/**
* イベント通知リスナーを登録する.
* @param uri 登録イベントのURI
* @param listener 通知リスナー
*/
void addEventListener(final Uri uri, final HttpDConnectSDK.OnEventListener listener) {
mListenerMap.put(convertUriToPath(uri), listener);
}
/**
* イベント通知リスナーを削除する.
* @param uri 解除するイベントのURI
*/
void removeEventListener(final Uri uri) {
mListenerMap.remove(convertUriToPath(uri));
}
/**
* アクセストークンをDevice Connect Managerに送信する.
* @param accessToken アクセストークン
*/
private void sendAccessToken(final String accessToken) {
mWebSocketClient.send("{\"" + DConnectMessage.EXTRA_ACCESS_TOKEN + "\":\"" + accessToken + "\"}");
}
private SSLSocketFactory createSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
TrustManager[] transManagers = {
new X509TrustManager() {
@Override
public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};
SSLContext sslcontext = SSLContext.getInstance("SSL");
sslcontext.init(null, transManagers, new SecureRandom());
return sslcontext.getSocketFactory();
}
}