/* DConnectHelper.java Copyright (c) 2016 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.awsiot.local; import android.net.Uri; import android.os.AsyncTask; import android.util.Log; import org.deviceconnect.android.deviceplugin.awsiot.cores.core.AWSIotDeviceApplication; import org.deviceconnect.android.deviceplugin.awsiot.cores.core.AWSIotPrefUtil; import org.deviceconnect.android.deviceplugin.awsiot.cores.util.AWSIotUtil; import org.deviceconnect.android.deviceplugin.awsiot.cores.util.HttpUtil; import org.deviceconnect.android.deviceplugin.awsiot.remote.BuildConfig; import org.deviceconnect.android.profile.AuthorizationProfile; import org.deviceconnect.message.DConnectMessage; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * DeviceConnectヘルパークラス. */ public class DConnectHelper { /** デバッグフラグ. */ private static final boolean DEBUG = BuildConfig.DEBUG; /** デバッグタグ. */ private static final String TAG = "DConnectHelper"; /** シングルトンなManagerのインスタンス. */ public static final DConnectHelper INSTANCE = new DConnectHelper(); public static final String ORIGIN = "http://org.deviceconnect.android.deviceplugin.awsiot"; private AuthInfo mAuthInfo; private Map<String, String> mDefaultHeader = new HashMap<>(); private List<String> mScopes = new ArrayList<String>() { { add("airconditioner"); add("atmosphericpressure"); add("battery"); add("camera"); add("canvas"); add("connection"); add("deviceorientation"); add("drivecontroller"); add("ecg"); add("echonetLite"); add("file"); add("filedescriptor"); add("geolocation"); add("gpio"); add("health"); add("humandetection"); add("humidity"); add("illuminance"); add("keyevent"); add("light"); add("mediaplayer"); add("mediastreamrecording"); add("messagehook"); add("notification"); add("omnidirectionalimage"); add("phone"); add("poseestimation"); add("power"); add("powermeter"); add("proximity"); add("remotecontroller"); add("servicediscovery"); add("serviceinformation"); add("setting"); add("sphero"); add("stressestimation"); add("system"); add("temperature"); add("touch"); add("tv"); add("vibration"); add("videochat"); add("walkstate"); } }; private AWSIotPrefUtil mPrefUtil; private boolean mSSL; private String mHost = "localhost"; private int mPort = 4035; private AWSIotWebSocketClient mAWSIotWebSocketClient; private AWSIotWebSocketClient.OnMessageEventListener mOnMessageEventListener; private DConnectHelper() { mDefaultHeader.put(DConnectMessage.HEADER_GOTAPI_ORIGIN, ORIGIN); mPrefUtil = new AWSIotPrefUtil(AWSIotDeviceApplication.getInstance()); String accessToken = mPrefUtil.getAuthAccessToken(); String clientId = mPrefUtil.getAuthClientId(); if (accessToken != null && clientId != null) { mAuthInfo = new AuthInfo(clientId, accessToken); } } /** * SSLフラグを取得する. * @return SSLが有効の場合はtrue、それ以外はfalse */ public boolean isSSL() { return mSSL; } /** * SSLフラグを設定する. * @param SSL SSLが有効の場合はtrue、それ以外はfalse */ public void setSSL(final boolean SSL) { mSSL = SSL; } /** * Host名を取得する. * @return ホスト名 */ public String getHost() { return mHost; } /** * Host名を設定する. * @param host ホスト名 */ public void setHost(final String host) { mHost = host; } /** * ポート番号を取得する. * @return ポート番号 */ public int getPort() { return mPort; } /** * ポート番号を設定する. * @param port ポート番号 */ public void setPort(int port) { mPort = port; } public void openWebSocket(final AWSIotWebSocketClient.OnMessageEventListener listener) { if (mAWSIotWebSocketClient != null) { mAWSIotWebSocketClient.close(); } mOnMessageEventListener = listener; mAWSIotWebSocketClient = new AWSIotWebSocketClient(mPrefUtil.getAuthAccessToken()); mAWSIotWebSocketClient.setOnMessageEventListener(listener); mAWSIotWebSocketClient.connect(); } public void closeWebSocket() { if (mAWSIotWebSocketClient != null) { mAWSIotWebSocketClient.setOnMessageEventListener(null); mAWSIotWebSocketClient.close(); mAWSIotWebSocketClient = null; } } public void sendRequest(final String request, final ConversionCallback conversionCallback, final FinishCallback callback) { try { String method = null; URIBuilder builder = new URIBuilder(); builder.setScheme("http"); builder.setHost("localhost"); builder.setPort(4035); Map<String, String> body = new HashMap<>(); body.put(AWSIotUtil.PARAM_SELF_FLAG, "true"); if (mAuthInfo != null) { body.put(DConnectMessage.EXTRA_ACCESS_TOKEN, mAuthInfo.getAccessToken()); } JSONObject jsonObject = new JSONObject(request); Iterator<String> it = jsonObject.keys(); while (it.hasNext()) { String key = it.next(); if (key.equals("api")) { builder.setApi(jsonObject.getString(key)); } else if (key.equals("profile")) { builder.setProfile(jsonObject.getString(key)); } else if (key.equals("interface")) { builder.setInterface(jsonObject.getString(key)); } else if (key.equals("attribute")) { builder.setAttribute(jsonObject.getString(key)); } else if (key.equals("action")) { method = parseMethod(jsonObject); } else if (key.equals("sessionKey")) { if (conversionCallback != null) { body.put(key, conversionCallback.convertSessionKey(jsonObject.getString(key))); } else { body.put(key, jsonObject.getString(key)); } } else if (key.equals("uri")) { if (conversionCallback != null) { body.put(key, conversionCallback.convertUri(jsonObject.getString(key))); } else { body.put(key, jsonObject.getString(key)); } } else if (key.equals("requestCode")) { } else if (key.equals("origin")) { } else if (key.equals("accessToken")) { } else if (key.equals("_type")) { } else if (key.equals("_app_type")) { } else if (key.equals("receiver")) { } else if (key.equals("version")) { } else if (key.equals("product")) { } else { body.put(key, jsonObject.getString(key)); } } sendRequest(method, builder.toString(), body, callback); } catch (JSONException e) { if (callback != null) { callback.onFinish(errorMessage(), e); } } } public void sendRequest(final String method, final String uri, final FinishCallback callback) { sendRequest(method, uri, new HashMap<String, String>(), callback); } public void availability(final FinishCallback callback) { sendRequest("GET", "http://localhost:4035/gotapi/availability", callback); } public void getSystem(final FinishCallback callback) { Map<String, String> param = new HashMap<>(); if (mAuthInfo != null) { String accessToken = mAuthInfo.getAccessToken(); param.put("accessToken", accessToken); } sendRequest("GET", "http://localhost:4035/gotapi/system", param, callback); } public void serviceDiscovery(final FinishCallback callback) { sendRequest("GET", "http://localhost:4035/gotapi/servicediscovery", callback); } public void serviceDiscoverySelfOnly(final FinishCallback callback) { Map<String, String> param = new HashMap<>(); param.put("_selfOnly", "true"); sendRequest("GET", "http://localhost:4035/gotapi/servicediscovery", param, callback); } public void serviceInformation(final String serviceId, final FinishCallback callback) { Map<String, String> param = new HashMap<>(); param.put("serviceId", serviceId); sendRequest("GET", "http://localhost:4035/gotapi/serviceinformation", param, callback); } private void sendRequest(final String method, final String uri, final Map<String, String> body, final FinishCallback callback) { new HttpTask(method, uri, mDefaultHeader, body) { @Override protected void onPostExecute(final String message) { if (callback != null) { if (message == null) { callback.onFinish(errorMessage(), null); } else { callback.onFinish(message, null); } } } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private String parseMethod(final JSONObject jsonObject) throws JSONException { String method = jsonObject.getString("action"); method = method.replace("org.deviceconnect.action.", ""); return method.toLowerCase(); } private String errorMessage() { try { JSONObject jsonObject = new JSONObject(); jsonObject.put("result", 1); jsonObject.put("errorCode", 1); jsonObject.put("errorMessage", ""); return jsonObject.toString(); } catch (JSONException e) { return null; } } private String toBody(final Map<String, String> body) { if (body == null) { return ""; } String data = ""; for (String key : body.keySet()) { if (data.length() > 0) { data += "&"; } data += (key + "=" + body.get(key)); } return data; } private String toScope() { String data = ""; for (String scope : mScopes) { if (data.length() > 0) { data += ","; } data += scope; } return data; } private class HttpTask extends AsyncTask<Void, Void, String> { public String mMethod; public String mUri; public Map<String, String> mHeaders; public Map<String, String> mBody; public HttpTask(final String method, final String uri, final Map<String, String> headers, final Map<String, String> body) { mMethod = method.toLowerCase(); mUri = uri; mHeaders = headers; mBody = body; } private String executeRequest() { if (DEBUG) { Log.d(TAG, "method=" + mMethod); Log.d(TAG, "Uri=" + mUri); Log.d(TAG, "Body=" + toBody(mBody)); } byte[] resp = null; if ("get".equals(mMethod)) { resp = HttpUtil.get(mUri + "?" + toBody(mBody), mHeaders); } else if ("post".equals(mMethod)) { resp = HttpUtil.post(mUri, mHeaders, mBody); } else if ("put".equals(mMethod)) { resp = HttpUtil.put(mUri, mHeaders, mBody); } else if ("delete".equals(mMethod)) { resp = HttpUtil.delete(mUri + "?" + toBody(mBody), mHeaders); } return (resp != null) ? new String(resp) : null; } private String executeAccessToken(final String clientId) { String profile = getProfile(); if (profile != null) { mScopes.add(profile); } URIBuilder builder = new URIBuilder(); builder.setScheme("http"); builder.setHost("localhost"); builder.setPort(4035); builder.setProfile(AuthorizationProfile.PROFILE_NAME); builder.setAttribute(AuthorizationProfile.ATTRIBUTE_ACCESS_TOKEN); builder.addParameter(AuthorizationProfile.PARAM_CLIENT_ID, clientId); builder.addParameter(AuthorizationProfile.PARAM_SCOPE, toScope()); builder.addParameter(AuthorizationProfile.PARAM_APPLICATION_NAME, "aws"); byte[] data = HttpUtil.get(builder.toString(), mHeaders); if (data == null) { return null; } String response = new String(data); try { JSONObject jsonObject = new JSONObject(response); int result = jsonObject.getInt("result"); if (result == 0) { String accessToken = jsonObject.getString("accessToken"); mPrefUtil.setAuthAccessToken(accessToken); mPrefUtil.setAuthClientId(clientId); mAuthInfo = new AuthInfo(clientId, accessToken); mBody.put("accessToken", accessToken); mPrefUtil.setAuthAccessToken(accessToken); mPrefUtil.setAuthClientId(clientId); openWebSocket(mOnMessageEventListener); return executeRequest(); } } catch (JSONException e) { e.printStackTrace(); } return response; } private String executeGrant() { URIBuilder builder = new URIBuilder(); builder.setScheme("http"); builder.setHost("localhost"); builder.setPort(4035); builder.setProfile(AuthorizationProfile.PROFILE_NAME); builder.setAttribute(AuthorizationProfile.ATTRIBUTE_GRANT); byte[] data = HttpUtil.get(builder.toString(), mHeaders); if (data == null) { return null; } String response = new String(data); try { JSONObject jsonObject = new JSONObject(response); int result = jsonObject.getInt("result"); if (result == 0) { String clientId = jsonObject.getString("clientId"); return executeAccessToken(clientId); } } catch (JSONException e) { e.printStackTrace(); } return response; } private String getProfile() { Uri uri = Uri.parse(mUri); List<String> paths = uri.getPathSegments(); if (paths.size() > 1) { return paths.get(1); } return null; } @Override protected String doInBackground(final Void... params) { String response = executeRequest(); if (response != null) { try { JSONObject jsonObject = new JSONObject(response); int result = jsonObject.getInt("result"); if (result == DConnectMessage.RESULT_ERROR) { DConnectMessage.ErrorCode errorCode = DConnectMessage.ErrorCode.getInstance(jsonObject.getInt("errorCode")); switch (errorCode) { case SCOPE: case AUTHORIZATION: case EXPIRED_ACCESS_TOKEN: case EMPTY_ACCESS_TOKEN: case NOT_FOUND_CLIENT_ID: String resp = executeGrant(); if (resp != null) { response = resp; } break; } } } catch (JSONException e) { e.printStackTrace(); } } return response; } } /** * 認証情報. */ public static class AuthInfo { public String mClientId; public String mAccessToken; public AuthInfo(final String clientId, final String accessToken) { mClientId = clientId; mAccessToken = accessToken; } public String getAccessToken() { return mAccessToken; } @Override public String toString() { return "AuthInfo{" + "mClientId=" + mClientId + ", mAccessToken=" + mAccessToken + "}"; } } public interface ConversionCallback { String convertUri(String uri); String convertSessionKey(String sessionKey); } /** 処理完了コールバック */ public interface FinishCallback { /** * 処理が完了した時に呼ばれます. * @param response レスポンス * @param error エラー */ void onFinish(String response, Exception error); } private class URIBuilder { /** * スキーム. */ private String mScheme = isSSL() ? "https" : "http"; /** * ホスト. */ private String mHost = DConnectHelper.this.getHost(); /** * ポート番号. */ private int mPort = DConnectHelper.this.getPort(); /** * パス. */ private String mPath; /** * パラメータ. */ private Map<String, String> mParameters = new HashMap<>(); /** * API. */ private String mApi = DConnectMessage.DEFAULT_API; /** * プロファイル. */ private String mProfile; /** * インターフェース. */ private String mInterface; /** * アトリビュート. */ private String mAttribute; /** * コンストラクタ. */ URIBuilder() { } /** * URIから {@link URIBuilder} クラスを生成する. * * @param uri URI * @throws URISyntaxException URIフォーマットが不正な場合 */ URIBuilder(final String uri) throws URISyntaxException { this(new URI(uri)); } /** * URIから {@link URIBuilder} クラスを生成する. * * @param uri URI */ URIBuilder(final URI uri) { mScheme = uri.getScheme(); mHost = uri.getHost(); mPort = uri.getPort(); mPath = uri.getPath(); String query = uri.getQuery(); if (query != null) { String[] params = query.split("&"); for (String param : params) { String[] splitted = param.split("="); if (splitted.length == 2) { addParameter(splitted[0], splitted[1]); } else { addParameter(splitted[0], ""); } } } } @Override public synchronized String toString() { return toString(true); } /** * スキームを取得する. * * @return スキーム */ public synchronized String getScheme() { return mScheme; } /** * スキームを設定する. * * @param scheme スキーム * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setScheme(final String scheme) { mScheme = scheme; return this; } /** * ホスト名を取得する. * * @return ホスト名 */ public synchronized String getHost() { return mHost; } /** * ホスト名を設定する. * * @param host ホスト名 * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setHost(final String host) { if (host == null) { throw new NullPointerException("host is null."); } if (host.isEmpty()) { throw new IllegalArgumentException("host is empty."); } mHost = host; return this; } /** * ポート番号を取得する. ポート番号が指定されていない場合は-1を返す * * @return ポート番号 */ public synchronized int getPort() { return mPort; } /** * ポート番号を設定する. * * @param port ポート番号 * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setPort(final int port) { if (port < 0 || port > 65535) { throw new IllegalArgumentException("port is invalid. port=" + port); } mPort = port; return this; } /** * パスを取得する. * * @return パス */ public synchronized String getPath() { return mPath; } /** * APIのパスを文字列で設定する. * このパラメータが設定されている場合はビルド時に api、profile、interface、attribute は無視される。 * * @param path パス * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setPath(final String path) { mPath = path; return this; } /** * APIを取得する. * * @return API */ public synchronized String getApi() { return mApi; } /** * APIを取得する. * パスが設定されている場合には、このパラメータは無視される。 * * @param api API * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setApi(final String api) { mApi = api; return this; } /** * プロファイルを取得する. * * @return プロファイル */ public synchronized String getProfile() { return mProfile; } /** * プロファイルを設定する. * <p> * パスが設定されている場合には、このパラメータは無視される。 * * @param profile プロファイル * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setProfile(final String profile) { mProfile = profile; return this; } /** * インターフェースを取得する. * * @return インターフェース */ public synchronized String getInterface() { return mInterface; } /** * インターフェースを設定する. * <p> * パスが設定されている場合には、このパラメータは無視される。 * * @param inter インターフェース * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setInterface(final String inter) { mInterface = inter; return this; } /** * アトリビュートを取得する. * * @return アトリビュート */ public synchronized String getAttribute() { return mAttribute; } /** * アトリビュートを設定する. * <p> * {@link #setPath}でパスが設定されている場合には、このパラメータは無視される。 * * @param attribute アトリビュート * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setAttribute(final String attribute) { mAttribute = attribute; return this; } /** * サービスIDを設定する. * @param serviceId サービスID * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder setServiceId(final String serviceId) { addParameter(DConnectMessage.EXTRA_SERVICE_ID, serviceId); return this; } /** * サービスIDを取得する. * <p> * 設定されていない場合にはnullを返却する。 * </p> * @return サービスID */ public synchronized String getServiceId() { return getParameter(DConnectMessage.EXTRA_SERVICE_ID); } /** * 指定したクエリパラメータを取得する. * <p> * 指定されたクエリパラメータが存在しない場合にはnullを返却する。 * </p> * @param name クエリパラメータ名 * @return クエリパラメータ */ public synchronized String getParameter(final String name) { return mParameters.get(name); } /** * キーバリューでクエリパラメータを追加する. * * @param key キー * @param value バリュー * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder addParameter(final String key, final String value) { if (key == null) { throw new NullPointerException("key is null."); } if (value == null) { throw new NullPointerException("value is null."); } mParameters.put(key, value); return this; } /** * 指定されたクエリパラメータを削除する. * @param key クエリパラメータ名 * @return {@link URIBuilder} インスタンス */ public synchronized URIBuilder removeParameter(final String key) { if (key == null) { throw new NullPointerException("key is null."); } mParameters.remove(key); return this; } /** * {@link Uri} オブジェクトを取得する. * * @return {@link Uri} オブジェクト */ public Uri build() { return Uri.parse(toString(true)); } /** * URIを文字列にして取得する. * * @param ascii ASCII変換の有無 * @return URIを表す文字列 */ private synchronized String toString(final boolean ascii) { StringBuilder builder = new StringBuilder(); if (mScheme != null) { builder.append(mScheme); builder.append("://"); } if (mHost != null) { builder.append(mHost); } if (mPort > 0) { builder.append(":"); builder.append(mPort); } if (mPath != null) { builder.append(mPath); } else { if (mApi != null) { builder.append("/"); builder.append(mApi); } if (mProfile != null) { builder.append("/"); builder.append(mProfile); } if (mInterface != null) { builder.append("/"); builder.append(mInterface); } if (mAttribute != null) { builder.append("/"); builder.append(mAttribute); } } if (mParameters != null && mParameters.size() > 0) { if (ascii) { builder.append("?"); builder.append(concatenateStringWithEncode(mParameters, "UTF-8")); } else { builder.append("?"); builder.append(concatenateString(mParameters)); } } return builder.toString(); } private String concatenateString(final Map<String, String> map) { String string = ""; for (Map.Entry<String, String> e : map.entrySet()) { if (string.length() > 0) { string += "&"; } string += e.getKey() + "=" + e.getValue(); } return string; } private String concatenateStringWithEncode(final Map<String, String> map, final String charset) { try { String string = ""; for (Map.Entry<String, String> e : map.entrySet()) { if (string.length() > 0) { string += "&"; } string += e.getKey() + "=" + URLEncoder.encode(e.getValue(), charset); } return string; } catch (UnsupportedEncodingException e) { return ""; } } } }