| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ex.camera2.portability; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.graphics.ImageFormat; |
| import android.graphics.Matrix; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.SurfaceTexture; |
| import android.hardware.camera2.CameraAccessException; |
| import android.hardware.camera2.CameraCaptureSession; |
| import android.hardware.camera2.CameraCharacteristics; |
| import android.hardware.camera2.CameraDevice; |
| import android.hardware.camera2.CameraManager; |
| import android.hardware.camera2.CaptureFailure; |
| import android.hardware.camera2.CaptureRequest; |
| import android.hardware.camera2.CaptureResult; |
| import android.hardware.camera2.TotalCaptureResult; |
| import android.hardware.camera2.params.MeteringRectangle; |
| import android.media.Image; |
| import android.media.ImageReader; |
| import android.media.MediaActionSound; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.view.Surface; |
| |
| import com.android.ex.camera2.portability.debug.Log; |
| import com.android.ex.camera2.utils.Camera2RequestSettingsSet; |
| |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * A class to implement {@link CameraAgent} of the Android camera2 framework. |
| */ |
| class AndroidCamera2AgentImpl extends CameraAgent { |
| private static final Log.Tag TAG = new Log.Tag("AndCam2AgntImp"); |
| |
| private final Camera2Handler mCameraHandler; |
| private final HandlerThread mCameraHandlerThread; |
| private final CameraStateHolder mCameraState; |
| private final DispatchThread mDispatchThread; |
| private final CameraManager mCameraManager; |
| private final MediaActionSound mNoisemaker; |
| private CameraExceptionHandler mExceptionHandler; |
| |
| /** |
| * Number of camera devices. The length of {@code mCameraDevices} does not reveal this |
| * information because that list may contain since-invalidated indices. |
| */ |
| private int mNumCameraDevices; |
| |
| /** |
| * Transformation between integral camera indices and the {@link java.lang.String} indices used |
| * by the underlying API. Note that devices may disappear because they've been disconnected or |
| * have otherwise gone offline. Because we need to keep the meanings of whatever indices we |
| * expose stable, we cannot simply remove them in such a case; instead, we insert {@code null}s |
| * to invalidate any such indices. Whenever new devices appear, they are appended to the end of |
| * the list, and thereby assigned the lowest index that has never yet been used. |
| */ |
| private final List<String> mCameraDevices; |
| |
| AndroidCamera2AgentImpl(Context context) { |
| mCameraHandlerThread = new HandlerThread("Camera2 Handler Thread"); |
| mCameraHandlerThread.start(); |
| mCameraHandler = new Camera2Handler(mCameraHandlerThread.getLooper()); |
| mExceptionHandler = new CameraExceptionHandler(mCameraHandler); |
| mCameraState = new AndroidCamera2StateHolder(); |
| mDispatchThread = new DispatchThread(mCameraHandler, mCameraHandlerThread); |
| mDispatchThread.start(); |
| mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); |
| mNoisemaker = new MediaActionSound(); |
| mNoisemaker.load(MediaActionSound.SHUTTER_CLICK); |
| |
| mNumCameraDevices = 0; |
| mCameraDevices = new ArrayList<String>(); |
| updateCameraDevices(); |
| } |
| |
| /** |
| * Updates the camera device index assignments stored in {@link mCameraDevices}, without |
| * reappropriating any currently-assigned index. |
| * @return Whether the operation was successful |
| */ |
| private boolean updateCameraDevices() { |
| try { |
| String[] currentCameraDevices = mCameraManager.getCameraIdList(); |
| Set<String> currentSet = new HashSet<String>(Arrays.asList(currentCameraDevices)); |
| |
| // Invalidate the indices assigned to any camera devices that are no longer present |
| for (int index = 0; index < mCameraDevices.size(); ++index) { |
| if (!currentSet.contains(mCameraDevices.get(index))) { |
| mCameraDevices.set(index, null); |
| --mNumCameraDevices; |
| } |
| } |
| |
| // Assign fresh indices to any new camera devices |
| currentSet.removeAll(mCameraDevices); // The devices we didn't know about |
| for (String device : currentCameraDevices) { |
| if (currentSet.contains(device)) { |
| mCameraDevices.add(device); |
| ++mNumCameraDevices; |
| } |
| } |
| |
| return true; |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Could not get device listing from camera subsystem", ex); |
| return false; |
| } |
| } |
| |
| // TODO: Implement |
| @Override |
| public void recycle() {} |
| |
| // TODO: Some indices may now be invalid; ensure everyone can handle that and update the docs |
| @Override |
| public CameraDeviceInfo getCameraDeviceInfo() { |
| updateCameraDevices(); |
| return new AndroidCamera2DeviceInfo(mCameraManager, mCameraDevices.toArray(new String[0]), |
| mNumCameraDevices); |
| } |
| |
| @Override |
| protected Handler getCameraHandler() { |
| return mCameraHandler; |
| } |
| |
| @Override |
| protected DispatchThread getDispatchThread() { |
| return mDispatchThread; |
| } |
| |
| @Override |
| protected CameraStateHolder getCameraState() { |
| return mCameraState; |
| } |
| |
| @Override |
| protected CameraExceptionHandler getCameraExceptionHandler() { |
| return mExceptionHandler; |
| } |
| |
| @Override |
| public void setCameraExceptionHandler(CameraExceptionHandler exceptionHandler) { |
| mExceptionHandler = exceptionHandler; |
| } |
| |
| private static abstract class CaptureAvailableListener |
| extends CameraCaptureSession.CaptureCallback |
| implements ImageReader.OnImageAvailableListener {}; |
| |
| private class Camera2Handler extends HistoryHandler { |
| // Caller-provided when leaving CAMERA_UNOPENED state: |
| private CameraOpenCallback mOpenCallback; |
| private int mCameraIndex; |
| private String mCameraId; |
| private int mCancelAfPending = 0; |
| |
| // Available in CAMERA_UNCONFIGURED state and above: |
| private CameraDevice mCamera; |
| private AndroidCamera2ProxyImpl mCameraProxy; |
| private Camera2RequestSettingsSet mPersistentSettings; |
| private Rect mActiveArray; |
| private boolean mLegacyDevice; |
| |
| // Available in CAMERA_CONFIGURED state and above: |
| private Size mPreviewSize; |
| private Size mPhotoSize; |
| |
| // Available in PREVIEW_READY state and above: |
| private SurfaceTexture mPreviewTexture; |
| private Surface mPreviewSurface; |
| private CameraCaptureSession mSession; |
| private ImageReader mCaptureReader; |
| |
| // Available from the beginning of PREVIEW_ACTIVE until the first preview frame arrives: |
| private CameraStartPreviewCallback mOneshotPreviewingCallback; |
| |
| // Available in FOCUS_LOCKED between AF trigger receipt and whenever the lens stops moving: |
| private CameraAFCallback mOneshotAfCallback; |
| |
| // Available when taking picture between AE trigger receipt and autoexposure convergence |
| private CaptureAvailableListener mOneshotCaptureCallback; |
| |
| // Available whenever setAutoFocusMoveCallback() was last invoked with a non-null argument: |
| private CameraAFMoveCallback mPassiveAfCallback; |
| |
| // Gets reset on every state change |
| private int mCurrentAeState = CaptureResult.CONTROL_AE_STATE_INACTIVE; |
| |
| Camera2Handler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(final Message msg) { |
| super.handleMessage(msg); |
| Log.v(TAG, "handleMessage - action = '" + CameraActions.stringify(msg.what) + "'"); |
| int cameraAction = msg.what; |
| try { |
| switch (cameraAction) { |
| case CameraActions.OPEN_CAMERA: |
| case CameraActions.RECONNECT: { |
| CameraOpenCallback openCallback = (CameraOpenCallback) msg.obj; |
| int cameraIndex = msg.arg1; |
| |
| if (mCameraState.getState() > AndroidCamera2StateHolder.CAMERA_UNOPENED) { |
| openCallback.onDeviceOpenedAlready(cameraIndex, |
| generateHistoryString(cameraIndex)); |
| break; |
| } |
| |
| mOpenCallback = openCallback; |
| mCameraIndex = cameraIndex; |
| mCameraId = mCameraDevices.get(mCameraIndex); |
| Log.i(TAG, String.format("Opening camera index %d (id %s) with camera2 API", |
| cameraIndex, mCameraId)); |
| |
| if (mCameraId == null) { |
| mOpenCallback.onCameraDisabled(msg.arg1); |
| break; |
| } |
| mCameraManager.openCamera(mCameraId, mCameraDeviceStateCallback, this); |
| |
| break; |
| } |
| |
| case CameraActions.RELEASE: { |
| if (mCameraState.getState() == AndroidCamera2StateHolder.CAMERA_UNOPENED) { |
| Log.w(TAG, "Ignoring release at inappropriate time"); |
| break; |
| } |
| |
| if (mSession != null) { |
| closePreviewSession(); |
| mSession = null; |
| } |
| if (mCamera != null) { |
| mCamera.close(); |
| mCamera = null; |
| } |
| mCameraProxy = null; |
| mPersistentSettings = null; |
| mActiveArray = null; |
| if (mPreviewSurface != null) { |
| mPreviewSurface.release(); |
| mPreviewSurface = null; |
| } |
| mPreviewTexture = null; |
| if (mCaptureReader != null) { |
| mCaptureReader.close(); |
| mCaptureReader = null; |
| } |
| mPreviewSize = null; |
| mPhotoSize = null; |
| mCameraIndex = 0; |
| mCameraId = null; |
| changeState(AndroidCamera2StateHolder.CAMERA_UNOPENED); |
| break; |
| } |
| |
| /*case CameraActions.UNLOCK: { |
| break; |
| } |
| |
| case CameraActions.LOCK: { |
| break; |
| }*/ |
| |
| case CameraActions.SET_PREVIEW_TEXTURE_ASYNC: { |
| setPreviewTexture((SurfaceTexture) msg.obj); |
| break; |
| } |
| |
| case CameraActions.START_PREVIEW_ASYNC: { |
| if (mCameraState.getState() != |
| AndroidCamera2StateHolder.CAMERA_PREVIEW_READY) { |
| // TODO: Provide better feedback here? |
| Log.w(TAG, "Refusing to start preview at inappropriate time"); |
| break; |
| } |
| |
| mOneshotPreviewingCallback = (CameraStartPreviewCallback) msg.obj; |
| changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE); |
| try { |
| mSession.setRepeatingRequest( |
| mPersistentSettings.createRequest(mCamera, |
| CameraDevice.TEMPLATE_PREVIEW, mPreviewSurface), |
| /*listener*/mCameraResultStateCallback, /*handler*/this); |
| } catch(CameraAccessException ex) { |
| Log.w(TAG, "Unable to start preview", ex); |
| changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY); |
| } |
| break; |
| } |
| |
| // FIXME: We need to tear down the CameraCaptureSession here |
| // (and unlock the CameraSettings object from our |
| // CameraProxy) so that the preview/photo sizes can be |
| // changed again while no preview is running. |
| case CameraActions.STOP_PREVIEW: { |
| if (mCameraState.getState() < |
| AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) { |
| Log.w(TAG, "Refusing to stop preview at inappropriate time"); |
| break; |
| } |
| |
| mSession.stopRepeating(); |
| changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY); |
| break; |
| } |
| |
| /*case CameraActions.SET_PREVIEW_CALLBACK_WITH_BUFFER: { |
| break; |
| } |
| |
| case CameraActions.ADD_CALLBACK_BUFFER: { |
| break; |
| } |
| |
| case CameraActions.SET_PREVIEW_DISPLAY_ASYNC: { |
| break; |
| } |
| |
| case CameraActions.SET_PREVIEW_CALLBACK: { |
| break; |
| } |
| |
| case CameraActions.SET_ONE_SHOT_PREVIEW_CALLBACK: { |
| break; |
| } |
| |
| case CameraActions.SET_PARAMETERS: { |
| break; |
| } |
| |
| case CameraActions.GET_PARAMETERS: { |
| break; |
| } |
| |
| case CameraActions.REFRESH_PARAMETERS: { |
| break; |
| }*/ |
| |
| case CameraActions.APPLY_SETTINGS: { |
| AndroidCamera2Settings settings = (AndroidCamera2Settings) msg.obj; |
| applyToRequest(settings); |
| break; |
| } |
| |
| case CameraActions.AUTO_FOCUS: { |
| if (mCancelAfPending > 0) { |
| Log.v(TAG, "handleMessage - Ignored AUTO_FOCUS because there was " |
| + mCancelAfPending + " pending CANCEL_AUTO_FOCUS messages"); |
| break; // ignore AF because a CANCEL_AF is queued after this |
| } |
| // We only support locking the focus while a preview is being displayed. |
| // However, it can be requested multiple times in succession; the effect of |
| // the subsequent invocations is determined by the focus mode defined in the |
| // provided CameraSettings object. In passive (CONTINUOUS_*) mode, the |
| // duplicate requests are no-ops and leave the lens locked at its current |
| // position, but in active (AUTO) mode, they perform another scan and lock |
| // once that is finished. In any manual focus mode, this call is a no-op, |
| // and most notably, this is the only case where the callback isn't invoked. |
| if (mCameraState.getState() < |
| AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) { |
| Log.w(TAG, "Ignoring attempt to autofocus without preview"); |
| break; |
| } |
| |
| // The earliest we can reliably tell whether the autofocus has locked in |
| // response to our latest request is when our one-time capture progresses. |
| // However, it will probably take longer than that, so once that happens, |
| // just start checking the repeating preview requests as they complete. |
| final CameraAFCallback callback = (CameraAFCallback) msg.obj; |
| CameraCaptureSession.CaptureCallback deferredCallbackSetter = |
| new CameraCaptureSession.CaptureCallback() { |
| private boolean mAlreadyDispatched = false; |
| |
| @Override |
| public void onCaptureProgressed(CameraCaptureSession session, |
| CaptureRequest request, |
| CaptureResult result) { |
| checkAfState(result); |
| } |
| |
| @Override |
| public void onCaptureCompleted(CameraCaptureSession session, |
| CaptureRequest request, |
| TotalCaptureResult result) { |
| checkAfState(result); |
| } |
| |
| private void checkAfState(CaptureResult result) { |
| if (result.get(CaptureResult.CONTROL_AF_STATE) != null && |
| !mAlreadyDispatched) { |
| // Now our mCameraResultStateCallback will invoke the callback |
| // the first time it finds the focus motor to be locked. |
| mAlreadyDispatched = true; |
| mOneshotAfCallback = callback; |
| // This is an optimization: check the AF state of this frame |
| // instead of simply waiting for the next. |
| mCameraResultStateCallback.monitorControlStates(result); |
| } |
| } |
| |
| @Override |
| public void onCaptureFailed(CameraCaptureSession session, |
| CaptureRequest request, |
| CaptureFailure failure) { |
| Log.e(TAG, "Focusing failed with reason " + failure.getReason()); |
| callback.onAutoFocus(false, mCameraProxy); |
| }}; |
| |
| // Send a one-time capture to trigger the camera driver to lock focus. |
| changeState(AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED); |
| Camera2RequestSettingsSet trigger = |
| new Camera2RequestSettingsSet(mPersistentSettings); |
| trigger.set(CaptureRequest.CONTROL_AF_TRIGGER, |
| CaptureRequest.CONTROL_AF_TRIGGER_START); |
| try { |
| mSession.capture( |
| trigger.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW, |
| mPreviewSurface), |
| /*listener*/deferredCallbackSetter, /*handler*/ this); |
| } catch(CameraAccessException ex) { |
| Log.e(TAG, "Unable to lock autofocus", ex); |
| changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE); |
| } |
| break; |
| } |
| |
| case CameraActions.CANCEL_AUTO_FOCUS: { |
| // Ignore all AFs that were already queued until we see |
| // a CANCEL_AUTO_FOCUS_FINISH |
| mCancelAfPending++; |
| // Why would you want to unlock the lens if it isn't already locked? |
| if (mCameraState.getState() < |
| AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) { |
| Log.w(TAG, "Ignoring attempt to release focus lock without preview"); |
| break; |
| } |
| |
| // Send a one-time capture to trigger the camera driver to resume scanning. |
| changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE); |
| Camera2RequestSettingsSet cancel = |
| new Camera2RequestSettingsSet(mPersistentSettings); |
| cancel.set(CaptureRequest.CONTROL_AF_TRIGGER, |
| CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); |
| try { |
| mSession.capture( |
| cancel.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW, |
| mPreviewSurface), |
| /*listener*/null, /*handler*/this); |
| } catch(CameraAccessException ex) { |
| Log.e(TAG, "Unable to cancel autofocus", ex); |
| changeState(AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED); |
| } |
| break; |
| } |
| |
| case CameraActions.CANCEL_AUTO_FOCUS_FINISH: { |
| // Stop ignoring AUTO_FOCUS messages unless there are additional |
| // CANCEL_AUTO_FOCUSes that were added |
| mCancelAfPending--; |
| break; |
| } |
| |
| case CameraActions.SET_AUTO_FOCUS_MOVE_CALLBACK: { |
| mPassiveAfCallback = (CameraAFMoveCallback) msg.obj; |
| break; |
| } |
| |
| /*case CameraActions.SET_ZOOM_CHANGE_LISTENER: { |
| break; |
| } |
| |
| case CameraActions.SET_FACE_DETECTION_LISTENER: { |
| break; |
| } |
| |
| case CameraActions.START_FACE_DETECTION: { |
| break; |
| } |
| |
| case CameraActions.STOP_FACE_DETECTION: { |
| break; |
| } |
| |
| case CameraActions.SET_ERROR_CALLBACK: { |
| break; |
| } |
| |
| case CameraActions.ENABLE_SHUTTER_SOUND: { |
| break; |
| }*/ |
| |
| case CameraActions.SET_DISPLAY_ORIENTATION: { |
| // Only set the JPEG capture orientation if requested to do so; otherwise, |
| // capture in the sensor's physical orientation. (e.g., JPEG rotation is |
| // necessary in auto-rotate mode. |
| mPersistentSettings.set(CaptureRequest.JPEG_ORIENTATION, msg.arg2 > 0 ? |
| mCameraProxy.getCharacteristics().getJpegOrientation(msg.arg1) : 0); |
| break; |
| } |
| |
| case CameraActions.SET_JPEG_ORIENTATION: { |
| mPersistentSettings.set(CaptureRequest.JPEG_ORIENTATION, msg.arg1); |
| break; |
| } |
| |
| case CameraActions.CAPTURE_PHOTO: { |
| if (mCameraState.getState() < |
| AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) { |
| Log.e(TAG, "Photos may only be taken when a preview is active"); |
| break; |
| } |
| if (mCameraState.getState() != |
| AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED) { |
| Log.w(TAG, "Taking a (likely blurry) photo without the lens locked"); |
| } |
| |
| final CaptureAvailableListener listener = |
| (CaptureAvailableListener) msg.obj; |
| if (mLegacyDevice || |
| (mCurrentAeState == CaptureResult.CONTROL_AE_STATE_CONVERGED && |
| !mPersistentSettings.matches(CaptureRequest.CONTROL_AE_MODE, |
| CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH) && |
| !mPersistentSettings.matches(CaptureRequest.FLASH_MODE, |
| CaptureRequest.FLASH_MODE_SINGLE))) |
| { |
| // Legacy devices don't support the precapture state keys and instead |
| // perform autoexposure convergence automatically upon capture. |
| |
| // On other devices, as long as it has already converged, it determined |
| // that flash was not required, and we're not going to invalidate the |
| // current exposure levels by forcing the force on, we can save |
| // significant capture time by not forcing a recalculation. |
| Log.i(TAG, "Skipping pre-capture autoexposure convergence"); |
| mCaptureReader.setOnImageAvailableListener(listener, /*handler*/this); |
| try { |
| mSession.capture( |
| mPersistentSettings.createRequest(mCamera, |
| CameraDevice.TEMPLATE_STILL_CAPTURE, |
| mCaptureReader.getSurface()), |
| listener, /*handler*/this); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Unable to initiate immediate capture", ex); |
| } |
| } else { |
| // We need to let AE converge before capturing. Once our one-time |
| // trigger capture has made it into the pipeline, we'll start checking |
| // for the completion of that convergence, capturing when that happens. |
| Log.i(TAG, "Forcing pre-capture autoexposure convergence"); |
| CameraCaptureSession.CaptureCallback deferredCallbackSetter = |
| new CameraCaptureSession.CaptureCallback() { |
| private boolean mAlreadyDispatched = false; |
| |
| @Override |
| public void onCaptureProgressed(CameraCaptureSession session, |
| CaptureRequest request, |
| CaptureResult result) { |
| checkAeState(result); |
| } |
| |
| @Override |
| public void onCaptureCompleted(CameraCaptureSession session, |
| CaptureRequest request, |
| TotalCaptureResult result) { |
| checkAeState(result); |
| } |
| |
| private void checkAeState(CaptureResult result) { |
| if (result.get(CaptureResult.CONTROL_AE_STATE) != null && |
| !mAlreadyDispatched) { |
| // Now our mCameraResultStateCallback will invoke the |
| // callback once the autoexposure routine has converged. |
| mAlreadyDispatched = true; |
| mOneshotCaptureCallback = listener; |
| // This is an optimization: check the AE state of this frame |
| // instead of simply waiting for the next. |
| mCameraResultStateCallback.monitorControlStates(result); |
| } |
| } |
| |
| @Override |
| public void onCaptureFailed(CameraCaptureSession session, |
| CaptureRequest request, |
| CaptureFailure failure) { |
| Log.e(TAG, "Autoexposure and capture failed with reason " + |
| failure.getReason()); |
| // TODO: Make an error callback? |
| }}; |
| |
| // Set a one-time capture to trigger the camera driver's autoexposure: |
| Camera2RequestSettingsSet expose = |
| new Camera2RequestSettingsSet(mPersistentSettings); |
| expose.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, |
| CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); |
| try { |
| mSession.capture( |
| expose.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW, |
| mPreviewSurface), |
| /*listener*/deferredCallbackSetter, /*handler*/this); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Unable to run autoexposure and perform capture", ex); |
| } |
| } |
| break; |
| } |
| |
| default: { |
| // TODO: Rephrase once everything has been implemented |
| throw new RuntimeException("Unimplemented CameraProxy message=" + msg.what); |
| } |
| } |
| } catch (final Exception ex) { |
| if (cameraAction != CameraActions.RELEASE && mCamera != null) { |
| // TODO: Handle this better |
| mCamera.close(); |
| mCamera = null; |
| } else if (mCamera == null) { |
| if (cameraAction == CameraActions.OPEN_CAMERA) { |
| if (mOpenCallback != null) { |
| mOpenCallback.onDeviceOpenFailure(mCameraIndex, |
| generateHistoryString(mCameraIndex)); |
| } |
| } else { |
| Log.w(TAG, "Cannot handle message " + msg.what + ", mCamera is null"); |
| } |
| return; |
| } |
| |
| if (ex instanceof RuntimeException) { |
| String commandHistory = generateHistoryString(Integer.parseInt(mCameraId)); |
| mExceptionHandler.onCameraException((RuntimeException) ex, commandHistory, |
| cameraAction, mCameraState.getState()); |
| } |
| } finally { |
| WaitDoneBundle.unblockSyncWaiters(msg); |
| } |
| } |
| |
| public CameraSettings buildSettings(AndroidCamera2Capabilities caps) { |
| try { |
| return new AndroidCamera2Settings(mCamera, CameraDevice.TEMPLATE_PREVIEW, |
| mActiveArray, mPreviewSize, mPhotoSize); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Unable to query camera device to build settings representation"); |
| return null; |
| } |
| } |
| |
| /** |
| * Simply propagates settings from provided {@link CameraSettings} |
| * object to our {@link CaptureRequest.Builder} for use in captures. |
| * <p>Most conversions to match the API 2 formats are performed by |
| * {@link AndroidCamera2Capabilities.IntegralStringifier}; otherwise |
| * any final adjustments are done here before updating the builder.</p> |
| * |
| * @param settings The new/updated settings |
| */ |
| private void applyToRequest(AndroidCamera2Settings settings) { |
| // TODO: If invoked when in PREVIEW_READY state, a new preview size will not take effect |
| |
| mPersistentSettings.union(settings.getRequestSettings()); |
| mPreviewSize = settings.getCurrentPreviewSize(); |
| mPhotoSize = settings.getCurrentPhotoSize(); |
| |
| if (mCameraState.getState() >= AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) { |
| // If we're already previewing, reflect most settings immediately |
| try { |
| mSession.setRepeatingRequest( |
| mPersistentSettings.createRequest(mCamera, |
| CameraDevice.TEMPLATE_PREVIEW, mPreviewSurface), |
| /*listener*/mCameraResultStateCallback, /*handler*/this); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Failed to apply updated request settings", ex); |
| } |
| } else if (mCameraState.getState() < AndroidCamera2StateHolder.CAMERA_PREVIEW_READY) { |
| // If we're already ready to preview, this doesn't regress our state |
| changeState(AndroidCamera2StateHolder.CAMERA_CONFIGURED); |
| } |
| } |
| |
| private void setPreviewTexture(SurfaceTexture surfaceTexture) { |
| // TODO: Must be called after providing a .*Settings populated with sizes |
| // TODO: We don't technically offer a selection of sizes tailored to SurfaceTextures! |
| |
| // TODO: Handle this error condition with a callback or exception |
| if (mCameraState.getState() < AndroidCamera2StateHolder.CAMERA_CONFIGURED) { |
| Log.w(TAG, "Ignoring texture setting at inappropriate time"); |
| return; |
| } |
| |
| // Avoid initializing another capture session unless we absolutely have to |
| if (surfaceTexture == mPreviewTexture) { |
| Log.i(TAG, "Optimizing out redundant preview texture setting"); |
| return; |
| } |
| |
| if (mSession != null) { |
| closePreviewSession(); |
| } |
| |
| mPreviewTexture = surfaceTexture; |
| surfaceTexture.setDefaultBufferSize(mPreviewSize.width(), mPreviewSize.height()); |
| |
| if (mPreviewSurface != null) { |
| mPreviewSurface.release(); |
| } |
| mPreviewSurface = new Surface(surfaceTexture); |
| |
| if (mCaptureReader != null) { |
| mCaptureReader.close(); |
| } |
| mCaptureReader = ImageReader.newInstance( |
| mPhotoSize.width(), mPhotoSize.height(), ImageFormat.JPEG, 1); |
| |
| try { |
| mCamera.createCaptureSession( |
| Arrays.asList(mPreviewSurface, mCaptureReader.getSurface()), |
| mCameraPreviewStateCallback, this); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Failed to create camera capture session", ex); |
| } |
| } |
| |
| private void closePreviewSession() { |
| try { |
| mSession.abortCaptures(); |
| mSession = null; |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Failed to close existing camera capture session", ex); |
| } |
| changeState(AndroidCamera2StateHolder.CAMERA_CONFIGURED); |
| } |
| |
| private void changeState(int newState) { |
| if (mCameraState.getState() != newState) { |
| mCameraState.setState(newState); |
| if (newState < AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) { |
| mCurrentAeState = CaptureResult.CONTROL_AE_STATE_INACTIVE; |
| mCameraResultStateCallback.resetState(); |
| } |
| } |
| } |
| |
| // This callback monitors our connection to and disconnection from camera devices. |
| private CameraDevice.StateCallback mCameraDeviceStateCallback = |
| new CameraDevice.StateCallback() { |
| @Override |
| public void onOpened(CameraDevice camera) { |
| mCamera = camera; |
| if (mOpenCallback != null) { |
| try { |
| CameraCharacteristics props = |
| mCameraManager.getCameraCharacteristics(mCameraId); |
| CameraDeviceInfo.Characteristics characteristics = |
| getCameraDeviceInfo().getCharacteristics(mCameraIndex); |
| mCameraProxy = new AndroidCamera2ProxyImpl(AndroidCamera2AgentImpl.this, |
| mCameraIndex, mCamera, characteristics, props); |
| mPersistentSettings = new Camera2RequestSettingsSet(); |
| mActiveArray = |
| props.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); |
| mLegacyDevice = |
| props.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) == |
| CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; |
| changeState(AndroidCamera2StateHolder.CAMERA_UNCONFIGURED); |
| mOpenCallback.onCameraOpened(mCameraProxy); |
| } catch (CameraAccessException ex) { |
| mOpenCallback.onDeviceOpenFailure(mCameraIndex, |
| generateHistoryString(mCameraIndex)); |
| } |
| } |
| } |
| |
| @Override |
| public void onDisconnected(CameraDevice camera) { |
| Log.w(TAG, "Camera device '" + mCameraIndex + "' was disconnected"); |
| } |
| |
| @Override |
| public void onError(CameraDevice camera, int error) { |
| Log.e(TAG, "Camera device '" + mCameraIndex + "' encountered error code '" + |
| error + '\''); |
| if (mOpenCallback != null) { |
| mOpenCallback.onDeviceOpenFailure(mCameraIndex, |
| generateHistoryString(mCameraIndex)); |
| } |
| }}; |
| |
| // This callback monitors our camera session (i.e. our transition into and out of preview). |
| private CameraCaptureSession.StateCallback mCameraPreviewStateCallback = |
| new CameraCaptureSession.StateCallback() { |
| @Override |
| public void onConfigured(CameraCaptureSession session) { |
| mSession = session; |
| changeState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY); |
| } |
| |
| @Override |
| public void onConfigureFailed(CameraCaptureSession session) { |
| // TODO: Invoke a callback |
| Log.e(TAG, "Failed to configure the camera for capture"); |
| } |
| |
| @Override |
| public void onActive(CameraCaptureSession session) { |
| if (mOneshotPreviewingCallback != null) { |
| // The session is up and processing preview requests. Inform the caller. |
| mOneshotPreviewingCallback.onPreviewStarted(); |
| mOneshotPreviewingCallback = null; |
| } |
| }}; |
| |
| private abstract class CameraResultStateCallback |
| extends CameraCaptureSession.CaptureCallback { |
| public abstract void monitorControlStates(CaptureResult result); |
| |
| public abstract void resetState(); |
| } |
| |
| // This callback monitors requested captures and notifies any relevant callbacks. |
| private CameraResultStateCallback mCameraResultStateCallback = |
| new CameraResultStateCallback() { |
| private int mLastAfState = -1; |
| private long mLastAfFrameNumber = -1; |
| private long mLastAeFrameNumber = -1; |
| |
| @Override |
| public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, |
| CaptureResult result) { |
| monitorControlStates(result); |
| } |
| |
| @Override |
| public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, |
| TotalCaptureResult result) { |
| monitorControlStates(result); |
| } |
| |
| @Override |
| public void monitorControlStates(CaptureResult result) { |
| Integer afStateMaybe = result.get(CaptureResult.CONTROL_AF_STATE); |
| if (afStateMaybe != null) { |
| int afState = afStateMaybe; |
| // Since we handle both partial and total results for multiple frames here, we |
| // might get the final callbacks for an earlier frame after receiving one or |
| // more that correspond to the next one. To prevent our data from oscillating, |
| // we never consider AF states that are older than the last one we've seen. |
| if (result.getFrameNumber() > mLastAfFrameNumber) { |
| boolean afStateChanged = afState != mLastAfState; |
| mLastAfState = afState; |
| mLastAfFrameNumber = result.getFrameNumber(); |
| |
| switch (afState) { |
| case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN: |
| case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED: |
| case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED: { |
| if (afStateChanged && mPassiveAfCallback != null) { |
| // A CameraAFMoveCallback is attached. If we just started to |
| // scan, the motor is moving; otherwise, it has settled. |
| mPassiveAfCallback.onAutoFocusMoving( |
| afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN, |
| mCameraProxy); |
| } |
| break; |
| } |
| |
| case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED: |
| case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: { |
| // This check must be made regardless of whether the focus state has |
| // changed recently to avoid infinite waiting during autoFocus() |
| // when the algorithm has already either converged or failed to. |
| if (mOneshotAfCallback != null) { |
| // A call to autoFocus() was just made to request a focus lock. |
| // Notify the caller that the lens is now indefinitely fixed, |
| // and report whether the image we're stuck with is in focus. |
| mOneshotAfCallback.onAutoFocus( |
| afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED, |
| mCameraProxy); |
| mOneshotAfCallback = null; |
| } |
| break; |
| } |
| } |
| } |
| } |
| |
| Integer aeStateMaybe = result.get(CaptureResult.CONTROL_AE_STATE); |
| if (aeStateMaybe != null) { |
| int aeState = aeStateMaybe; |
| // Since we handle both partial and total results for multiple frames here, we |
| // might get the final callbacks for an earlier frame after receiving one or |
| // more that correspond to the next one. To prevent our data from oscillating, |
| // we never consider AE states that are older than the last one we've seen. |
| if (result.getFrameNumber() > mLastAeFrameNumber) { |
| mCurrentAeState = aeStateMaybe; |
| mLastAeFrameNumber = result.getFrameNumber(); |
| |
| switch (aeState) { |
| case CaptureResult.CONTROL_AE_STATE_CONVERGED: |
| case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED: |
| case CaptureResult.CONTROL_AE_STATE_LOCKED: { |
| // This check must be made regardless of whether the exposure state |
| // has changed recently to avoid infinite waiting during |
| // takePicture() when the algorithm has already converged. |
| if (mOneshotCaptureCallback != null) { |
| // A call to takePicture() was just made, and autoexposure |
| // converged so it's time to initiate the capture! |
| mCaptureReader.setOnImageAvailableListener( |
| /*listener*/mOneshotCaptureCallback, |
| /*handler*/Camera2Handler.this); |
| try { |
| mSession.capture( |
| mPersistentSettings.createRequest(mCamera, |
| CameraDevice.TEMPLATE_STILL_CAPTURE, |
| mCaptureReader.getSurface()), |
| /*callback*/mOneshotCaptureCallback, |
| /*handler*/Camera2Handler.this); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Unable to initiate capture", ex); |
| } finally { |
| mOneshotCaptureCallback = null; |
| } |
| } |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void resetState() { |
| mLastAfState = -1; |
| mLastAfFrameNumber = -1; |
| mLastAeFrameNumber = -1; |
| } |
| |
| @Override |
| public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, |
| CaptureFailure failure) { |
| Log.e(TAG, "Capture attempt failed with reason " + failure.getReason()); |
| }}; |
| } |
| |
| private class AndroidCamera2ProxyImpl extends CameraAgent.CameraProxy { |
| private final AndroidCamera2AgentImpl mCameraAgent; |
| private final int mCameraIndex; |
| private final CameraDevice mCamera; |
| private final CameraDeviceInfo.Characteristics mCharacteristics; |
| private final AndroidCamera2Capabilities mCapabilities; |
| private CameraSettings mLastSettings; |
| private boolean mShutterSoundEnabled; |
| |
| public AndroidCamera2ProxyImpl( |
| AndroidCamera2AgentImpl agent, |
| int cameraIndex, |
| CameraDevice camera, |
| CameraDeviceInfo.Characteristics characteristics, |
| CameraCharacteristics properties) { |
| mCameraAgent = agent; |
| mCameraIndex = cameraIndex; |
| mCamera = camera; |
| mCharacteristics = characteristics; |
| mCapabilities = new AndroidCamera2Capabilities(properties); |
| mLastSettings = null; |
| mShutterSoundEnabled = true; |
| } |
| |
| // TODO: Implement |
| @Override |
| public android.hardware.Camera getCamera() { return null; } |
| |
| @Override |
| public int getCameraId() { |
| return mCameraIndex; |
| } |
| |
| @Override |
| public CameraDeviceInfo.Characteristics getCharacteristics() { |
| return mCharacteristics; |
| } |
| |
| @Override |
| public CameraCapabilities getCapabilities() { |
| return mCapabilities; |
| } |
| |
| public CameraAgent getAgent() { |
| return mCameraAgent; |
| } |
| |
| private AndroidCamera2Capabilities getSpecializedCapabilities() { |
| return mCapabilities; |
| } |
| |
| // FIXME: Unlock the sizes in stopPreview(), as per the corresponding |
| // explanation on the STOP_PREVIEW case in the handler. |
| @Override |
| public void setPreviewTexture(SurfaceTexture surfaceTexture) { |
| // Once the Surface has been selected, we configure the session and |
| // are no longer able to change the sizes. |
| getSettings().setSizesLocked(true); |
| super.setPreviewTexture(surfaceTexture); |
| } |
| |
| // FIXME: Unlock the sizes in stopPreview(), as per the corresponding |
| // explanation on the STOP_PREVIEW case in the handler. |
| @Override |
| public void setPreviewTextureSync(SurfaceTexture surfaceTexture) { |
| // Once the Surface has been selected, we configure the session and |
| // are no longer able to change the sizes. |
| getSettings().setSizesLocked(true); |
| super.setPreviewTexture(surfaceTexture); |
| } |
| |
| // TODO: Implement |
| @Override |
| public void setPreviewDataCallback(Handler handler, CameraPreviewDataCallback cb) {} |
| |
| // TODO: Implement |
| @Override |
| public void setOneShotPreviewCallback(Handler handler, CameraPreviewDataCallback cb) {} |
| |
| // TODO: Implement |
| @Override |
| public void setPreviewDataCallbackWithBuffer(Handler handler, CameraPreviewDataCallback cb) |
| {} |
| |
| // TODO: Implement |
| public void addCallbackBuffer(final byte[] callbackBuffer) {} |
| |
| @Override |
| public void autoFocus(final Handler handler, final CameraAFCallback cb) { |
| try { |
| mDispatchThread.runJob(new Runnable() { |
| @Override |
| public void run() { |
| CameraAFCallback cbForward = null; |
| if (cb != null) { |
| cbForward = new CameraAFCallback() { |
| @Override |
| public void onAutoFocus(final boolean focused, |
| final CameraProxy camera) { |
| handler.post(new Runnable() { |
| @Override |
| public void run() { |
| cb.onAutoFocus(focused, camera); |
| } |
| }); |
| } |
| }; |
| } |
| |
| mCameraState.waitForStates(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE | |
| AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED); |
| mCameraHandler.obtainMessage(CameraActions.AUTO_FOCUS, cbForward) |
| .sendToTarget(); |
| } |
| }); |
| } catch (RuntimeException ex) { |
| mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex); |
| } |
| } |
| |
| @TargetApi(Build.VERSION_CODES.JELLY_BEAN) |
| @Override |
| public void setAutoFocusMoveCallback(final Handler handler, final CameraAFMoveCallback cb) { |
| try { |
| mDispatchThread.runJob(new Runnable() { |
| @Override |
| public void run() { |
| CameraAFMoveCallback cbForward = null; |
| if (cb != null) { |
| cbForward = new CameraAFMoveCallback() { |
| @Override |
| public void onAutoFocusMoving(final boolean moving, |
| final CameraProxy camera) { |
| handler.post(new Runnable() { |
| @Override |
| public void run() { |
| cb.onAutoFocusMoving(moving, camera); |
| } |
| }); |
| } |
| }; |
| } |
| |
| mCameraHandler.obtainMessage(CameraActions.SET_AUTO_FOCUS_MOVE_CALLBACK, |
| cbForward).sendToTarget(); |
| } |
| }); |
| } catch (RuntimeException ex) { |
| mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex); |
| } |
| } |
| |
| @Override |
| public void takePicture(final Handler handler, |
| final CameraShutterCallback shutter, |
| CameraPictureCallback raw, |
| CameraPictureCallback postview, |
| final CameraPictureCallback jpeg) { |
| // TODO: We never call raw or postview |
| final CaptureAvailableListener picListener = |
| new CaptureAvailableListener() { |
| @Override |
| public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, |
| long timestamp, long frameNumber) { |
| if (shutter != null) { |
| handler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (mShutterSoundEnabled) { |
| mNoisemaker.play(MediaActionSound.SHUTTER_CLICK); |
| } |
| shutter.onShutter(AndroidCamera2ProxyImpl.this); |
| }}); |
| } |
| } |
| |
| @Override |
| public void onImageAvailable(ImageReader reader) { |
| try (Image image = reader.acquireNextImage()) { |
| if (jpeg != null) { |
| ByteBuffer buffer = image.getPlanes()[0].getBuffer(); |
| final byte[] pixels = new byte[buffer.remaining()]; |
| buffer.get(pixels); |
| handler.post(new Runnable() { |
| @Override |
| public void run() { |
| jpeg.onPictureTaken(pixels, AndroidCamera2ProxyImpl.this); |
| }}); |
| } |
| } |
| }}; |
| try { |
| mDispatchThread.runJob(new Runnable() { |
| @Override |
| public void run() { |
| // Wait until PREVIEW_ACTIVE or better |
| mCameraState.waitForStates( |
| ~(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE - 1)); |
| mCameraHandler.obtainMessage(CameraActions.CAPTURE_PHOTO, picListener) |
| .sendToTarget(); |
| } |
| }); |
| } catch (RuntimeException ex) { |
| mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex); |
| } |
| } |
| |
| // TODO: Implement |
| @Override |
| public void setZoomChangeListener(android.hardware.Camera.OnZoomChangeListener listener) {} |
| |
| // TODO: Implement |
| @Override |
| public void setFaceDetectionCallback(Handler handler, CameraFaceDetectionCallback callback) |
| {} |
| |
| // TODO: Remove this method override once we handle this message |
| @Override |
| public void startFaceDetection() {} |
| |
| // TODO: Remove this method override once we handle this message |
| @Override |
| public void stopFaceDetection() {} |
| |
| // TODO: Implement |
| @Override |
| public void setParameters(android.hardware.Camera.Parameters params) {} |
| |
| // TODO: Implement |
| @Override |
| public android.hardware.Camera.Parameters getParameters() { return null; } |
| |
| @Override |
| public CameraSettings getSettings() { |
| if (mLastSettings == null) { |
| mLastSettings = mCameraHandler.buildSettings(mCapabilities); |
| } |
| return mLastSettings; |
| } |
| |
| @Override |
| public boolean applySettings(CameraSettings settings) { |
| if (settings == null) { |
| Log.w(TAG, "null parameters in applySettings()"); |
| return false; |
| } |
| if (!(settings instanceof AndroidCamera2Settings)) { |
| Log.e(TAG, "Provided settings not compatible with the backing framework API"); |
| return false; |
| } |
| |
| // Wait for any state that isn't OPENED |
| if (applySettingsHelper(settings, ~AndroidCamera2StateHolder.CAMERA_UNOPENED)) { |
| mLastSettings = settings; |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public void enableShutterSound(boolean enable) { |
| mShutterSoundEnabled = enable; |
| } |
| |
| // TODO: Implement |
| @Override |
| public String dumpDeviceSettings() { return null; } |
| |
| @Override |
| public Handler getCameraHandler() { |
| return AndroidCamera2AgentImpl.this.getCameraHandler(); |
| } |
| |
| @Override |
| public DispatchThread getDispatchThread() { |
| return AndroidCamera2AgentImpl.this.getDispatchThread(); |
| } |
| |
| @Override |
| public CameraStateHolder getCameraState() { |
| return mCameraState; |
| } |
| } |
| |
| /** A linear state machine: each state entails all the states below it. */ |
| private static class AndroidCamera2StateHolder extends CameraStateHolder { |
| // Usage flow: openCamera() -> applySettings() -> setPreviewTexture() -> startPreview() -> |
| // autoFocus() -> takePicture() |
| // States are mutually exclusive, but must be separate bits so that they can be used with |
| // the StateHolder#waitForStates() and StateHolder#waitToAvoidStates() methods. |
| // Do not set the state to be a combination of these values! |
| /* Camera states */ |
| /** No camera device is opened. */ |
| public static final int CAMERA_UNOPENED = 1 << 0; |
| /** A camera is opened, but no settings have been provided. */ |
| public static final int CAMERA_UNCONFIGURED = 1 << 1; |
| /** The open camera has been configured by providing it with settings. */ |
| public static final int CAMERA_CONFIGURED = 1 << 2; |
| /** A capture session is ready to stream a preview, but still has no repeating request. */ |
| public static final int CAMERA_PREVIEW_READY = 1 << 3; |
| /** A preview is currently being streamed. */ |
| public static final int CAMERA_PREVIEW_ACTIVE = 1 << 4; |
| /** The lens is locked on a particular region. */ |
| public static final int CAMERA_FOCUS_LOCKED = 1 << 5; |
| |
| public AndroidCamera2StateHolder() { |
| this(CAMERA_UNOPENED); |
| } |
| |
| public AndroidCamera2StateHolder(int state) { |
| super(state); |
| } |
| } |
| |
| private static class AndroidCamera2DeviceInfo implements CameraDeviceInfo { |
| private final CameraManager mCameraManager; |
| private final String[] mCameraIds; |
| private final int mNumberOfCameras; |
| private final int mFirstBackCameraId; |
| private final int mFirstFrontCameraId; |
| |
| public AndroidCamera2DeviceInfo(CameraManager cameraManager, |
| String[] cameraIds, int numberOfCameras) { |
| mCameraManager = cameraManager; |
| mCameraIds = cameraIds; |
| mNumberOfCameras = numberOfCameras; |
| |
| int firstBackId = NO_DEVICE; |
| int firstFrontId = NO_DEVICE; |
| for (int id = 0; id < cameraIds.length; ++id) { |
| try { |
| int lensDirection = cameraManager.getCameraCharacteristics(cameraIds[id]) |
| .get(CameraCharacteristics.LENS_FACING); |
| if (firstBackId == NO_DEVICE && |
| lensDirection == CameraCharacteristics.LENS_FACING_BACK) { |
| firstBackId = id; |
| } |
| if (firstFrontId == NO_DEVICE && |
| lensDirection == CameraCharacteristics.LENS_FACING_FRONT) { |
| firstFrontId = id; |
| } |
| } catch (CameraAccessException ex) { |
| Log.w(TAG, "Couldn't get characteristics of camera '" + id + "'", ex); |
| } |
| } |
| mFirstBackCameraId = firstBackId; |
| mFirstFrontCameraId = firstFrontId; |
| } |
| |
| @Override |
| public Characteristics getCharacteristics(int cameraId) { |
| String actualId = mCameraIds[cameraId]; |
| try { |
| CameraCharacteristics info = mCameraManager.getCameraCharacteristics(actualId); |
| return new AndroidCharacteristics2(info); |
| } catch (CameraAccessException ex) { |
| return null; |
| } |
| } |
| |
| @Override |
| public int getNumberOfCameras() { |
| return mNumberOfCameras; |
| } |
| |
| @Override |
| public int getFirstBackCameraId() { |
| return mFirstBackCameraId; |
| } |
| |
| @Override |
| public int getFirstFrontCameraId() { |
| return mFirstFrontCameraId; |
| } |
| |
| private static class AndroidCharacteristics2 extends Characteristics { |
| private CameraCharacteristics mCameraInfo; |
| |
| AndroidCharacteristics2(CameraCharacteristics cameraInfo) { |
| mCameraInfo = cameraInfo; |
| } |
| |
| @Override |
| public boolean isFacingBack() { |
| return mCameraInfo.get(CameraCharacteristics.LENS_FACING) |
| .equals(CameraCharacteristics.LENS_FACING_BACK); |
| } |
| |
| @Override |
| public boolean isFacingFront() { |
| return mCameraInfo.get(CameraCharacteristics.LENS_FACING) |
| .equals(CameraCharacteristics.LENS_FACING_FRONT); |
| } |
| |
| @Override |
| public int getSensorOrientation() { |
| return mCameraInfo.get(CameraCharacteristics.SENSOR_ORIENTATION); |
| } |
| |
| @Override |
| public Matrix getPreviewTransform(int currentDisplayOrientation, |
| RectF surfaceDimensions, |
| RectF desiredBounds) { |
| if (!orientationIsValid(currentDisplayOrientation)) { |
| return new Matrix(); |
| } |
| |
| // The system transparently transforms the image to fill the surface |
| // when the device is in its natural orientation. We rotate the |
| // coordinates of the rectangle's corners to be relative to the |
| // original image, instead of to the current screen orientation. |
| float[] surfacePolygon = rotate(convertRectToPoly(surfaceDimensions), |
| 2 * currentDisplayOrientation / 90); |
| float[] desiredPolygon = convertRectToPoly(desiredBounds); |
| |
| Matrix transform = new Matrix(); |
| // Use polygons instead of rectangles so that rotation will be |
| // calculated, since that is not done by the new camera API. |
| transform.setPolyToPoly(surfacePolygon, 0, desiredPolygon, 0, 4); |
| return transform; |
| } |
| |
| @Override |
| public boolean canDisableShutterSound() { |
| return true; |
| } |
| |
| private static float[] convertRectToPoly(RectF rf) { |
| return new float[] {rf.left, rf.top, rf.right, rf.top, |
| rf.right, rf.bottom, rf.left, rf.bottom}; |
| } |
| |
| private static float[] rotate(float[] arr, int times) { |
| if (times < 0) { |
| times = times % arr.length + arr.length; |
| } |
| |
| float[] res = new float[arr.length]; |
| for (int offset = 0; offset < arr.length; ++offset) { |
| res[offset] = arr[(times + offset) % arr.length]; |
| } |
| return res; |
| } |
| } |
| } |
| } |