package org.deviceconnect.android.deviceplugin.theta.core; import android.content.Context; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.os.Build; import com.theta360.lib.PtpipInitiator; import com.theta360.lib.ThetaException; import com.theta360.lib.ptpip.entity.DeviceInfo; import com.theta360.lib.ptpip.entity.ObjectHandles; import com.theta360.lib.ptpip.entity.ObjectInfo; import com.theta360.lib.ptpip.entity.PtpObject; import com.theta360.lib.ptpip.eventlistener.PtpipEventListener; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.concurrent.CountDownLatch; import javax.net.SocketFactory; class ThetaM15 extends AbstractThetaDevice { private static final String ID_PREFIX = "theta-m15-"; private static final String HOST = "192.168.1.1"; private static final String BRAND_SAMSUNG = "samsung"; private static final String MANUFACTURER_SAMSUNG = "samsung"; private static final SimpleDateFormat BEFORE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); private static final SimpleDateFormat AFTER_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); private static final VersionName VERSION_5_MIN_VIDEO = new VersionName("01.30"); private final SocketFactory mSocketFactory; private final Comparator<ThetaObject> mComparator = new Comparator<ThetaObject>() { @Override public int compare(final ThetaObject o1, final ThetaObject o2) { Long t1 = new Long(o1.getCreationTimeWithUnixTime()); Long t2 = new Long(o2.getCreationTimeWithUnixTime()); return t2.compareTo(t1); } }; private VersionName mDeviceVersion; private Recorder mRecorder; ThetaM15(final Context context, final String ssId) { super(ssId); mSocketFactory = getWifiSocketFactory(context); } public boolean initialize() { mDeviceVersion = getDeviceVersion(); if (mDeviceVersion == null) { return false; } mRecorder = detectRecorder(); if (mRecorder == null) { return false; } return true; } private PtpipInitiator getInitiator() throws ThetaException, IOException { // TODO Null check for mSocketFactory. return new PtpipInitiator(mSocketFactory, HOST); } private static SocketFactory getWifiSocketFactory(final Context context) { SocketFactory socketFactory = SocketFactory.getDefault(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !isGalaxyDevice()) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); Network[] allNetwork = cm.getAllNetworks(); for (Network network : allNetwork) { NetworkCapabilities networkCapabilities = cm.getNetworkCapabilities(network); if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { socketFactory = network.getSocketFactory(); } } } return socketFactory; } private static boolean isGalaxyDevice() { if ((Build.BRAND != null) && (Build.BRAND.toLowerCase(Locale.ENGLISH).contains(BRAND_SAMSUNG))) { return true; } if ((Build.MANUFACTURER != null) && (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains(MANUFACTURER_SAMSUNG))) { return true; } return false; } @Override public String getId() { return ID_PREFIX + mSSID; } @Override public ThetaDeviceModel getModel() { return ThetaDeviceModel.THETA_M15; } @Override public List<ThetaObject> fetchAllObjectList() throws ThetaDeviceException { try { PtpipInitiator initiator = getInitiator(); ObjectHandles objectHandles = initiator.getObjectHandles( PtpipInitiator.PARAMETER_VALUE_DEFAULT, PtpipInitiator.PARAMETER_VALUE_DEFAULT, PtpipInitiator.PARAMETER_VALUE_DEFAULT); List<ThetaObject> result = new ArrayList<ThetaObject>(); for (int i = 0; i < objectHandles.size(); i++) { int objectHandle = objectHandles.getObjectHandle(i); ObjectInfo objectInfo = initiator.getObjectInfo(objectHandle); ThetaObject object = parseDevice(objectHandle, objectInfo); if (object != null) { result.add(object); } } Collections.sort(result, mComparator); return result; } catch (IOException e) { try { PtpipInitiator.close(); } catch (ThetaException e1) { e1.printStackTrace(); } throw new ThetaDeviceException(ThetaDeviceException.IO_ERROR, e); } catch (ThetaException e) { try { PtpipInitiator.close(); } catch (ThetaException e1) { e1.printStackTrace(); } throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } private ThetaObject parseDevice(final int handle, final ObjectInfo info) { String name = info.getFilename(); String date = info.getCaptureDate(); int width = info.getImagePixWidth(); int height = info.getImagePixHeight(); int thumbnailFormat = info.getThumbFormat(); int objectFormat = info.getObjectFormat(); if (thumbnailFormat != 0) { if (objectFormat == ObjectInfo.OBJECT_FORMAT_CODE_EXIF_JPEG) { return new ThetaImageObject(handle, name, date, width, height); } else { return new ThetaVideoObject(handle, name, date, width, height); } } else { return null; } } @Override public List<ThetaObject> fetchObjectList(int offset, int maxLength) throws ThetaDeviceException { throw new UnsupportedOperationException(); } @Override public ThetaObject takePicture() throws ThetaDeviceException { try { final CountDownLatch lockObj = new CountDownLatch(1); final PtpipInitiator initiator = getInitiator(); final ThetaObject[] photo = new ThetaObject[1]; if (initiator.getStillCaptureMode() == PtpipInitiator.DEVICE_PROP_VALUE_UNDEFINED_CAPTURE_MODE) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN); } initiator.setStillCaptureMode(PtpipInitiator.DEVICE_PROP_VALUE_SINGLE_CAPTURE_MODE); initiator.initiateCapture(new PtpipEventListener() { @Override public void onObjectAdded(final int handle) { try { ObjectInfo info = initiator.getObjectInfo(handle); photo[0] = parseDevice(handle, info); } catch (ThetaException e) { e.printStackTrace(); // Nothing to do. } finally { lockObj.countDown(); } } }); lockObj.await(); if (photo[0] != null) { return photo[0]; } else { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN); } } catch (IOException e) { throw new ThetaDeviceException(ThetaDeviceException.IO_ERROR, e); } catch (ThetaException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } catch (InterruptedException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } @Override public void startVideoRecording() throws ThetaDeviceException { try { PtpipInitiator initiator = getInitiator(); final int mode = initiator.getStillCaptureMode(); if (mode != PtpipInitiator.DEVICE_PROP_VALUE_UNDEFINED_CAPTURE_MODE) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN); } final short status = initiator.getCaptureStatus(); if (status == PtpipInitiator.DEVICE_PROP_VALUE_CAPTURE_STATUS_CONTINUOUS_SHOOTING_RUNNING) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN); } initiator.initiateOpenCapture(); } catch (IOException e) { throw new ThetaDeviceException(ThetaDeviceException.IO_ERROR, e); } catch (ThetaException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } @Override public void stopVideoRecording() throws ThetaDeviceException { try { PtpipInitiator initiator = getInitiator(); final short status = initiator.getCaptureStatus(); if (status == PtpipInitiator.DEVICE_PROP_VALUE_CAPTURE_STATUS_WAIT) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN); } initiator.terminateOpenCapture(); } catch (IOException e) { throw new ThetaDeviceException(ThetaDeviceException.IO_ERROR, e); } catch (ThetaException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } @Override public long getMaxVideoLength() { return is5minVideo() ? 5 * 60 * 1000 : 3 * 60 * 1000; } private boolean is5minVideo() { return mDeviceVersion.isSameOrLaterThan(VERSION_5_MIN_VIDEO); } @Override public double getBatteryLevel() throws ThetaDeviceException { try { return getInitiator().getBatteryLevel().getValue() / 100d; } catch (IOException e) { throw new ThetaDeviceException(ThetaDeviceException.IO_ERROR, e); } catch (ThetaException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } @Override public ShootingMode getShootingMode() throws ThetaDeviceException { try { PtpipInitiator ptpIp = getInitiator(); short captureMode = ptpIp.getStillCaptureMode(); ShootingMode mode; switch (captureMode) { case PtpipInitiator.DEVICE_PROP_VALUE_SINGLE_CAPTURE_MODE: mode = ShootingMode.IMAGE; break; case PtpipInitiator.DEVICE_PROP_VALUE_TIMELAPSE_CAPTURE_MODE: mode = ShootingMode.IMAGE_INTERVAL; break; case PtpipInitiator.DEVICE_PROP_VALUE_UNDEFINED_CAPTURE_MODE: mode = ShootingMode.VIDEO; break; default: mode = ShootingMode.UNKNOWN; break; } return mode; } catch (IOException e) { throw new ThetaDeviceException(ThetaDeviceException.IO_ERROR, e); } catch (ThetaException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } @Override public void changeShootingMode(final ShootingMode mode) throws ThetaDeviceException { throw new UnsupportedOperationException(); } @Override public Recorder getRecorder() throws ThetaDeviceException { return mRecorder; } @Override public InputStream getLiveStream() throws IOException { throw new UnsupportedOperationException(); } private VersionName getDeviceVersion() { DeviceInfo deviceInfo; try { deviceInfo = getInitiator().getDeviceInfo(); } catch (IOException e) { return null; } catch (ThetaException e) { return null; } return new VersionName(deviceInfo.getDeviceVersion()); } private Recorder detectRecorder() { try { ShootingMode mode = getShootingMode(); switch (mode) { case IMAGE: return new ThetaImageRecorderM15("0"); case VIDEO: return new ThetaVideoRecorderM15("1"); default: return null; } } catch (ThetaDeviceException e) { return null; } } @Override public void destroy() { try { PtpipInitiator.close(); } catch (ThetaException e) { // Nothing to do. } } private class ThetaImageObject extends ThetaObjectM15 { private static final String MIMETYPE_IMAGE = "image/jpeg"; public ThetaImageObject(final int handle, final String filename, final String date, final int width, final int height) { super(handle, filename, date, width, height); } @Override public String getMimeType() { return MIMETYPE_IMAGE; } @Override public Boolean isImage() { return true; } } private class ThetaVideoObject extends ThetaObjectM15 { private static final String MIMETYPE_VIDEO = "video/mpeg"; public ThetaVideoObject(final int handle, final String filename, final String date, final int width, final int height) { super(handle, filename, date, width, height); } @Override public String getMimeType() { return MIMETYPE_VIDEO; } @Override public Boolean isImage() { return false; } } private abstract class ThetaObjectM15 implements ThetaObject { private final int mObjectHandle; private final String mFilename; private final String mDateTime; private long mDateTimeUnix; private final int mWidth; private final int mHeight; private byte[] mThumbnail; private byte[] mMain; public ThetaObjectM15(final int handle, final String filename, final String date, final int width, final int height) { mObjectHandle = handle; mFilename = filename; mWidth = width; mHeight = height; String dateTime; try { dateTime = AFTER_FORMAT.format(BEFORE_FORMAT.parse(date)); mDateTimeUnix = AFTER_FORMAT.parse(dateTime).getTime(); } catch (ParseException e) { dateTime = ""; mDateTimeUnix = 0; } mDateTime = dateTime; } protected int getHandle() { return mObjectHandle; } @Override public void fetch(final DataType type) throws ThetaDeviceException { try { switch (type) { case THUMBNAIL: PtpObject obj = getInitiator().getThumb(getHandle()); mThumbnail = obj.getDataObject(); break; case MAIN: mMain = getInitiator().getObject(getHandle()); break; default: throw new IllegalArgumentException(); } } catch (IOException e) { throw new ThetaDeviceException(ThetaDeviceException.IO_ERROR, e); } catch (ThetaException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } @Override public boolean isFetched(final DataType type) { switch (type) { case THUMBNAIL: return mThumbnail != null; case MAIN: return mMain != null; default: throw new IllegalArgumentException(); } } @Override public void remove() throws ThetaDeviceException { try { getInitiator().deleteObject(getHandle(), PtpipInitiator.PARAMETER_VALUE_DEFAULT); PtpipInitiator.close(); } catch (IOException e) { throw new ThetaDeviceException(ThetaDeviceException.IO_ERROR, e); } catch (ThetaException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } @Override public void clear(final DataType type) { switch (type) { case THUMBNAIL: mThumbnail = null; break; case MAIN: mMain = null; break; default: throw new IllegalArgumentException(); } } @Override public String getCreationTime() { return mDateTime; } @Override public long getCreationTimeWithUnixTime() { return mDateTimeUnix; } @Override public String getFileName() { return mFilename; } @Override public Integer getWidth() { return mWidth; } @Override public Integer getHeight() { return mHeight; } @Override public byte[] getThumbnailData() { return mThumbnail; } @Override public byte[] getMainData() { return mMain; } } private static class VersionName { private static final Comparator<VersionName> COMPARATOR = new Comparator<VersionName>() { @Override public int compare(final VersionName v1, final VersionName v2) { VersionName a = v1; VersionName b = v2; if (a.getLength() > b.getLength()) { VersionName tmp = a; a = b; b = tmp; } for (int i = 0; i < b.getLength(); i++) { int valueA = (i <= a.getLength() - 1) ? a.mVersions[i] : 0; int valueB = b.mVersions[i]; if (valueA > valueB) { return 1; } else if (valueA < valueB) { return -1; } } return 0; } }; private final int[] mVersions; public VersionName(final String expression) { mVersions = parse(expression); } private int getLength() { return mVersions.length; } private static int[] parse(final String expression) { String[] versions = expression.split("\\."); int[] result = new int[versions.length]; for (int i = 0; i < versions.length; i++) { result[i] = Integer.parseInt(versions[i]); } return result; } public boolean isSameOrLaterThan(final VersionName otherVersion) { return COMPARATOR.compare(this, otherVersion) > 0; } } private class ThetaImageRecorderM15 extends ThetaRecorderM15 { private static final String NAME = "THETA m15 - photo"; private static final String MIME_TYPE = "image/jpeg"; private static final int IMAGE_WIDTH = 2048; private static final int IMAGE_HEIGHT = 1024; public ThetaImageRecorderM15(final String id) { super(id, NAME, MIME_TYPE, IMAGE_WIDTH, IMAGE_HEIGHT); } @Override public boolean supportsVideoRecording() { return false; } @Override public boolean supportsPhoto() { return true; } @Override public RecorderState getState() throws ThetaDeviceException { return RecorderState.INACTIVE; } } private class ThetaVideoRecorderM15 extends ThetaRecorderM15 { private static final String NAME = "THETA m15 - video"; private static final String MIME_TYPE = "video/mov"; private static final int IMAGE_WIDTH = 1920; private static final int IMAGE_HEIGHT = 1080; public ThetaVideoRecorderM15(final String id) { super(id, NAME, MIME_TYPE, IMAGE_WIDTH, IMAGE_HEIGHT); } @Override public boolean supportsVideoRecording() { return true; } @Override public boolean supportsPhoto() { return false; } @Override public RecorderState getState() throws ThetaDeviceException { try { short status = getInitiator().getCaptureStatus(); switch (status) { case 1: return RecorderState.RECORDING; case 0: return RecorderState.INACTIVE; default: return RecorderState.UNKNOWN; } } catch (ThetaException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } catch (IOException e) { throw new ThetaDeviceException(ThetaDeviceException.UNKNOWN, e); } } } private abstract class ThetaRecorderM15 implements Recorder { private final String mId; private final String mName; private final String mMimeType; private final int mImageWidth; private final int mImageHeight; public ThetaRecorderM15(final String id, final String name, final String mimeType, final int imageWidth, final int imageHeight) { mId = id; mName = name; mMimeType = mimeType; mImageWidth = imageWidth; mImageHeight = imageHeight; } @Override public String getId() { return mId; } @Override public String getName() { return mName; } @Override public String getMimeType() { return mMimeType; } @Override public int getImageWidth() { return mImageWidth; } @Override public int getImageHeight() { return mImageHeight; } @Override public int getPreviewWidth() { return 0; } @Override public int getPreviewHeight() { return 0; } @Override public double getPreviewMaxFrameRate() { return 0; } @Override public boolean supportsPreview() { return false; } } }