/* WearCanvasProfile.java Copyright (c) 2015 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.wear.profile; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.MessageApi; import com.google.android.gms.wearable.NodeApi; import org.deviceconnect.android.deviceplugin.wear.WearDeviceService; import org.deviceconnect.android.deviceplugin.wear.WearManager; import org.deviceconnect.android.message.MessageUtils; import org.deviceconnect.android.profile.CanvasProfile; import org.deviceconnect.android.profile.api.DConnectApi; import org.deviceconnect.android.profile.api.DeleteApi; import org.deviceconnect.android.profile.api.PostApi; import org.deviceconnect.message.DConnectMessage; import java.io.ByteArrayOutputStream; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; /** * Android Wear用のCanvasプロファイル. * * @author NTT DOCOMO, INC. */ public class WearCanvasProfile extends CanvasProfile { /** * Android wearは1MB以上の画像は送信できない. */ private static final int LIMIT_DATA_SIZE = 1024 * 1024; private final Logger mLogger = Logger.getLogger("dconnect.wear"); private ExecutorService mImageService = Executors.newSingleThreadExecutor(); private final Map<String, DrawImageRequest> mRequestMap = new HashMap<>(); private final WearManager mWearManager; public WearCanvasProfile(final WearManager mgr) { mWearManager = mgr; mgr.addMessageEventListener(WearConst.WEAR_TO_DEVICE_CANVAS_RESULT, new WearManager.OnMessageEventListener() { @Override public void onEvent(final String nodeId, final String message) { mLogger.info("onEvent: message = " + message); onCanvasResponse(nodeId, message); } }); addApi(mPostDrawImage); addApi(mDeleteDrawImage); } private DConnectApi mPostDrawImage = new PostApi() { @Override public String getAttribute() { return ATTRIBUTE_DRAW_IMAGE; } @Override public boolean onRequest(final Intent request, final Intent response) { final String nodeId = WearUtils.getNodeId(getServiceID(request)); final byte[] data = getData(request); final double x = getX(request); final double y = getY(request); final String mode = getMode(request); String mimeType = getMIMEType(request); if (mimeType != null && !mimeType.contains("image")) { MessageUtils.setInvalidRequestParameterError(response, "Unsupported mimeType: " + mimeType); return true; } if (data == null) { mImageService.execute(new Runnable() { @Override public void run() { String uri = getURI(request); byte[] result = getData(uri); if (result == null) { MessageUtils.setInvalidRequestParameterError(response, "could not get image from uri."); sendResponse(response); return; } drawImage(response, nodeId, result, x, y, mode); } }); return false; } else { drawImage(response, nodeId, data, x, y, mode); return false; } } }; private void drawImage(final Intent response, final String nodeId, final byte[] data, final double x, final double y, final String mode) { mWearManager.getLocalNodeId(new WearManager.OnLocalNodeListener() { @Override public void onResult(final NodeApi.GetLocalNodeResult localNode) { final String localNodeId = localNode.getNode().getId(); if (data.length > LIMIT_DATA_SIZE) { MessageUtils.setInvalidRequestParameterError(response, "data size more than 1MB"); sendResponse(response); return; } Mode m = Mode.getInstance(mode); if ((mode != null && mode.length() > 0) && m == null) { MessageUtils.setInvalidRequestParameterError(response, "mode is invalid"); sendResponse(response); return; } //for check binary Bitmap bitmap; try { bitmap = getBitmap(data); } catch (OutOfMemoryError e) { MessageUtils.setInvalidRequestParameterError(response, "Too large bitmap for host device."); sendResponse(response); return; } if (bitmap == null) { MessageUtils.setInvalidRequestParameterError(response, "format invalid"); sendResponse(response); return; } int mm = WearUtils.convertMode(m); //Adjust image format and compress ByteArrayOutputStream o = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 50, o); final byte[] bitmapData = o.toByteArray(); final String requestId = UUID.randomUUID().toString(); final DrawImageRequest wearRequest = createCanvasRequest(nodeId, requestId); getManager().sendImageData(localNodeId, requestId, bitmapData, (int) x, (int) y, mm, new WearManager.OnDataItemResultListener() { @Override public void onResult(final DataApi.DataItemResult result) { if (result.getStatus().isSuccess()) { try { DrawImageResponse wearResponse = wearRequest.await(); if (wearResponse.isSuccess()) { setResult(response, DConnectMessage.RESULT_OK); } else { int errorCode = wearResponse.getErrorCode(); String errorMessage = wearResponse.getErrorMessage(); MessageUtils.setError(response, errorCode, errorMessage); } } catch (Exception e) { MessageUtils.setUnknownError(response); } } else { MessageUtils.setIllegalDeviceStateError(response); } sendResponse(response); } @Override public void onError() { MessageUtils.setIllegalDeviceStateError(response); sendResponse(response); } }); } @Override public void onError() { MessageUtils.setUnknownError(response, "Failed to get Local Node ID."); } }); } private final DConnectApi mDeleteDrawImage = new DeleteApi() { @Override public String getAttribute() { return ATTRIBUTE_DRAW_IMAGE; } @Override public boolean onRequest(final Intent request, final Intent response) { String nodeId = WearUtils.getNodeId(getServiceID(request)); getManager().sendMessageToWear(nodeId, WearConst.DEVICE_TO_WEAR_CANCAS_DELETE_IMAGE, "", new WearManager.OnMessageResultListener() { @Override public void onResult(final MessageApi.SendMessageResult result) { if (result.getStatus().isSuccess()) { setResult(response, DConnectMessage.RESULT_OK); } else { MessageUtils.setIllegalDeviceStateError(response); } sendResponse(response); } @Override public void onError() { MessageUtils.setIllegalDeviceStateError(response); sendResponse(response); } }); return false; } }; /** * データを画像に変換します. * * @param data 画像データ * @return Bitmap */ private Bitmap getBitmap(final byte[] data) { return BitmapFactory.decodeByteArray(data, 0, data.length); } /** * Android Wear管理クラスを取得する. * * @return WearManager管理クラス */ private WearManager getManager() { return ((WearDeviceService) getContext()).getManager(); } private DrawImageRequest createCanvasRequest(final String nodeId, final String requestId) { DrawImageRequest request = new DrawImageRequest(nodeId); mRequestMap.put(requestId, request); return request; } private void onCanvasResponse(final String nodeId, final String message) { String[] items = message.split(","); String requestId = items[0]; String result = items[1]; DrawImageRequest request = mRequestMap.get(requestId); if (request == null) { mLogger.warning("onCanvasImageResponse: request is not found: nodeId = " + nodeId); return; } if (!request.getNodeId().equals(nodeId)) { mLogger.warning("onCanvasImageResponse: nodeId are not matched for request: requestId = " + requestId); return; } request.receive(result); mRequestMap.remove(requestId); } private static class DrawImageRequest { private CountDownLatch mLock = new CountDownLatch(1); private DrawImageResponse mResponse; private final String mNodeId; public DrawImageRequest(final String nodeId) { mNodeId = nodeId; } public DrawImageResponse await() throws InterruptedException, ResponseTimeoutException { mLock.await(30, TimeUnit.SECONDS); if (!hasResponse()) { throw new ResponseTimeoutException(); } return mResponse; } public void receive(final String message) { mResponse = new DrawImageResponse(message); mLock.countDown(); } public boolean hasResponse() { return mResponse != null; } public String getNodeId() { return mNodeId; } } private static class DrawImageResponse { private final String mResult; private final int mErrorCode; private final String mErrorMessage; public DrawImageResponse(final String message) { mResult = message; if (WearConst.RESULT_ERROR_TOO_LARGE_BITMAP.equals(message)) { mErrorCode = DConnectMessage.ErrorCode.INVALID_REQUEST_PARAMETER.getCode(); mErrorMessage = "Too large bitmap for watch."; } else if (WearConst.RESULT_ERROR_CONNECTION_FAILURE.equals(message)) { mErrorCode = DConnectMessage.ErrorCode.ILLEGAL_DEVICE_STATE.getCode(); mErrorMessage = "Connection failure."; } else if (WearConst.RESULT_ERROR_NOT_SUPPORTED_FORMAT.equals(message)) { mErrorCode = DConnectMessage.ErrorCode.INVALID_REQUEST_PARAMETER.getCode(); mErrorMessage = "Not supported format."; } else { mErrorCode = 0; mErrorMessage = ""; } } public boolean isSuccess() { return WearConst.RESULT_SUCCESS.equals(mResult); } public int getErrorCode() { return mErrorCode; } public String getErrorMessage() { return mErrorMessage; } } private static class ResponseTimeoutException extends TimeoutException { } }