/* IntentDConnectSDK.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.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; import org.deviceconnect.message.entity.BinaryEntity; import org.deviceconnect.message.entity.Entity; import org.deviceconnect.message.entity.FileEntity; import org.deviceconnect.message.entity.MultipartEntity; import org.deviceconnect.message.entity.StringEntity; import org.deviceconnect.message.intent.message.IntentDConnectMessage; import org.json.JSONException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** * Intentを使用してDevice Connect Managerと通信を行うSDKクラス. * @author NTT DOCOMO, INC. */ class IntentDConnectSDK extends DConnectSDK { /** * レスポンスを格納するマップ. */ private static Map<Integer, Intent> sResponseMap = new ConcurrentHashMap<>(); /** * イベントを配送するSDKを登録するリスト. */ private static final List<IntentDConnectSDK> sEventList = Collections.synchronizedList(new ArrayList<IntentDConnectSDK>()); /** * リスナーを登録するマップ. */ private Map<String, HttpDConnectSDK.OnEventListener> mListenerMap = new HashMap<>(); /** * コンテキスト. */ private Context mContext; /** * タイムアウト. */ private int mSoTimeout = 30 * 1000; /** * Device Connect Managerのパッケージ名. * TODO: 別のManagerへ送りたいときにどうするべきか検討 */ private String mManagerPackageName = "org.deviceconnect.android.manager"; /** * Device Connect Managerのクラス名. */ private String mManagerClassName = "org.deviceconnect.android.manager.DConnectBroadcastReceiver"; /** * イベントを通知するリスナー. */ private OnWebSocketListener mOnWebSocketListener; /** * コンストラクタ. * @param context コンテキスト */ IntentDConnectSDK(final Context context) { mContext = context; } /** * 接続先のDevice Connect Managerのパッケージ名を設定する. * @param managerPackageName パッケージ名 */ public void setManagerPackageName(final String managerPackageName) { mManagerPackageName = managerPackageName; } /** * 接続先のDevcie Connect Managerのクラス名を設定する. * @param managerClassName クラス名 */ public void setManagerClassName(final String managerClassName) { mManagerClassName = managerClassName; } @Override public void connectWebSocket(final OnWebSocketListener listener) { if (listener == null) { throw new NullPointerException("listener is null."); } else { mOnWebSocketListener = listener; sEventList.add(IntentDConnectSDK.this); listener.onOpen(); } } @Override public void disconnectWebSocket() { if (mOnWebSocketListener != null) { mOnWebSocketListener.onClose(); mOnWebSocketListener = null; } sEventList.remove(IntentDConnectSDK.this); } @Override public boolean isConnectedWebSocket() { return !sEventList.isEmpty(); } @Override public void addEventListener(final Uri uri, final OnEventListener listener) { if (uri == null) { throw new NullPointerException("uri is null."); } if (listener == null) { throw new NullPointerException("listener is null."); } put(uri, null, new OnResponseListener() { @Override public void onResponse(final DConnectResponseMessage response) { if (response.getResult() == DConnectMessage.RESULT_OK) { mListenerMap.put(convertUriToPath(uri), listener); } listener.onResponse(response); } }); } @Override public void removeEventListener(final Uri uri) { if (uri == null) { throw new NullPointerException("uri is null."); } delete(uri, new OnResponseListener() { @Override public void onResponse(final DConnectResponseMessage response) { } }); mListenerMap.remove(convertUriToPath(uri)); } @Override protected DConnectResponseMessage sendRequest(final Method method, final Uri uri, final Map<String, String> headers, final Entity body) { final int requestCode = UUID.randomUUID().hashCode(); String[] paths = parsePath(uri); String api; String profile; String interfaces = null; String attribute = null; if (paths.length == 2) { api = paths[0]; profile = paths[1]; } else if (paths.length == 3) { api = paths[0]; profile = paths[1]; attribute = paths[2]; } else if (paths.length == 4) { api = paths[0]; profile = paths[1]; interfaces = paths[2]; attribute = paths[3]; } else { throw new IllegalArgumentException("uri is invalid."); } Intent request = new Intent(); request.setAction(convertMethod(method)); request.setClassName(mManagerPackageName, mManagerClassName); request.putExtra(IntentDConnectMessage.EXTRA_API, api); request.putExtra(IntentDConnectMessage.EXTRA_PROFILE, profile); if (interfaces != null) { request.putExtra(IntentDConnectMessage.EXTRA_INTERFACE, interfaces); } if (attribute != null) { request.putExtra(IntentDConnectMessage.EXTRA_ATTRIBUTE, attribute); } if (uri.getQueryParameterNames() != null) { for (String key : uri.getQueryParameterNames()) { request.putExtra(key, uri.getQueryParameter(key)); } } if (getOrigin() != null) { request.putExtra(IntentDConnectMessage.EXTRA_ORIGIN, getOrigin()); } if (body != null && body instanceof MultipartEntity) { for (Map.Entry<String, Entity> data : (((MultipartEntity) body).getContent()).entrySet()) { String key = data.getKey(); Entity val = data.getValue(); if (val instanceof StringEntity) { request.putExtra(key, ((StringEntity) val).getContent()); } else if (val instanceof BinaryEntity) { request.putExtra(key, ((BinaryEntity) val).getContent()); } else if (val instanceof FileEntity) { request.putExtra(key, ((FileEntity) val).getContent()); } } } request.putExtra(IntentDConnectMessage.EXTRA_REQUEST_CODE, requestCode); request.putExtra(IntentDConnectMessage.EXTRA_RECEIVER, new ComponentName(mContext, DConnectMessageReceiver.class)); mContext.sendBroadcast(request); try { return new DConnectResponseMessage(waitForResponse(requestCode)); } catch (JSONException e) { return createErrorMessage(DConnectMessage.ErrorCode.UNKNOWN.getCode(), e.getMessage()); } catch (IOException e) { return createTimeoutResponse(); } } /** * URIからパスを抽出する. * @param uri パスを抽出するURI * @return パス */ private String convertUriToPath(final Uri uri) { return uri.getPath().toLowerCase(); } /** * URLパスを「/」で分割した配列を作成します. * <p> * 分割できない場合には、0の配列を返却します。 * </p> * * @param uri uri * @return パスの配列 */ private String[] parsePath(final Uri uri) { String path = uri.getPath(); if (path == null || !path.contains("/")) { return new String[0]; } return path.substring(1).split("/"); } /** * HttpメソッドをIntentのAction名に変換する. * @param method Httpメソッド * @return Action名 */ private String convertMethod(final Method method) { switch (method) { case GET: return IntentDConnectMessage.ACTION_GET; case PUT: return IntentDConnectMessage.ACTION_PUT; case POST: return IntentDConnectMessage.ACTION_POST; case DELETE: return IntentDConnectMessage.ACTION_DELETE; default: throw new IllegalArgumentException("Unknown method."); } } /** * レスポンスが返ってくるまで待ちます. * <p> * ただし、タイムアウトなどを起こした場合にはnullが返却される。 * </p> * * @param requestCode リクエストコード * @return レスポンス用のIntent */ private Intent waitForResponse(final int requestCode) throws IOException { long parseStart = System.currentTimeMillis(); while (sResponseMap.get(requestCode) == null && (mSoTimeout == 0 || mSoTimeout > System.currentTimeMillis() - parseStart)) { try { Thread.sleep(200); } catch (InterruptedException e) { throw new IOException(e); } } if (sResponseMap.get(requestCode) == null) { throw new IOException("response timeout"); } return sResponseMap.remove(requestCode); } private void onReceivedEvent(final Intent intent) { try { DConnectSDK.OnEventListener l = mListenerMap.get(createPath(intent)); if (l != null) { l.onMessage(new DConnectEventMessage(intent)); } } catch (JSONException e) { e.printStackTrace(); } } private String createPath(final Intent intent) { String profile = intent.getStringExtra(DConnectMessage.EXTRA_PROFILE); String interfaces = intent.getStringExtra(DConnectMessage.EXTRA_INTERFACE); String attribute = intent.getStringExtra(DConnectMessage.EXTRA_ATTRIBUTE); String uri = "/gotapi"; if (profile != null) { uri += "/"; uri += profile; } if (interfaces != null) { uri += "/"; uri += interfaces; } if (attribute != null) { uri += "/"; uri += attribute; } return uri.toLowerCase(); } /** * レスポンスを追加する. * * @param intent レスポンスインテント */ static void onReceivedResponse(final Intent intent) { String action = intent.getAction(); if (IntentDConnectMessage.ACTION_RESPONSE.equals(action)) { int requestCode = intent.getIntExtra(DConnectMessage.EXTRA_REQUEST_CODE, 0); if (requestCode != 0) { sResponseMap.put(requestCode, intent); } } else if (IntentDConnectMessage.ACTION_EVENT.equals(action)) { synchronized (sEventList) { for (IntentDConnectSDK sdk : sEventList) { sdk.onReceivedEvent(intent); } } } } }