/*
DConnectHelper.java
Copyright (c) 2016 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.app.simplebot.utils;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import org.deviceconnect.android.app.simplebot.BuildConfig;
import org.deviceconnect.android.app.simplebot.R;
import org.deviceconnect.android.app.simplebot.data.SettingData;
import org.deviceconnect.message.DConnectEventMessage;
import org.deviceconnect.message.DConnectMessage;
import org.deviceconnect.message.DConnectResponseMessage;
import org.deviceconnect.message.DConnectSDK;
import org.deviceconnect.message.DConnectSDKFactory;
import org.deviceconnect.message.entity.MultipartEntity;
import org.deviceconnect.message.entity.StringEntity;
import org.deviceconnect.profile.AuthorizationProfileConstants;
import org.deviceconnect.profile.ServiceDiscoveryProfileConstants;
import org.deviceconnect.profile.ServiceInformationProfileConstants;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* dConnectヘルパークラス
*/
public class DConnectHelper {
//region Declaration
//---------------------------------------------------------------------------------------
/** シングルトンなManagerのインスタンス */
public static final DConnectHelper INSTANCE = new DConnectHelper();
/** デバッグタグ */
private static final String TAG = "DConnectHelper";
/** イベントハンドラー */
private EventHandler eventHandler = null;
/** Device Connect ManagerへアクセスするためのSDK. */
private DConnectSDK mDConnectSDK;
/** コンテキスト. */
private Context mContext;
/**
* リトライを行う間隔(SECOND).
*/
private static final int RETRY_INTERVAL = 10;
/**
* イベントが有効化フラグ.
* イベントが有効の場合にはtrue、それ以外はfalse。
*/
private boolean mActiveWebSocket;
/**
* リトライを行うためのスレッド管理クラス.
*/
private ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor();
/** 処理完了コールバック */
public interface FinishCallback<Result> {
/**
* 処理が完了した時に呼ばれます.
* @param result 結果
* @param error エラー
*/
void onFinish(Result result, Exception error);
}
/** イベントハンドラー */
public interface EventHandler {
/**
* イベントが発生した時に呼ばれます.
* @param event イベント
*/
void onEvent(DConnectEventMessage event);
}
/**
* Serviceの情報
*/
public static class ServiceInfo {
public String id;
public String name;
public List<String> scopes;
public ServiceInfo(String id, String name, List<String> scopes) {
this.id = id;
this.name = name;
this.scopes = scopes;
}
@Override
public String toString() {
return "ServiceInfo{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", scopes='" + scopes.toString() + '\'' + '}';
}
}
/**
* APIの情報
*/
public static class APIInfo {
public String name;
public String method;
public String path;
public List<APIParam> params;
}
/**
* APIパラメータ情報
*/
public static class APIParam {
public String type;
public boolean required;
public String name;
}
/**
* 認証情報
*/
public static class AuthInfo {
public String clientId;
public String accessToken;
public AuthInfo(String clientId, String accessToken) {
this.clientId = clientId;
this.accessToken = accessToken;
}
@Override
public String toString() {
return "AuthInfo{" + "clientId='" + clientId + '\'' + ", accessToken='" + accessToken + '\'' + '}';
}
}
/** DConnectHelperの基底Exception */
public abstract class DConnectHelperException extends Exception {
public int errorCode = 0;
}
/** Resultが不正 */
public class DConnectInvalidResultException extends DConnectHelperException {}
/** 認証失敗 */
public class DConnectAuthFailedException extends DConnectHelperException {}
//endregion
//---------------------------------------------------------------------------------------
//region Methods
public void setContext(Context context) {
mContext = context;
if (context == null) {
mDConnectSDK = null;
} else {
mDConnectSDK = DConnectSDKFactory.create(context, DConnectSDKFactory.Type.HTTP);
mDConnectSDK.setOrigin(context.getPackageName());
SettingData setting = SettingData.getInstance(context);
if (setting.accessToken != null) {
mDConnectSDK.setAccessToken(setting.accessToken);
}
}
}
/**
* イベントハンドラーを設定する.
*
* @param handler ハンドラー
*/
public void setEventHandler(EventHandler handler) {
this.eventHandler = handler;
}
/**
* 接続先情報を設定する.
*
* @param ssl SSL通信を行う場合true
* @param host ホスト名
* @param port ポート番号
*/
public void setHostInfo(boolean ssl, String host, int port) {
mDConnectSDK.setSSL(ssl);
mDConnectSDK.setHost(host);
mDConnectSDK.setPort(port);
}
public void availability(final FinishCallback<Void> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "availability");
mDConnectSDK.availability(new DConnectSDK.OnResponseListener() {
@Override
public void onResponse(final DConnectResponseMessage response) {
Exception e = null;
if (response.getResult() == DConnectMessage.RESULT_ERROR) {
e = new Exception(response.getErrorMessage());
}
callback.onFinish(null, e);
}
});
}
/**
* ServiceDiscoveryを実行する.
*
* @param callback コールバック
*/
public void serviceDiscovery(final FinishCallback<List<ServiceInfo>> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "serviceDiscovery");
mDConnectSDK.serviceDiscovery(new DConnectSDK.OnResponseListener() {
@Override
public void onResponse(final DConnectResponseMessage response) {
if (response.getResult() == DConnectMessage.RESULT_ERROR) {
switch (DConnectMessage.ErrorCode.getInstance(response.getErrorCode())) {
case AUTHORIZATION:
case EXPIRED_ACCESS_TOKEN:
case EMPTY_ACCESS_TOKEN:
case SCOPE:
case NOT_FOUND_CLIENT_ID:
auth(new FinishCallback<AuthInfo>() {
@Override
public void onFinish(AuthInfo authInfo, Exception error) {
if (error == null) {
serviceDiscovery(callback);
} else {
DConnectHelperException e = new DConnectInvalidResultException();
e.errorCode = response.getInt(DConnectMessage.EXTRA_ERROR_CODE);
callback.onFinish(null, e);
}
}
});
return;
}
}
if (response.getResult() == DConnectMessage.RESULT_OK) {
// サービスリストを取得
List<Object> services = response.getList(ServiceDiscoveryProfileConstants.PARAM_SERVICES);
if (services == null) {
// サービスがない場合
callback.onFinish(null, null);
return;
}
// 詰め直しして返却
List<ServiceInfo> list = new ArrayList<>();
for (Object object: services) {
@SuppressWarnings("unchecked")
Map<String, Object> service = (Map<String, Object>) object;
@SuppressWarnings("unchecked")
ServiceInfo info = new ServiceInfo(
service.get(ServiceDiscoveryProfileConstants.PARAM_ID).toString(),
service.get(ServiceDiscoveryProfileConstants.PARAM_NAME).toString(),
(List<String>) service.get(ServiceDiscoveryProfileConstants.PARAM_SCOPES));
list.add(info);
}
callback.onFinish(list, null);
} else {
DConnectHelperException e = new DConnectInvalidResultException();
e.errorCode = response.getInt(DConnectMessage.EXTRA_ERROR_CODE);
callback.onFinish(null, e);
}
}
});
}
/**
* ServiceInformationを実行する.
*
* @param serviceId ServiceID
* @param callback コールバック
*/
public void serviceInformation(final String serviceId, final FinishCallback<Map<String, List<APIInfo>>> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "serviceInformation");
mDConnectSDK.getServiceInformation(serviceId, new DConnectSDK.OnResponseListener() {
@Override
public void onResponse(final DConnectResponseMessage response) {
int result = response.getInt(DConnectMessage.EXTRA_RESULT);
if (result == DConnectMessage.RESULT_ERROR) {
switch (DConnectMessage.ErrorCode.getInstance(response.getErrorCode())) {
case AUTHORIZATION:
case EXPIRED_ACCESS_TOKEN:
case EMPTY_ACCESS_TOKEN:
case SCOPE:
case NOT_FOUND_CLIENT_ID:
auth(new FinishCallback<AuthInfo>() {
@Override
public void onFinish(AuthInfo authInfo, Exception error) {
if (error == null) {
serviceInformation(serviceId, callback);
} else {
DConnectHelperException e = new DConnectInvalidResultException();
e.errorCode = response.getInt(DConnectMessage.EXTRA_ERROR_CODE);
callback.onFinish(null, e);
}
}
});
return;
default:
DConnectHelperException e = new DConnectInvalidResultException();
e.errorCode = response.getInt(DConnectMessage.EXTRA_ERROR_CODE);
callback.onFinish(null, e);
break;
}
}
// APIリストを取得
@SuppressWarnings("unchecked")
Map<String, Map<String, Object>> profiles = (Map<String, Map<String, Object>>)response.get(ServiceInformationProfileConstants.PARAM_SUPPORT_APIS);
if (profiles == null) {
// サービスがない場合
callback.onFinish(null, null);
return;
}
// 詰め直しして返却
Map<String, List<APIInfo>> res = new HashMap<>();
for (Map.Entry<String, Map<String, Object>> profile: profiles.entrySet()) {
List<APIInfo> list = new ArrayList<>();
String profileName = profile.getKey();
if (!(profile.getValue() instanceof Map)) {
continue;
}
for (Map.Entry<String, Object> info: profile.getValue().entrySet()) {
if ("paths".equals(info.getKey())) {
@SuppressWarnings("unchecked")
Map<String, Map<String, Object>> apis = (Map<String, Map<String, Object>>) info.getValue();
for (Map.Entry<String, Map<String, Object>> api: apis.entrySet()) {
for (Map.Entry<String, Object> method: api.getValue().entrySet()) {
APIInfo apiInfo = new APIInfo();
apiInfo.path = profileName;
if (!api.getKey().endsWith("/")) {
apiInfo.path += api.getKey();
}
apiInfo.method = method.getKey().toUpperCase();
@SuppressWarnings("unchecked")
Map<String, Object> methodInfo = (Map<String, Object>) method.getValue();
apiInfo.name = (String) methodInfo.get("summary");
if (apiInfo.name == null || apiInfo.name.length() == 0) {
apiInfo.name = apiInfo.path + " [" + apiInfo.method + "]";
} else {
apiInfo.name += "\n" + apiInfo.path + " [" + apiInfo.method + "]";
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> params = (List<Map<String, Object>>) methodInfo.get("parameters");
if (params != null) {
apiInfo.params = new ArrayList<>();
for (Map<String, Object> paramMap: params) {
APIParam param = new APIParam();
param.name = paramMap.get("name").toString();
param.type = paramMap.get("type").toString();
param.required = paramMap.get("required").toString().equals("true");
apiInfo.params.add(param);
}
}
list.add(apiInfo);
}
}
}
}
res.put(profileName, list);
}
callback.onFinish(res, null);
}
});
}
private void auth(final FinishCallback<AuthInfo> callback) {
final SettingData setting = SettingData.getInstance(mContext);
String appName = mContext.getString(R.string.app_name);
String scopes[] = setting.getScopes();
auth(appName, scopes, callback);
}
/**
* Local OAuth認証を行う.
*
* @param appName アプリ名
* @param scopes スコープ
* @param callback 完了コールバック
*/
public void auth(String appName, String[] scopes, final FinishCallback<AuthInfo> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "auth");
mDConnectSDK.authorization(appName, scopes, new DConnectSDK.OnAuthorizationListener() {
@Override
public void onResponse(final String clientId, final String accessToken) {
mDConnectSDK.setAccessToken(accessToken);
SettingData setting = SettingData.getInstance(mContext);
setting.clientId = clientId;
setting.accessToken = accessToken;
setting.save();
callback.onFinish(new AuthInfo(clientId, accessToken), null);
}
@Override
public void onError(final int errorCode, final String errorMessage) {
DConnectHelperException e = new DConnectAuthFailedException();
e.errorCode = errorCode;
if (BuildConfig.DEBUG) Log.e(TAG, "Error on auth:" + errorCode);
callback.onFinish(null, e);
}
});
}
/**
* メッセージ送信する.
*
* @param serviceId ServiceID
* @param channelId ChannelID
* @param text Text
*/
public void sendMessage(String serviceId, String channelId, String text, String resource, final FinishCallback<Void> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "sendMessage:" + channelId + ":" + text);
// 接続情報
ConnectionParam connectionParam = new ConnectionParam(
"POST",
"messageHook",
"message"
);
// パラメータ
Map<String, Object> params = new HashMap<>();
params.put(DConnectMessage.EXTRA_SERVICE_ID, serviceId);
if (text != null) {
params.put("text", text);
}
params.put("channelId", channelId);
if (resource != null && resource.length() > 0) {
params.put("resource", resource);
params.put("mimeType", "image");
}
// 接続
new HttpTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new TaskParam(connectionParam, params) {
@Override
public void callBack(DConnectMessage message) {
// コールバックがないので処理する意味がない
if (callback == null) {
return;
}
// エラーチェック
int result = message.getInt(DConnectMessage.EXTRA_RESULT);
if (result == DConnectMessage.RESULT_ERROR) {
DConnectHelperException e = new DConnectInvalidResultException();
e.errorCode = message.getInt(DConnectMessage.EXTRA_ERROR_CODE);
callback.onFinish(null, e);
return;
}
callback.onFinish(null, null);
}
});
}
/**
* リクエスト送信する.
*
* @param method Method
* @param path Path
* @param serviceId ServiceID
* @param params パラメータ
* @param callback Callback
*/
public void sendRequest(String method, String path, String serviceId, Map<String, Object> params, final FinishCallback<Map<String, Object>> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "sendRequest");
// 接続情報
ConnectionParam connectionParam = new ConnectionParam(
method,
path
);
// パラメータ
if (params == null) {
params = new HashMap<>();
}
params.put(DConnectMessage.EXTRA_SERVICE_ID, serviceId);
new HttpTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new TaskParam(connectionParam, params) {
@Override
public void callBack(DConnectMessage message) {
// コールバックがないので処理する意味がない
if (callback == null) {
return;
}
// エラーチェック
int result = message.getInt(DConnectMessage.EXTRA_RESULT);
if (result == DConnectMessage.RESULT_ERROR) {
DConnectHelperException e = new DConnectInvalidResultException();
e.errorCode = message.getInt(DConnectMessage.EXTRA_ERROR_CODE);
callback.onFinish(message, e);
return;
}
callback.onFinish(message, null);
}
});
}
/**
* WebSocketを開く.
*/
public synchronized void openWebSocket() {
mActiveWebSocket = true;
connectWebSocket();
}
/**
* WebSocketを閉じる.
*/
public synchronized void closeWebSocket() {
mActiveWebSocket = false;
unregisterEvent();
mDConnectSDK.disconnectWebSocket();
}
/**
* WebSocketの接続状態を確認する.
* @return 接続されている場合はtrue、それ以外はfalse
*/
public synchronized boolean isOpenWebSocket() {
return mActiveWebSocket && mDConnectSDK.isConnectedWebSocket();
}
/**
* WebSocketの接続処理を行う.
* <p>
* 接続が切れた場合には、{@link #RETRY_INTERVAL}秒後に再接続を試みる。
* </p>
*/
private void connectWebSocket() {
mDConnectSDK.connectWebSocket(new DConnectSDK.OnWebSocketListener() {
@Override
public void onOpen() {
if (BuildConfig.DEBUG) {
Log.i(TAG, "WebSocket is opened.");
}
registerEvent();
}
@Override
public void onClose() {
if (BuildConfig.DEBUG) {
Log.i(TAG, "WebSocket is closed.");
}
}
@Override
public void onError(Exception e) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "WebSocket occurred a exception. ", e);
}
}
});
}
/**
* イベント登録・解除する.
* @param profile Profile
* @param attribute Attribute
* @param serviceId ServiceID
* @param unregist 登録の場合はtrue、解除の場合はfalse
* @param callback Callback
*/
private void registerEvent(String profile, String attribute, String serviceId, boolean unregist, final FinishCallback<Void> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "registerEvent:" + profile + ":" + attribute);
DConnectSDK.URIBuilder builder = mDConnectSDK.createURIBuilder();
builder.setProfile(profile);
builder.setAttribute(attribute);
builder.setServiceId(serviceId);
if (!unregist) {
mDConnectSDK.addEventListener(builder.build(), new DConnectSDK.OnEventListener() {
@Override
public void onMessage(DConnectEventMessage message) {
eventHandler.onEvent(message);
}
@Override
public void onResponse(final DConnectResponseMessage response) {
// エラーチェック
int result = response.getResult();
if (result == DConnectMessage.RESULT_ERROR) {
DConnectHelperException e = new DConnectInvalidResultException();
e.errorCode = response.getErrorCode();
callback.onFinish(null, e);
return;
}
callback.onFinish(null, null);
}
});
} else {
mDConnectSDK.removeEventListener(builder.build());
}
}
/**
* messageHookプロファイルにイベントの登録する.
*/
private void registerEvent() {
SettingData setting = SettingData.getInstance(mContext);
DConnectHelper.INSTANCE.registerEvent("messageHook", "onmessage", setting.serviceId, false, new DConnectHelper.FinishCallback<Void>() {
@Override
public void onFinish(Void aVoid, Exception error) {
if (error != null) {
mExecutor.schedule(new Runnable() {
@Override
public void run() {
if (mActiveWebSocket) {
registerEvent();
}
}
}, RETRY_INTERVAL, TimeUnit.SECONDS);
}
}
});
}
/**
* messageHookプロファイルへのイベントの解除する.
*/
private void unregisterEvent() {
SettingData setting = SettingData.getInstance(mContext);
DConnectHelper.INSTANCE.registerEvent("messageHook", "onmessage", setting.serviceId, true, new DConnectHelper.FinishCallback<Void>() {
@Override
public void onFinish(Void aVoid, Exception error) {
}
});
}
//endregion
//---------------------------------------------------------------------------------------
//region Tasks
/**
* 接続情報
*/
private class ConnectionParam {
public String profileName;
public String attributeName;
public String method;
public String path;
public ConnectionParam(String method, String profileName, String attributeName) {
this.method = method;
this.profileName = profileName;
this.attributeName = attributeName;
}
public ConnectionParam(String method, String path) {
this.method = method;
this.path = path;
}
}
/**
* Task用パラメータ
*/
private class TaskParam {
/** 接続情報 */
public ConnectionParam connection;
/** その他パラメータ */
public Map<String, Object> params;
/** コールバック */
public void callBack(DConnectMessage message) {}
public TaskParam(ConnectionParam url, Map<String, Object > params) {
this.connection = url;
this.params = params;
}
}
/**
* Http用Task
*/
private class HttpTask extends AsyncTask<TaskParam, Void, DConnectMessage> {
/** パラメータ */
TaskParam param = null;
private DConnectResponseMessage executeRequest() {
DConnectSDK.URIBuilder builder = mDConnectSDK.createURIBuilder();
MultipartEntity entity = null;
ConnectionParam conn = param.connection;
if (conn.path == null) {
builder.setProfile(conn.profileName);
builder.setAttribute(conn.attributeName);
} else {
builder.setPath(conn.path);
}
boolean isQuery = conn.method.equals("GET") || conn.method.equals("DELETE");
if (isQuery) {
// GET/DELETEはQueryをURIに付加
for (String key: param.params.keySet()) {
builder.addParameter(key, (String) param.params.get(key));
}
} else {
entity = new MultipartEntity();
for (String key: param.params.keySet()) {
entity.add(key, new StringEntity((String) param.params.get(key)));
}
}
DConnectResponseMessage message = new DConnectResponseMessage(DConnectMessage.RESULT_ERROR);
if (conn.method.equals("GET")) {
message = mDConnectSDK.get(builder.build());
} else if (conn.method.equals("PUT")) {
message = mDConnectSDK.put(builder.build(), entity);
} else if (conn.method.equals("POST")) {
message = mDConnectSDK.post(builder.build(), entity);
} else if (conn.method.equals("DELETE")) {
message = mDConnectSDK.delete(builder.build());
}
return message;
}
/**
* 実行するパスから使用するプロファイルを抽出してスコープに追加します.
*/
private void addNewProfileName() {
SettingData setting = SettingData.getInstance(mContext);
ConnectionParam conn = param.connection;
if (conn.path == null) {
if (conn.profileName != null) {
setting.scopes.add(conn.profileName);
}
} else {
String[] segments = conn.path.split("/");
if (segments.length > 1) {
setting.scopes.add(segments[1]);
}
}
setting.save();
}
/**
* Local OAuthを実行します.
* @return 実行結果
*/
private DConnectResponseMessage authorization() {
final SettingData setting = SettingData.getInstance(mContext);
String appName = mContext.getString(R.string.app_name);
String scopes[] = setting.getScopes();
DConnectResponseMessage response = mDConnectSDK.authorization(appName, scopes);
if (response.getResult() == DConnectMessage.RESULT_OK) {
String accessToken = response.getString(AuthorizationProfileConstants.PARAM_ACCESS_TOKEN);
mDConnectSDK.setAccessToken(accessToken);
setting.accessToken = accessToken;
setting.save();
}
return response;
}
@Override
protected DConnectMessage doInBackground(TaskParam... params) {
param = params[0];
DConnectResponseMessage response = executeRequest();
if (response.getResult() == DConnectMessage.RESULT_ERROR) {
switch (DConnectMessage.ErrorCode.getInstance(response.getErrorCode())) {
case SCOPE:
addNewProfileName();
case AUTHORIZATION:
case EXPIRED_ACCESS_TOKEN:
case EMPTY_ACCESS_TOKEN:
case NOT_FOUND_CLIENT_ID: {
DConnectResponseMessage resp = authorization();
if (resp.getResult() == DConnectMessage.RESULT_OK) {
response = executeRequest();
}
} break;
}
}
return response;
}
@Override
protected void onPostExecute(DConnectMessage message) {
param.callBack(message);
}
}
//endregion
//---------------------------------------------------------------------------------------
}