/*
ThetaOmnidirectionalImageProfile.java
Copyright (c) 2015 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.deviceplugin.theta.profile;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.ResultReceiver;
import android.provider.Settings;
import org.deviceconnect.android.activity.IntentHandlerActivity;
import org.deviceconnect.android.deviceplugin.theta.ThetaDeviceService;
import org.deviceconnect.android.deviceplugin.theta.core.SphericalViewParam;
import org.deviceconnect.android.deviceplugin.theta.core.SphericalViewRenderer;
import org.deviceconnect.android.deviceplugin.theta.core.sensor.HeadTracker;
import org.deviceconnect.android.deviceplugin.theta.utils.MixedReplaceMediaServer;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.android.profile.OmnidirectionalImageProfile;
import org.deviceconnect.android.profile.api.DConnectApi;
import org.deviceconnect.android.profile.api.DeleteApi;
import org.deviceconnect.android.profile.api.GetApi;
import org.deviceconnect.android.profile.api.PutApi;
import org.deviceconnect.message.DConnectMessage;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
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;
/**
* Theta Omnidirectional Image Profile.
*
* @author NTT DOCOMO, INC.
*/
public class ThetaOmnidirectionalImageProfile extends OmnidirectionalImageProfile
implements MixedReplaceMediaServer.ServerEventListener {
private final Object mLockObj = new Object();
private MixedReplaceMediaServer mServer;
private Map<String, Viewer> mViewers = new HashMap<String, Viewer>();
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final HeadTracker mHeadTracker;
private final Handler mHandler;
private final DConnectApi mGetViewApi = new GetApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_ROI;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
requestView(request, response, getServiceID(request), getSource(request), true);
return false;
}
};
private final DConnectApi mPutViewApi = new PutApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_ROI;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
requestView(request, response, getServiceID(request), getSource(request), false);
return false;
}
};
private final DConnectApi mDeleteViewApi = new DeleteApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_ROI;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
Viewer viewer = mViewers.remove(omitParameters(getURI(request)));
if (viewer != null) {
viewer.stop();
mServer.stopMedia(viewer.getId());
}
setResult(response, DConnectMessage.RESULT_OK);
return true;
}
};
private final DConnectApi mPutSettingsApi = new PutApi() {
@Override
public String getInterface() {
return INTERFACE_ROI;
}
@Override
public String getAttribute() {
return ATTRIBUTE_SETTINGS;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final Viewer viewer = mViewers.get(omitParameters(getURI(request)));
if (viewer == null) {
MessageUtils.setInvalidRequestParameterError(response, "The specified media is not found.");
return true;
}
viewer.setParameter(parseParam(request));
setResult(response, DConnectMessage.RESULT_OK);
return true;
}
};
public ThetaOmnidirectionalImageProfile(final HeadTracker tracker) {
mHeadTracker = tracker;
mHandler = new Handler(Looper.getMainLooper());
addApi(mGetViewApi);
addApi(mPutViewApi);
addApi(mDeleteViewApi);
addApi(mPutSettingsApi);
}
private String startMediaServer() {
synchronized (mLockObj) {
if (mServer == null) {
mServer = new MixedReplaceMediaServer();
mServer.setServerName("ThetaDevicePlugin Server");
mServer.setContentType("image/jpeg");
mServer.setServerEventListener(ThetaOmnidirectionalImageProfile.this);
mServer.start();
}
}
return mServer.getUrl();
}
private void requestView(final Intent request, final Intent response, final String serviceId,
final String source, final boolean isGet) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
try {
final String serverUri = startMediaServer();
final String id = generateId();
final String resourceUri = serverUri + "/" + id;
final String[] outputs = parseOutputs(getOutput(request));
Projector projector;
if (isRequiredOverlay(outputs)) {
if (!checkOverlayPermission()) {
MessageUtils.setIllegalDeviceStateError(response, "Overlay is not allowed.");
((ThetaDeviceService) getContext()).sendResponse(response);
return;
}
projector = new OverlayProjector(getContext());
} else if (isRequiredMJPEG(outputs)) {
projector = new DefaultProjector();
} else {
MessageUtils.setInvalidRequestParameterError(response);
((ThetaDeviceService) getContext()).sendResponse(response);
return;
}
SphericalViewRenderer renderer = new SphericalViewRenderer();
renderer.setFlipVertical(true);
renderer.setStereoImageType(SphericalViewRenderer.StereoImageType.DOUBLE);
renderer.setScreenSizeMutable(true);
renderer.setScreenSettings(600, 400, false);
projector.setRenderer(renderer);
if (isRequiredMJPEG(outputs)) {
projector.setScreen(new ProjectionScreen() {
@Override
public void onStart(final Projector projector) {
}
@Override
public void onProjected(final Projector projector, final byte[] frame) {
mServer.offerMedia(id, frame);
}
@Override
public void onStop(final Projector projector) {
}
});
}
ImageViewer viewer = new ImageViewer(getContext());
viewer.setId(id);
viewer.setHeadTracker(mHeadTracker);
viewer.setImage(source);
viewer.setProjector(projector);
viewer.start();
mViewers.put(resourceUri, viewer);
setResult(response, DConnectMessage.RESULT_OK);
if (isGet) {
setURI(response, resourceUri + "?snapshot");
} else {
setURI(response, resourceUri);
}
} catch (MalformedURLException e) {
MessageUtils.setInvalidRequestParameterError(response, "uri is malformed: " + source);
} catch (FileNotFoundException e) {
MessageUtils.setInvalidRequestParameterError(response, "Image is not found: " + source);
} catch (IOException e) {
MessageUtils.setUnknownError(response, e.getMessage());
} catch (Throwable e) {
e.printStackTrace();
MessageUtils.setUnknownError(response, e.getMessage());
}
((ThetaDeviceService) getContext()).sendResponse(response);
}
});
}
private boolean checkOverlayPermission() {
final Context context = getContext();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(context)) {
return true;
}
final Boolean[] isPermitted = new Boolean[1];
final CountDownLatch lockObj = new CountDownLatch(1);
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context.getPackageName()));
IntentHandlerActivity.startActivityForResult(context, intent, new ResultReceiver(mHandler) {
@Override
protected void onReceiveResult(final int resultCode, final Bundle resultData) {
isPermitted[0] = Settings.canDrawOverlays(context);
lockObj.countDown();
}
});
try {
lockObj.await(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return false;
}
return isPermitted[0] != null && isPermitted[0];
} else {
return true;
}
}
private String generateId() {
return UUID.randomUUID().toString();
}
private String[] parseOutputs(final String outputParam) {
if (outputParam == null) {
return new String[]{"overlay", "mjpeg"};
}
return outputParam.split(",");
}
private boolean isRequiredOverlay(final String[] requiredOutputs) {
for (String output : requiredOutputs) {
if ("overlay".equals(output)) {
return true;
}
}
return false;
}
private boolean isRequiredMJPEG(final String[] requiredOutputs) {
for (String output : requiredOutputs) {
if ("mjpeg".equals(output)) {
return true;
}
}
return false;
}
@Override
public byte[] onConnect(final MixedReplaceMediaServer.Request request) {
String resourceUri = request.getUri();
Viewer viewer = mViewers.get(resourceUri);
if (viewer == null) {
return null;
}
byte[] cache = viewer.getImageCache();
mServer.offerMedia(viewer.getId(), cache);
return cache;
}
@Override
public void onDisconnect(final MixedReplaceMediaServer.Request request) {
if (!request.isGet()) {
Viewer viewer = mViewers.remove(request.getUri());
if (viewer != null) {
viewer.stop();
}
}
}
@Override
public void onCloseServer() {
mViewers.clear();
}
private String omitParameters(final String uri) {
if (uri == null) {
return null;
}
int index = uri.indexOf("?");
if (index >= 0) {
return uri.substring(0, index);
}
return uri;
}
private SphericalViewParam parseParam(final Intent request) {
Double x = getX(request);
Double y = getY(request);
Double z = getZ(request);
Double roll = getRoll(request);
Double pitch = getPitch(request);
Double yaw = getYaw(request);
Double fov = getFOV(request);
Double sphereSize = getSphereSize(request);
Integer width = getWidth(request);
Integer height = getHeight(request);
Boolean stereo = getStereo(request);
Boolean vr = getVR(request);
SphericalViewParam param = new SphericalViewParam();
if (x != null) {
param.setCameraX(x);
}
if (y != null) {
param.setCameraY(y);
}
if (z != null) {
param.setCameraZ(z);
}
if (roll != null && !param.isVRMode()) {
param.setCameraRoll(roll);
}
if (pitch != null && !param.isVRMode()) {
param.setCameraPitch(pitch);
}
if (yaw != null && !param.isVRMode()) {
param.setCameraYaw(yaw);
}
if (fov != null) {
param.setFOV(fov);
}
if (sphereSize != null) {
param.setSphereSize(sphereSize);
}
if (width != null) {
param.setWidth(width);
}
if (height != null) {
param.setHeight(height);
}
if (stereo != null) {
param.setStereo(stereo);
}
if (vr != null) {
param.setVRMode(vr);
}
return param;
}
public void forceStopPreview() {
/** プレビュー停止処理 */
mExecutor.execute(new Runnable() {
@Override
public void run() {
for (Map.Entry<String, Viewer> entry : mViewers.entrySet()) {
Viewer viewer = entry.getValue();
if (viewer != null) {
viewer.stop();
mServer.stopMedia(viewer.getId());
mViewers.remove(viewer.getId());
}
}
}
});
}
}