/* CameraOverlay.java Copyright (c) 2014 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.host.recorder.camera; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.graphics.YuvImage; import android.hardware.Camera; import android.hardware.Camera.Parameters; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.NonNull; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.Surface; import android.view.View; import android.view.WindowManager; import org.deviceconnect.android.deviceplugin.host.BuildConfig; import org.deviceconnect.android.deviceplugin.host.recorder.HostDeviceRecorder; import org.deviceconnect.android.deviceplugin.host.recorder.util.CapabilityUtil; import org.deviceconnect.android.deviceplugin.host.recorder.util.MixedReplaceMediaServer; import org.deviceconnect.android.provider.FileManager; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.concurrent.Executors; import java.util.logging.Logger; /** * カメラのプレビューをオーバーレイで表示するクラス. * * @author NTT DOCOMO, INC. */ @SuppressWarnings("deprecation") public class CameraOverlay implements Camera.PreviewCallback, Camera.ErrorCallback { /** * JPEGの圧縮クオリティを定義. */ private static final int JPEG_COMPRESS_QUALITY = 100; /** ファイル名に付けるプレフィックス. */ private static final String FILENAME_PREFIX = "android_camera_"; /** ファイルの拡張子. */ private static final String FILE_EXTENSION = ".jpg"; /** Default Maximum Frame Rate. */ private static final double DEFAULT_MAX_FPS = 10.0d; /** 日付のフォーマット. */ private SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("yyyyMMdd_kkmmss", Locale.JAPAN); /** コンテキスト. */ private final Context mContext; /** ウィンドウ管理クラス. */ private final WindowManager mWinMgr; /** 作業用スレッド。 */ private final HandlerThread mWorkerThread; /** ハンドラ. */ private final Handler mHandler; /** Camera ID. */ private final int mCameraId; /** ファイル管理クラス. */ private FileManager mFileMgr; /** プレビュー画面. */ private Preview mPreview; /** 使用するカメラのインスタンス. */ private Camera mCamera; /** * カメラの操作をブロックするロックオブジェクト. */ private final Object mCameraLock = new Object(); /** * 画像を送るサーバ. */ private MixedReplaceMediaServer mServer; /** * プレビューサイズ. */ private HostDeviceRecorder.PictureSize mPreviewSize; /** * 写真サイズ. */ private HostDeviceRecorder.PictureSize mPictureSize; /** * 最後のフレームを取得した時間. */ private long mLastFrameTime; /** * インターバル. */ private long mFrameInterval; /** * プレビューのFPS. */ private double mMaxFps; /** * JPEGのクォリティ. */ private int mJpegQuality = JPEG_COMPRESS_QUALITY; /** * カメラの向き. */ private int mFacingDirection = 1; /** * ロガー. */ private final Logger mLogger = Logger.getLogger("host.dplugin"); /** * プレビュー表示モード. */ private boolean mPreviewMode; /** * カメラパラメーター. */ Camera.Parameters mParams; /** * フラッシュライト使用中フラグ. */ private boolean mUseFlashLight = false; /** * フラッシュライト状態. */ private boolean mFlashLightState = false; /** * 画面回転のイベントを受け付けるレシーバー. */ private BroadcastReceiver mOrientReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { updatePosition(mPreview); } } }; /** * コンストラクタ. * * @param context コンテキスト * @param cameraId Camera ID. */ public CameraOverlay(final Context context, final int cameraId) { mContext = context; mWinMgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mWorkerThread = new HandlerThread(getClass().getSimpleName()); mWorkerThread.start(); mHandler = new Handler(mWorkerThread.getLooper()); mCameraId = cameraId; mMaxFps = DEFAULT_MAX_FPS; setPreviewFrameRate(mMaxFps); } @Override protected void finalize() throws Throwable { mWorkerThread.quit(); super.finalize(); } /** * プレビューモードを設定します. * * @param flag trueの場合はプレビューモードをON、それ以外はプレビューモードをOFF */ public void setPreviewMode(final boolean flag) { mPreviewMode = flag; } public void setFacingDirection(final int dir) { mFacingDirection = dir; } public HostDeviceRecorder.PictureSize getPictureSize() { return mPictureSize; } public void setPictureSize(final HostDeviceRecorder.PictureSize size) { mPictureSize = size; } public HostDeviceRecorder.PictureSize getPreviewSize() { return mPreviewSize; } public void setPreviewSize(final HostDeviceRecorder.PictureSize size) { mPreviewSize = size; } public void setPreviewFrameRate(final double max) { mMaxFps = max; mFrameInterval = (long) (1 / max) * 1000L; } public double getPreviewMaxFrameRate() { return mMaxFps; } public void setJpegQuality(final int jpegQuality) { mJpegQuality = jpegQuality; } /** * MixedReplaceMediaServerを設定する. * * @param server サーバのインスタンス */ public void setServer(final MixedReplaceMediaServer server) { mServer = server; } /** * FileManagerを設定する. * * @param mgr FileManagerのインスタンス */ public void setFileManager(final FileManager mgr) { mFileMgr = mgr; } /** * カメラのオーバーレイが表示されているかを確認する. * * @return 表示されている場合はtrue、それ以外はfalse */ public synchronized boolean isShow() { return mPreview != null; } /** * Overlayを表示する. * @param callback Overlayの表示結果を通知するコールバック */ public void show(final Callback callback) { Executors.newSingleThreadExecutor().submit(new Runnable() { @Override public void run() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { CapabilityUtil.checkCapability(mContext, mHandler, new CapabilityUtil.Callback() { @Override public void onSuccess() { try { showInternal(callback); } catch (IOException e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } } } @Override public void onFail() { callback.onFail(); } }); } else { try { showInternal(callback); } catch (IOException e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } } } } }); } private void showInternal(final Callback callback) throws IOException { mHandler.post(new Runnable() { @Override public void run() { try { mPreview = new Preview(mContext); Point size = getDisplaySize(); int pt = (int) (5 * getScaledDensity()); WindowManager.LayoutParams l = new WindowManager.LayoutParams(pt, pt, WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, PixelFormat.TRANSLUCENT); l.x = -size.x / 2; l.y = -size.y / 2; mWinMgr.addView(mPreview, l); if (mCamera == null) { mCamera = Camera.open(mCameraId); if (mCamera == null) { throw new IOException("Failure to open the camera."); } } setCameraParameter(mCamera); mPreview.switchCamera(mCameraId, mCamera); mCamera.setPreviewCallback(CameraOverlay.this); mCamera.setErrorCallback(CameraOverlay.this); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); mContext.registerReceiver(mOrientReceiver, filter); callback.onSuccess(); } catch (Throwable t) { if (BuildConfig.DEBUG) { Log.w("Overlay", "", t); } callback.onFail(); } } }); } private void setCameraParameter(final Camera camera) { if (camera != null) { Camera.Parameters params = camera.getParameters(); params.setPictureSize(mPictureSize.getWidth(), mPictureSize.getHeight()); params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); params.setPreviewFrameRate((int) mMaxFps); try { camera.setParameters(params); } catch (Exception e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } } } } /** * Overlayを非表示にする. */ public void hide() { mHandler.post(new Runnable() { @Override public void run() { try { synchronized (mCameraLock) { mPreviewMode = false; if (mCamera != null) { mPreview.setCamera(0, null); mCamera.stopPreview(); mCamera.setPreviewCallback(null); if (!mFlashLightState) { mCamera.release(); mParams = null; mCamera = null; } } if (mPreview != null) { mWinMgr.removeView(mPreview); mPreview = null; } mContext.unregisterReceiver(mOrientReceiver); } } catch (Throwable t) { if (BuildConfig.DEBUG) { Log.w("Overlay", "", t); } } } }); } /** * 写真撮影を行う. * <p> * 写真撮影の結果はlistenerに通知される。 * </p> * @param listener 撮影結果を通知するリスナー */ public void takePicture(final OnTakePhotoListener listener) { if (isShow()) { takePictureInternal(listener); } else { show(new CameraOverlay.Callback() { @Override public void onSuccess() { takePictureInternal(listener); } @Override public void onFail() { listener.onFailedTakePhoto(); } }); } } /** * 撮影後の後始末を行います. */ private void cleanup() { synchronized (CameraOverlay.this) { if (!mPreviewMode) { hide(); } else if (mCamera != null) { mCamera.startPreview(); } } } /** * 写真撮影を行う内部メソッド. * * @param listener 撮影結果を通知するリスナー */ private void takePictureInternal(final OnTakePhotoListener listener) { synchronized (mCameraLock) { if (mPreview == null || mCamera == null) { if (listener != null) { listener.onFailedTakePhoto(); } return; } mPreview.takePicture(new Camera.PictureCallback() { @Override public void onPictureTaken(final byte[] data, final Camera camera) { if (data == null) { listener.onFailedTakePhoto(); cleanup(); return; } int degrees = 0; switch (mWinMgr.getDefaultDisplay().getRotation()) { case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; } mLogger.info("takePicture: display rotation = " + degrees); int rotation = mFacingDirection == 1 ? 0 : degrees + 180; Bitmap original = BitmapFactory.decodeByteArray(data, 0, data.length); Bitmap rotated; if (rotation == 0) { rotated = original; } else { Matrix m = new Matrix(); m.setRotate(rotation); rotated = Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), m, true); original.recycle(); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); rotated.compress(CompressFormat.JPEG, mJpegQuality, baos); byte[] jpeg = baos.toByteArray(); rotated.recycle(); // 常に違うファイル名になるためforceOverwriteはtrue mFileMgr.saveFile(createNewFileName(), jpeg, true, new FileManager.SaveFileCallback() { @Override public void onSuccess(@NonNull final String uri) { String filePath = mFileMgr.getBasePath().getAbsolutePath() + "/" + uri; if (listener != null) { listener.onTakenPhoto(uri, filePath); } cleanup(); } @Override public void onFail(@NonNull final Throwable throwable) { if (listener != null) { listener.onFailedTakePhoto(); } cleanup(); } }); } }); } } /** * Displayの密度を取得する. * * @return 密度 */ private float getScaledDensity() { DisplayMetrics metrics = new DisplayMetrics(); mWinMgr.getDefaultDisplay().getMetrics(metrics); return metrics.scaledDensity; } /** * Displayのサイズを取得する. * * @return サイズ */ private Point getDisplaySize() { Display disp = mWinMgr.getDefaultDisplay(); Point size = new Point(); disp.getSize(size); return size; } /** * Viewの座標を画面の左上に移動する. * * @param view 座標を移動するView */ private void updatePosition(final View view) { if (view == null) { return; } Point size = getDisplaySize(); final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) view.getLayoutParams(); lp.x = -size.x / 2; lp.y = -size.y / 2; view.post(new Runnable() { @Override public void run() { mWinMgr.updateViewLayout(view, lp); } }); } /** * 新規のファイル名を作成する. * * @return ファイル名 */ private String createNewFileName() { return FILENAME_PREFIX + mSimpleDateFormat.format(new Date()) + FILE_EXTENSION; } @Override public void onError(final int error, final Camera camera) { if (BuildConfig.DEBUG) { Log.w("Overlay", "onError: " + error); } hide(); } @Override public void onPreviewFrame(final byte[] data, final Camera camera) { synchronized (mCameraLock) { final long currentTime = System.currentTimeMillis(); if (mLastFrameTime != 0) { if ((currentTime - mLastFrameTime) < mFrameInterval) { mLastFrameTime = currentTime; return; } } if (mCamera != null && mCamera.equals(camera)) { mCamera.setPreviewCallback(null); if (mServer != null && mPreview != null) { int format = mPreview.getPreviewFormat(); int width = mPreview.getPreviewWidth(); int height = mPreview.getPreviewHeight(); YuvImage yuvimage = new YuvImage(data, format, width, height, null); Rect rect = new Rect(0, 0, width, height); ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (yuvimage.compressToJpeg(rect, mJpegQuality, baos)) { byte[] jdata = baos.toByteArray(); int degree = mPreview.getCameraDisplayOrientation(mContext); if (degree == 0) { mServer.offerMedia(jdata); } else { BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options(); bitmapFactoryOptions.inPreferredConfig = Bitmap.Config.RGB_565; Bitmap bmp = BitmapFactory.decodeByteArray(jdata, 0, jdata.length, bitmapFactoryOptions); if (bmp != null) { Matrix m = new Matrix(); m.setRotate(degree * mFacingDirection); Bitmap rotatedBmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, true); if (rotatedBmp != null) { baos.reset(); if (rotatedBmp.compress(CompressFormat.JPEG, mJpegQuality, baos)) { mServer.offerMedia(baos.toByteArray()); } rotatedBmp.recycle(); } bmp.recycle(); } } } } mCamera.setPreviewCallback(this); } mLastFrameTime = currentTime; } } /** * 写真撮影結果を通知するリスナー. */ public interface OnTakePhotoListener { /** * 写真撮影を行った画像へのURIを通知する. * * @param uri URI * @param filePath file path. */ void onTakenPhoto(String uri, String filePath); /** * 写真撮影に失敗したことを通知する. */ void onFailedTakePhoto(); } /** * Overlayの表示結果を通知するコールバック. */ public interface Callback { /** * 表示できたことを通知します. */ void onSuccess(); /** * 表示できなかったことを通知します. */ void onFail(); } /** * フラッシュライトの仕様状態を取得する. * @return 使用中はtrue、それ以外はfalse */ public synchronized boolean isUseFlashLight() { return mUseFlashLight; } /** * フラッシュライトの状態を取得する. * @return 点灯中はtrue、それ以外はfalse */ public synchronized boolean isFlashLightState() { return mFlashLightState; } /** * フラッシュライト点灯. */ public synchronized void turnOnFlashLight() { if (!isShow() && mCamera == null) { mCamera = Camera.open(); if (mCamera == null) { return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { SurfaceTexture preview = new SurfaceTexture(0); try { mCamera.setPreviewTexture(preview); } catch (IOException e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } } } mCamera.startPreview(); } if (mCamera != null && !isFlashLightState()) { Parameters p = mCamera.getParameters(); p.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); mCamera.setParameters(p); } mFlashLightState = true; mUseFlashLight = true; } /** * フラッシュライト消灯. */ public synchronized void turnOffFlashLight() { if (mCamera != null && isFlashLightState()) { Parameters p = mCamera.getParameters(); p.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); mCamera.setParameters(p); if (!isShow()) { mCamera.stopPreview(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { mCamera.setPreviewTexture(null); } catch (IOException e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } } } mCamera.release(); mParams = null; mCamera = null; } } mFlashLightState = false; mUseFlashLight = false; } }