diff options
Diffstat (limited to 'camera2')
41 files changed, 8742 insertions, 77 deletions
diff --git a/camera2/portability/Android.mk b/camera2/portability/Android.mk new file mode 100644 index 0000000..95b6448 --- /dev/null +++ b/camera2/portability/Android.mk @@ -0,0 +1,17 @@ +# Copyright 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. + +LOCAL_PATH := $(call my-dir) +include $(LOCAL_PATH)/portability.mk \ + $(call all-subdir-makefiles) diff --git a/camera2/portability/portability.mk b/camera2/portability/portability.mk new file mode 100644 index 0000000..2ecc1df --- /dev/null +++ b/camera2/portability/portability.mk @@ -0,0 +1,24 @@ +# Copyright 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE := android-ex-camera2-portability +LOCAL_MODULE_TAGS := optional +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2-utils + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java new file mode 100644 index 0000000..d139c62 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java @@ -0,0 +1,1207 @@ +/* + * 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; + + /** + * 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()); + 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 setCameraDefaultExceptionCallback(CameraExceptionCallback callback, + Handler handler) {} + + // 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; + } + + private static abstract class CaptureAvailableListener + extends CameraCaptureSession.CaptureListener + implements ImageReader.OnImageAvailableListener {}; + + private class Camera2Handler extends HistoryHandler { + // Caller-provided when leaving CAMERA_UNOPENED state: + private CameraOpenCallback mOpenCallback; + private int mCameraIndex; + private String mCameraId; + + // 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; + + Camera2Handler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(final Message msg) { + super.handleMessage(msg); + try { + switch(msg.what) { + 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, mCameraDeviceStateListener, 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; + mCameraState.setState(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; + mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE); + try { + mSession.setRepeatingRequest( + mPersistentSettings.createRequest(mCamera, + CameraDevice.TEMPLATE_PREVIEW, mPreviewSurface), + /*listener*/mCameraFocusStateListener, /*handler*/this); + } catch(CameraAccessException ex) { + Log.w(TAG, "Unable to start preview", ex); + mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_READY); + } + break; + } + + case CameraActions.STOP_PREVIEW: { + if (mCameraState.getState() < + AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE) { + Log.w(TAG, "Refusing to stop preview at inappropriate time"); + break; + } + + mSession.stopRepeating(); + mCameraState.setState(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: { + // 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 completes. + // 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.CaptureListener deferredCallbackSetter = + new CameraCaptureSession.CaptureListener() { + @Override + public void onCaptureCompleted(CameraCaptureSession session, + CaptureRequest request, + TotalCaptureResult result) { + // Now our mCameraFocusStateListener will invoke the callback the + // first time it finds the focus motor to be locked. + mOneshotAfCallback = callback; + } + + @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. + mCameraState.setState(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); + mCameraState.setState(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE); + } + break; + } + + case CameraActions.CANCEL_AUTO_FOCUS: { + // 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. + mCameraState.setState(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); + mCameraState.setState( + AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED); + } + 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 + mPersistentSettings.set(CaptureRequest.JPEG_ORIENTATION, msg.arg2 > 0 ? + mCameraProxy.getCharacteristics().getJpegOrientation(msg.arg1) : 0); + 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) { + // Just snap the shot + 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 legacy capture", ex); + } + } else { + // Not a legacy device, so we need to let AE converge before capturing + CameraCaptureSession.CaptureListener deferredCallbackSetter = + new CameraCaptureSession.CaptureListener() { + @Override + public void onCaptureCompleted(CameraCaptureSession session, + CaptureRequest request, + TotalCaptureResult result) { + mOneshotCaptureCallback = listener; + } + + @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 (msg.what != CameraActions.RELEASE && mCamera != null) { + // TODO: Handle this better + mCamera.close(); + mCamera = null; + } else if (mCamera == null) { + if (msg.what == 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) { + post(new Runnable() { + @Override + public void run() { + sCameraExceptionCallback.onCameraException((RuntimeException) ex); + }}); + } + } + } + + 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*/mCameraFocusStateListener, /*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 + mCameraState.setState(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()), + mCameraPreviewStateListener, 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); + } + mCameraState.setState(AndroidCamera2StateHolder.CAMERA_CONFIGURED); + } + + // This listener monitors our connection to and disconnection from camera devices. + private CameraDevice.StateListener mCameraDeviceStateListener = + new CameraDevice.StateListener() { + @Override + public void onOpened(CameraDevice camera) { + mCamera = camera; + if (mOpenCallback != null) { + try { + CameraCharacteristics props = + mCameraManager.getCameraCharacteristics(mCameraId); + mCameraProxy = new AndroidCamera2ProxyImpl(mCameraIndex, mCamera, + getCameraDeviceInfo().getCharacteristics(mCameraIndex), 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; + mCameraState.setState(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 listener monitors our camera session (i.e. our transition into and out of preview). + private CameraCaptureSession.StateListener mCameraPreviewStateListener = + new CameraCaptureSession.StateListener() { + @Override + public void onConfigured(CameraCaptureSession session) { + mSession = session; + mCameraState.setState(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; + } + }}; + + // This listener monitors requested captures and notifies any relevant callbacks. + private CameraCaptureSession.CaptureListener mCameraFocusStateListener = + new CameraCaptureSession.CaptureListener() { + private int mLastAfState = -1; + + @Override + public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) { + Integer afStateMaybe = result.get(CaptureResult.CONTROL_AF_STATE); + if (afStateMaybe != null) { + int afState = afStateMaybe; + boolean afStateChanged = false; + if (afState != mLastAfState) { + mLastAfState = afState; + afStateChanged = true; + } + + 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: { + 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 now 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; + + switch (aeState) { + case CaptureResult.CONTROL_AE_STATE_CONVERGED: + case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED: + case CaptureResult.CONTROL_AE_STATE_LOCKED: { + if (mOneshotCaptureCallback != null) { + // A call to takePicture() was just made, and autoexposure converged + // so it's time to initiate the capture! + mCaptureReader.setOnImageAvailableListener(mOneshotCaptureCallback, + /*handler*/Camera2Handler.this); + try { + mSession.capture( + mPersistentSettings.createRequest(mCamera, + CameraDevice.TEMPLATE_STILL_CAPTURE, + mCaptureReader.getSurface()), + /*listener*/mOneshotCaptureCallback, + /*handler*/Camera2Handler.this); + } catch (CameraAccessException ex) { + Log.e(TAG, "Unable to initiate capture", ex); + } finally { + mOneshotCaptureCallback = null; + } + } + break; + } + } + } + } + + @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 int mCameraIndex; + private final CameraDevice mCamera; + private final CameraDeviceInfo.Characteristics mCharacteristics; + private final AndroidCamera2Capabilities mCapabilities; + + public AndroidCamera2ProxyImpl(int cameraIndex, CameraDevice camera, + CameraDeviceInfo.Characteristics characteristics, + CameraCharacteristics properties) { + mCameraIndex = cameraIndex; + mCamera = camera; + mCharacteristics = characteristics; + mCapabilities = new AndroidCamera2Capabilities(properties); + } + + // 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; + } + + private AndroidCamera2Capabilities getSpecializedCapabilities() { + return mCapabilities; + } + + // 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) { + 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(); + }}); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Override + public void setAutoFocusMoveCallback(final Handler handler, final CameraAFMoveCallback cb) { + 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(); + }}); + } + + @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) { + if (shutter != null) { + handler.post(new Runnable() { + @Override + public void run() { + 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); + }}); + } + } + }}; + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraState.waitForStates(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE | + AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED); + mCameraHandler.obtainMessage(CameraActions.CAPTURE_PHOTO, picListener) + .sendToTarget(); + }}); + } + + // 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 setErrorCallback(Handler handler, CameraErrorCallback cb) {} + + // 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() { + return mCameraHandler.buildSettings(mCapabilities); + } + + @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; + } + + return applySettingsHelper(settings, AndroidCamera2StateHolder.CAMERA_UNCONFIGURED | + AndroidCamera2StateHolder.CAMERA_CONFIGURED | + AndroidCamera2StateHolder.CAMERA_PREVIEW_READY); + } + + // 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() + /* Camera states */ + /** No camera device is opened. */ + public static final int CAMERA_UNOPENED = 1; + /** A camera is opened, but no settings have been provided. */ + public static final int CAMERA_UNCONFIGURED = 2; + /** The open camera has been configured by providing it with settings. */ + public static final int CAMERA_CONFIGURED = 3; + /** A capture session is ready to stream a preview, but still has no repeating request. */ + public static final int CAMERA_PREVIEW_READY = 4; + /** A preview is currently being streamed. */ + public static final int CAMERA_PREVIEW_ACTIVE = 5; + /** The lens is locked on a particular region. */ + public static final int CAMERA_FOCUS_LOCKED = 6; + + 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() { + // The new API doesn't support this operation, so don't encourage people to try it. + // TODO: What kind of assumptions have callers made about this result's meaning? + return false; + } + + 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; + } + } + } + + private static final CameraExceptionCallback sCameraExceptionCallback = + new CameraExceptionCallback() { + @Override + public synchronized void onCameraException(RuntimeException e) { + throw e; + } + }; +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java new file mode 100644 index 0000000..51c1422 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java @@ -0,0 +1,259 @@ +/* + * 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 static android.hardware.camera2.CameraCharacteristics.*; + +import android.graphics.ImageFormat; +import android.graphics.Point; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.MediaRecorder; +import android.util.Range; +import android.util.Rational; + +import com.android.ex.camera2.portability.debug.Log; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * The subclass of {@link CameraCapabilities} for Android Camera 2 API. + */ +public class AndroidCamera2Capabilities extends CameraCapabilities { + private static Log.Tag TAG = new Log.Tag("AndCam2Capabs"); + + AndroidCamera2Capabilities(CameraCharacteristics p) { + super(new Stringifier()); + + StreamConfigurationMap s = p.get(SCALER_STREAM_CONFIGURATION_MAP); + + for (Range<Integer> fpsRange : p.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)) { + mSupportedPreviewFpsRange.add(new int[] { fpsRange.getLower(), fpsRange.getUpper() }); + } + + // TODO: We only support TextureView preview rendering + mSupportedPreviewSizes.addAll(Size.buildListFromAndroidSizes(Arrays.asList( + s.getOutputSizes(SurfaceTexture.class)))); + for (int format : s.getOutputFormats()) { + mSupportedPreviewFormats.add(format); + } + + // TODO: We only support MediaRecorder video capture + mSupportedVideoSizes.addAll(Size.buildListFromAndroidSizes(Arrays.asList( + s.getOutputSizes(MediaRecorder.class)))); + + // TODO: We only support JPEG image capture + mSupportedPhotoSizes.addAll(Size.buildListFromAndroidSizes(Arrays.asList( + s.getOutputSizes(ImageFormat.JPEG)))); + mSupportedPhotoFormats.addAll(mSupportedPreviewFormats); + + buildSceneModes(p); + buildFlashModes(p); + buildFocusModes(p); + buildWhiteBalances(p); + // TODO: Populate mSupportedFeatures + + // TODO: Populate mPreferredPreviewSizeForVideo + + Range<Integer> ecRange = p.get(CONTROL_AE_COMPENSATION_RANGE); + mMinExposureCompensation = ecRange.getLower(); + mMaxExposureCompensation = ecRange.getUpper(); + + Rational ecStep = p.get(CONTROL_AE_COMPENSATION_STEP); + mExposureCompensationStep = (float) ecStep.getNumerator() / ecStep.getDenominator(); + + mMaxNumOfFacesSupported = p.get(STATISTICS_INFO_MAX_FACE_COUNT); + mMaxNumOfMeteringArea = p.get(CONTROL_MAX_REGIONS_AE); + + // TODO: Populate mMaxZoomRatio + // TODO: Populate mHorizontalViewAngle + // TODO: Populate mVerticalViewAngle + // TODO: Populate mZoomRatioList + // TODO: Populate mMaxZoomIndex + + if (supports(FocusMode.AUTO)) { + mMaxNumOfFocusAreas = p.get(CONTROL_MAX_REGIONS_AF); + if (mMaxNumOfFocusAreas > 0) { + mSupportedFeatures.add(Feature.FOCUS_AREA); + } + } + if (mMaxNumOfMeteringArea > 0) { + mSupportedFeatures.add(Feature.METERING_AREA); + } + + // TODO: Detect other features + } + + private void buildSceneModes(CameraCharacteristics p) { + int[] scenes = p.get(CONTROL_AVAILABLE_SCENE_MODES); + if (scenes != null) { + for (int scene : scenes) { + SceneMode equiv = sceneModeFromInt(scene); + if (equiv != null) { + mSupportedSceneModes.add(equiv); + } + } + } + } + + private void buildFlashModes(CameraCharacteristics p) { + mSupportedFlashModes.add(FlashMode.OFF); + if (p.get(FLASH_INFO_AVAILABLE)) { + mSupportedFlashModes.add(FlashMode.AUTO); + mSupportedFlashModes.add(FlashMode.ON); + mSupportedFlashModes.add(FlashMode.TORCH); + for (int expose : p.get(CONTROL_AE_AVAILABLE_MODES)) { + if (expose == CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE) { + mSupportedFlashModes.add(FlashMode.RED_EYE); + } + } + } + } + + private void buildFocusModes(CameraCharacteristics p) { + int[] focuses = p.get(CONTROL_AF_AVAILABLE_MODES); + if (focuses != null) { + for (int focus : focuses) { + FocusMode equiv = focusModeFromInt(focus); + if (equiv != null) { + mSupportedFocusModes.add(equiv); + } + } + } + } + + private void buildWhiteBalances(CameraCharacteristics p) { + int[] bals = p.get(CONTROL_AWB_AVAILABLE_MODES); + if (bals != null) { + for (int bal : bals) { + WhiteBalance equiv = whiteBalanceFromInt(bal); + if (equiv != null) { + mSupportedWhiteBalances.add(equiv); + } + } + } + } + + /** + * Converts the API-related integer representation of the focus mode to the + * abstract representation. + * + * @param fm The integral representation. + * @return The mode represented by the input integer, or {@code null} if it + * cannot be converted. + */ + public static FocusMode focusModeFromInt(int fm) { + switch (fm) { + case CONTROL_AF_MODE_AUTO: + return FocusMode.AUTO; + case CONTROL_AF_MODE_CONTINUOUS_PICTURE: + return FocusMode.CONTINUOUS_PICTURE; + case CONTROL_AF_MODE_CONTINUOUS_VIDEO: + return FocusMode.CONTINUOUS_VIDEO; + case CONTROL_AF_MODE_EDOF: + return FocusMode.EXTENDED_DOF; + case CONTROL_AF_MODE_OFF: + return FocusMode.FIXED; + // TODO: We cannot support INFINITY + case CONTROL_AF_MODE_MACRO: + return FocusMode.MACRO; + } + Log.w(TAG, "Unable to convert from API 2 focus mode: " + fm); + return null; + } + + /** + * Converts the API-related integer representation of the scene mode to the + * abstract representation. + * + * @param sm The integral representation. + * @return The mode represented by the input integer, or {@code null} if it + * cannot be converted. + */ + public static SceneMode sceneModeFromInt(int sm) { + switch (sm) { + case CONTROL_SCENE_MODE_DISABLED: + return SceneMode.AUTO; + case CONTROL_SCENE_MODE_ACTION: + return SceneMode.ACTION; + case CONTROL_SCENE_MODE_BARCODE: + return SceneMode.BARCODE; + case CONTROL_SCENE_MODE_BEACH: + return SceneMode.BEACH; + case CONTROL_SCENE_MODE_CANDLELIGHT: + return SceneMode.CANDLELIGHT; + case CONTROL_SCENE_MODE_FIREWORKS: + return SceneMode.FIREWORKS; + // TODO: We cannot support HDR + case CONTROL_SCENE_MODE_LANDSCAPE: + return SceneMode.LANDSCAPE; + case CONTROL_SCENE_MODE_NIGHT: + return SceneMode.NIGHT; + // TODO: We cannot support NIGHT_PORTRAIT + case CONTROL_SCENE_MODE_PARTY: + return SceneMode.PARTY; + case CONTROL_SCENE_MODE_PORTRAIT: + return SceneMode.PORTRAIT; + case CONTROL_SCENE_MODE_SNOW: + return SceneMode.SNOW; + case CONTROL_SCENE_MODE_SPORTS: + return SceneMode.SPORTS; + case CONTROL_SCENE_MODE_STEADYPHOTO: + return SceneMode.STEADYPHOTO; + case CONTROL_SCENE_MODE_SUNSET: + return SceneMode.SUNSET; + case CONTROL_SCENE_MODE_THEATRE: + return SceneMode.THEATRE; + // TODO: We cannot expose FACE_PRIORITY, or HIGH_SPEED_VIDEO + } + Log.w(TAG, "Unable to convert from API 2 scene mode: " + sm); + return null; + } + + /** + * Converts the API-related integer representation of the white balance to + * the abstract representation. + * + * @param wb The integral representation. + * @return The balance represented by the input integer, or {@code null} if + * it cannot be converted. + */ + public static WhiteBalance whiteBalanceFromInt(int wb) { + switch (wb) { + case CONTROL_AWB_MODE_AUTO: + return WhiteBalance.AUTO; + case CONTROL_AWB_MODE_CLOUDY_DAYLIGHT: + return WhiteBalance.CLOUDY_DAYLIGHT; + case CONTROL_AWB_MODE_DAYLIGHT: + return WhiteBalance.DAYLIGHT; + case CONTROL_AWB_MODE_FLUORESCENT: + return WhiteBalance.FLUORESCENT; + case CONTROL_AWB_MODE_INCANDESCENT: + return WhiteBalance.INCANDESCENT; + case CONTROL_AWB_MODE_SHADE: + return WhiteBalance.SHADE; + case CONTROL_AWB_MODE_TWILIGHT: + return WhiteBalance.TWILIGHT; + case CONTROL_AWB_MODE_WARM_FLUORESCENT: + return WhiteBalance.WARM_FLUORESCENT; + } + Log.w(TAG, "Unable to convert from API 2 white balance: " + wb); + return null; + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java new file mode 100644 index 0000000..efa68e8 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java @@ -0,0 +1,472 @@ +/* + * 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 static android.hardware.camera2.CaptureRequest.*; + +import android.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Range; + +import com.android.ex.camera2.portability.CameraCapabilities.FlashMode; +import com.android.ex.camera2.portability.CameraCapabilities.FocusMode; +import com.android.ex.camera2.portability.CameraCapabilities.SceneMode; +import com.android.ex.camera2.portability.CameraCapabilities.WhiteBalance; +import com.android.ex.camera2.portability.debug.Log; +import com.android.ex.camera2.utils.Camera2RequestSettingsSet; + +import java.util.List; +import java.util.Objects; + +/** + * The subclass of {@link CameraSettings} for Android Camera 2 API. + */ +public class AndroidCamera2Settings extends CameraSettings { + private static final Log.Tag TAG = new Log.Tag("AndCam2Set"); + + private final Builder mTemplateSettings; + private final Rect mActiveArray; + private final Camera2RequestSettingsSet mRequestSettings; + + /** + * Create a settings representation that answers queries of unspecified + * options in the same way as the provided template would. + * + * <p>The default settings provided by the given template are only ever used + * for reporting back to the client app (i.e. when it queries an option + * it didn't explicitly set first). {@link Camera2RequestSettingsSet}s + * generated by an instance of this class will have any settings not + * modified using one of that instance's mutators forced to default, so that + * their effective values when submitting a capture request will be those of + * the template that is provided to the camera framework at that time.</p> + * + * @param camera Device from which to draw default settings. + * @param template Specific template to use for the defaults. + * @param activeArray Boundary coordinates of the sensor's active array. + * @param preview Dimensions of preview streams. + * @param photo Dimensions of captured images. + * + * @throws CameraAccessException Upon internal framework/driver failure. + */ + public AndroidCamera2Settings(CameraDevice camera, int template, Rect activeArray, + Size preview, Size photo) throws CameraAccessException { + mTemplateSettings = camera.createCaptureRequest(template); + mActiveArray = activeArray; + mRequestSettings = new Camera2RequestSettingsSet(); + + Range<Integer> previewFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE); + if (previewFpsRange != null) { + setPreviewFpsRange(previewFpsRange.getLower(), previewFpsRange.getUpper()); + } + setPreviewSize(preview); + // TODO: mCurrentPreviewFormat + setPhotoSize(photo); + mJpegCompressQuality = queryTemplateDefaultOrMakeOneUp(JPEG_QUALITY, (byte) 0); + // TODO: mCurrentPhotoFormat + // TODO: mCurrentZoomRatio + mCurrentZoomRatio = 1.0f; + // TODO: mCurrentZoomIndex + mExposureCompensationIndex = + queryTemplateDefaultOrMakeOneUp(CONTROL_AE_EXPOSURE_COMPENSATION, 0); + + mCurrentFlashMode = flashModeFromRequest(); + Integer currentFocusMode = mTemplateSettings.get(CONTROL_AF_MODE); + if (currentFocusMode != null) { + mCurrentFocusMode = AndroidCamera2Capabilities.focusModeFromInt(currentFocusMode); + } + Integer currentSceneMode = mTemplateSettings.get(CONTROL_SCENE_MODE); + if (currentSceneMode != null) { + mCurrentSceneMode = AndroidCamera2Capabilities.sceneModeFromInt(currentSceneMode); + } + Integer whiteBalance = mTemplateSettings.get(CONTROL_AWB_MODE); + if (whiteBalance != null) { + mWhiteBalance = AndroidCamera2Capabilities.whiteBalanceFromInt(whiteBalance); + } + + mVideoStabilizationEnabled = queryTemplateDefaultOrMakeOneUp( + CONTROL_VIDEO_STABILIZATION_MODE, CONTROL_VIDEO_STABILIZATION_MODE_OFF) == + CONTROL_VIDEO_STABILIZATION_MODE_ON; + mAutoExposureLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AE_LOCK, false); + mAutoWhiteBalanceLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AWB_LOCK, false); + // TODO: mRecordingHintEnabled + // TODO: mGpsData + android.util.Size exifThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE); + if (exifThumbnailSize != null) { + mExifThumbnailSize = + new Size(exifThumbnailSize.getWidth(), exifThumbnailSize.getHeight()); + } + } + + public AndroidCamera2Settings(AndroidCamera2Settings other) { + super(other); + mTemplateSettings = other.mTemplateSettings; + mActiveArray = other.mActiveArray; + mRequestSettings = new Camera2RequestSettingsSet(other.mRequestSettings); + } + + @Override + public CameraSettings copy() { + return new AndroidCamera2Settings(this); + } + + private <T> T queryTemplateDefaultOrMakeOneUp(Key<T> key, T defaultDefault) { + T val = mTemplateSettings.get(key); + if (val != null) { + return val; + } else { + // Spoof the default so matchesTemplateDefault excludes this key from generated sets. + // This approach beats a simple sentinel because it provides basic boolean support. + mTemplateSettings.set(key, defaultDefault); + return defaultDefault; + } + } + + private FlashMode flashModeFromRequest() { + Integer autoExposure = mTemplateSettings.get(CONTROL_AE_MODE); + if (autoExposure != null) { + switch (autoExposure) { + case CONTROL_AE_MODE_ON: + return FlashMode.OFF; + case CONTROL_AE_MODE_ON_AUTO_FLASH: + return FlashMode.AUTO; + case CONTROL_AE_MODE_ON_ALWAYS_FLASH: { + if (mTemplateSettings.get(FLASH_MODE) == FLASH_MODE_TORCH) { + return FlashMode.TORCH; + } else { + return FlashMode.ON; + } + } + case CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE: + return FlashMode.RED_EYE; + } + } + return null; + } + + private boolean matchesTemplateDefault(Key<?> setting) { + if (setting == CONTROL_AE_REGIONS) { + return mMeteringAreas.size() == 0; + } else if (setting == CONTROL_AF_REGIONS) { + return mFocusAreas.size() == 0; + } else if (setting == CONTROL_AE_TARGET_FPS_RANGE) { + Range defaultFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE); + return (mPreviewFpsRangeMin == 0 && mPreviewFpsRangeMax == 0) || + (defaultFpsRange != null && mPreviewFpsRangeMin == defaultFpsRange.getLower() && + mPreviewFpsRangeMax == defaultFpsRange.getUpper()); + } else if (setting == JPEG_QUALITY) { + return Objects.equals(mJpegCompressQuality, + mTemplateSettings.get(JPEG_QUALITY)); + } else if (setting == CONTROL_AE_EXPOSURE_COMPENSATION) { + return Objects.equals(mExposureCompensationIndex, + mTemplateSettings.get(CONTROL_AE_EXPOSURE_COMPENSATION)); + } else if (setting == CONTROL_VIDEO_STABILIZATION_MODE) { + Integer videoStabilization = mTemplateSettings.get(CONTROL_VIDEO_STABILIZATION_MODE); + return (videoStabilization != null && + (mVideoStabilizationEnabled && videoStabilization == + CONTROL_VIDEO_STABILIZATION_MODE_ON) || + (!mVideoStabilizationEnabled && videoStabilization == + CONTROL_VIDEO_STABILIZATION_MODE_OFF)); + } else if (setting == CONTROL_AE_LOCK) { + return Objects.equals(mAutoExposureLocked, mTemplateSettings.get(CONTROL_AE_LOCK)); + } else if (setting == CONTROL_AWB_LOCK) { + return Objects.equals(mAutoWhiteBalanceLocked, mTemplateSettings.get(CONTROL_AWB_LOCK)); + } else if (setting == JPEG_THUMBNAIL_SIZE) { + android.util.Size defaultThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE); + return (mExifThumbnailSize.width() == 0 && mExifThumbnailSize.height() == 0) || + (defaultThumbnailSize != null && + mExifThumbnailSize.width() == defaultThumbnailSize.getWidth() && + mExifThumbnailSize.height() == defaultThumbnailSize.getHeight()); + } + Log.w(TAG, "Settings implementation checked default of unhandled option key"); + // Since this class isn't equipped to handle it, claim it matches the default to prevent + // updateRequestSettingOrForceToDefault from going with the user-provided preference + return true; + } + + private <T> void updateRequestSettingOrForceToDefault(Key<T> setting, T possibleChoice) { + mRequestSettings.set(setting, matchesTemplateDefault(setting) ? null : possibleChoice); + } + + public Camera2RequestSettingsSet getRequestSettings() { + updateRequestSettingOrForceToDefault(CONTROL_AE_REGIONS, + legacyAreasToMeteringRectangles(mMeteringAreas)); + updateRequestSettingOrForceToDefault(CONTROL_AF_REGIONS, + legacyAreasToMeteringRectangles(mFocusAreas)); + updateRequestSettingOrForceToDefault(CONTROL_AE_TARGET_FPS_RANGE, + new Range(mPreviewFpsRangeMin, mPreviewFpsRangeMax)); + // TODO: mCurrentPreviewFormat + updateRequestSettingOrForceToDefault(JPEG_QUALITY, mJpegCompressQuality); + // TODO: mCurrentPhotoFormat + // TODO: mCurrentZoomRatio + // TODO: mCurrentZoomIndex + updateRequestSettingOrForceToDefault(CONTROL_AE_EXPOSURE_COMPENSATION, + mExposureCompensationIndex); + updateRequestFlashMode(); + updateRequestFocusMode(); + updateRequestSceneMode(); + updateRequestWhiteBalance(); + updateRequestSettingOrForceToDefault(CONTROL_VIDEO_STABILIZATION_MODE, + mVideoStabilizationEnabled ? + CONTROL_VIDEO_STABILIZATION_MODE_ON : CONTROL_VIDEO_STABILIZATION_MODE_OFF); + // OIS shouldn't be on if software video stabilization is. + mRequestSettings.set(LENS_OPTICAL_STABILIZATION_MODE, + mVideoStabilizationEnabled ? LENS_OPTICAL_STABILIZATION_MODE_OFF : + null); + updateRequestSettingOrForceToDefault(CONTROL_AE_LOCK, mAutoExposureLocked); + updateRequestSettingOrForceToDefault(CONTROL_AWB_LOCK, mAutoWhiteBalanceLocked); + // TODO: mRecordingHintEnabled + // TODO: mGpsData + updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE, + new android.util.Size( + mExifThumbnailSize.width(), mExifThumbnailSize.height())); + + return mRequestSettings; + } + + private MeteringRectangle[] legacyAreasToMeteringRectangles( + List<android.hardware.Camera.Area> reference) { + MeteringRectangle[] transformed = null; + if (reference.size() > 0) { + + transformed = new MeteringRectangle[reference.size()]; + for (int index = 0; index < reference.size(); ++index) { + android.hardware.Camera.Area source = reference.get(index); + Rect rectangle = source.rect; + + // Old API coordinates were [-1000,1000]; new ones are [0,ACTIVE_ARRAY_SIZE). + double oldLeft = (rectangle.left + 1000) / 2000.0; + double oldTop = (rectangle.top + 1000) / 2000.0; + double oldRight = (rectangle.right + 1000) / 2000.0; + double oldBottom = (rectangle.bottom + 1000) / 2000.0; + int left = toIntConstrained( mActiveArray.width() * oldLeft + mActiveArray.left, + 0, mActiveArray.width() - 1); + int top = toIntConstrained( mActiveArray.height() * oldTop + mActiveArray.top, + 0, mActiveArray.height() - 1); + int right = toIntConstrained( mActiveArray.width() * oldRight + mActiveArray.left, + 0, mActiveArray.width() - 1); + int bottom = toIntConstrained( mActiveArray.height() * oldBottom + mActiveArray.top, + 0, mActiveArray.height() - 1); + transformed[index] = new MeteringRectangle(left, top, right - left, bottom - top, + source.weight); + } + } + return transformed; + } + + private int toIntConstrained(double original, int min, int max) { + original = Math.max(original, min); + original = Math.min(original, max); + return (int) original; + } + + private void updateRequestFlashMode() { + Integer aeMode = null; + Integer flashMode = null; + if (mCurrentFlashMode != null) { + switch (mCurrentFlashMode) { + case AUTO: { + aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH; + break; + } + case OFF: { + aeMode = CONTROL_AE_MODE_ON; + flashMode = FLASH_MODE_OFF; + break; + } + case ON: { + aeMode = CONTROL_AE_MODE_ON_ALWAYS_FLASH; + flashMode = FLASH_MODE_SINGLE; + break; + } + case TORCH: { + flashMode = FLASH_MODE_TORCH; + break; + } + case RED_EYE: { + aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE; + break; + } + default: { + Log.w(TAG, "Unable to convert to API 2 flash mode: " + mCurrentFlashMode); + break; + } + } + } + mRequestSettings.set(CONTROL_AE_MODE, aeMode); + mRequestSettings.set(FLASH_MODE, flashMode); + } + + private void updateRequestFocusMode() { + Integer mode = null; + if (mCurrentFocusMode != null) { + switch (mCurrentFocusMode) { + case AUTO: { + mode = CONTROL_AF_MODE_AUTO; + break; + } + case CONTINUOUS_PICTURE: { + mode = CONTROL_AF_MODE_CONTINUOUS_PICTURE; + break; + } + case CONTINUOUS_VIDEO: { + mode = CONTROL_AF_MODE_CONTINUOUS_VIDEO; + break; + } + case EXTENDED_DOF: { + mode = CONTROL_AF_MODE_EDOF; + break; + } + case FIXED: { + mode = CONTROL_AF_MODE_OFF; + break; + } + // TODO: We cannot support INFINITY + case MACRO: { + mode = CONTROL_AF_MODE_MACRO; + break; + } + default: { + Log.w(TAG, "Unable to convert to API 2 focus mode: " + mCurrentFocusMode); + break; + } + } + } + mRequestSettings.set(CONTROL_AF_MODE, mode); + } + + private void updateRequestSceneMode() { + Integer mode = null; + if (mCurrentSceneMode != null) { + switch (mCurrentSceneMode) { + case AUTO: { + mode = CONTROL_SCENE_MODE_DISABLED; + break; + } + case ACTION: { + mode = CONTROL_SCENE_MODE_ACTION; + break; + } + case BARCODE: { + mode = CONTROL_SCENE_MODE_BARCODE; + break; + } + case BEACH: { + mode = CONTROL_SCENE_MODE_BEACH; + break; + } + case CANDLELIGHT: { + mode = CONTROL_SCENE_MODE_CANDLELIGHT; + break; + } + case FIREWORKS: { + mode = CONTROL_SCENE_MODE_FIREWORKS; + break; + } + // TODO: We cannot support HDR + case LANDSCAPE: { + mode = CONTROL_SCENE_MODE_LANDSCAPE; + break; + } + case NIGHT: { + mode = CONTROL_SCENE_MODE_NIGHT; + break; + } + // TODO: We cannot support NIGHT_PORTRAIT + case PARTY: { + mode = CONTROL_SCENE_MODE_PARTY; + break; + } + case PORTRAIT: { + mode = CONTROL_SCENE_MODE_PORTRAIT; + break; + } + case SNOW: { + mode = CONTROL_SCENE_MODE_SNOW; + break; + } + case SPORTS: { + mode = CONTROL_SCENE_MODE_SPORTS; + break; + } + case STEADYPHOTO: { + mode = CONTROL_SCENE_MODE_STEADYPHOTO; + break; + } + case SUNSET: { + mode = CONTROL_SCENE_MODE_SUNSET; + break; + } + case THEATRE: { + mode = CONTROL_SCENE_MODE_THEATRE; + break; + } + default: { + Log.w(TAG, "Unable to convert to API 2 scene mode: " + mCurrentSceneMode); + break; + } + } + } + mRequestSettings.set(CONTROL_SCENE_MODE, mode); + } + + private void updateRequestWhiteBalance() { + Integer mode = null; + if (mWhiteBalance != null) { + switch (mWhiteBalance) { + case AUTO: { + mode = CONTROL_AWB_MODE_AUTO; + break; + } + case CLOUDY_DAYLIGHT: { + mode = CONTROL_AWB_MODE_CLOUDY_DAYLIGHT; + break; + } + case DAYLIGHT: { + mode = CONTROL_AWB_MODE_DAYLIGHT; + break; + } + case FLUORESCENT: { + mode = CONTROL_AWB_MODE_FLUORESCENT; + break; + } + case INCANDESCENT: { + mode = CONTROL_AWB_MODE_INCANDESCENT; + break; + } + case SHADE: { + mode = CONTROL_AWB_MODE_SHADE; + break; + } + case TWILIGHT: { + mode = CONTROL_AWB_MODE_TWILIGHT; + break; + } + case WARM_FLUORESCENT: { + mode = CONTROL_AWB_MODE_WARM_FLUORESCENT; + break; + } + default: { + Log.w(TAG, "Unable to convert to API 2 white balance: " + mWhiteBalance); + break; + } + } + } + mRequestSettings.set(CONTROL_AWB_MODE, mode); + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java new file mode 100644 index 0000000..c26a1a3 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java @@ -0,0 +1,1255 @@ +/* + * Copyright (C) 2013 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.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.AutoFocusCallback; +import android.hardware.Camera.AutoFocusMoveCallback; +import android.hardware.Camera.ErrorCallback; +import android.hardware.Camera.FaceDetectionListener; +import android.hardware.Camera.OnZoomChangeListener; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.PictureCallback; +import android.hardware.Camera.PreviewCallback; +import android.hardware.Camera.ShutterCallback; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.view.SurfaceHolder; + +import com.android.ex.camera2.portability.debug.Log; + +import java.io.IOException; +import java.util.StringTokenizer; + +/** + * A class to implement {@link CameraAgent} of the Android camera framework. + */ +class AndroidCameraAgentImpl extends CameraAgent { + private static final Log.Tag TAG = new Log.Tag("AndCamAgntImp"); + + private CameraDeviceInfo.Characteristics mCharacteristics; + private AndroidCameraCapabilities mCapabilities; + + private final CameraHandler mCameraHandler; + private final HandlerThread mCameraHandlerThread; + private final CameraStateHolder mCameraState; + private final DispatchThread mDispatchThread; + + private Handler mCameraExceptionCallbackHandler; + private CameraExceptionCallback mCameraExceptionCallback = + new CameraExceptionCallback() { + @Override + public void onCameraException(RuntimeException e) { + throw e; + } + }; + + AndroidCameraAgentImpl() { + mCameraHandlerThread = new HandlerThread("Camera Handler Thread"); + mCameraHandlerThread.start(); + mCameraHandler = new CameraHandler(mCameraHandlerThread.getLooper()); + mCameraExceptionCallbackHandler = mCameraHandler; + mCameraState = new AndroidCameraStateHolder(); + mDispatchThread = new DispatchThread(mCameraHandler, mCameraHandlerThread); + mDispatchThread.start(); + } + + @Override + public void setCameraDefaultExceptionCallback(CameraExceptionCallback callback, + Handler handler) { + synchronized (mCameraExceptionCallback) { + mCameraExceptionCallback = callback; + mCameraExceptionCallbackHandler = handler; + } + } + + @Override + public void recycle() { + closeCamera(null, true); + mDispatchThread.end(); + } + + @Override + public CameraDeviceInfo getCameraDeviceInfo() { + return AndroidCameraDeviceInfo.create(); + } + + @Override + protected Handler getCameraHandler() { + return mCameraHandler; + } + + @Override + protected DispatchThread getDispatchThread() { + return mDispatchThread; + } + + private static class AndroidCameraDeviceInfo implements CameraDeviceInfo { + private final Camera.CameraInfo[] mCameraInfos; + private final int mNumberOfCameras; + private final int mFirstBackCameraId; + private final int mFirstFrontCameraId; + + private AndroidCameraDeviceInfo(Camera.CameraInfo[] info, int numberOfCameras, + int firstBackCameraId, int firstFrontCameraId) { + + mCameraInfos = info; + mNumberOfCameras = numberOfCameras; + mFirstBackCameraId = firstBackCameraId; + mFirstFrontCameraId = firstFrontCameraId; + } + + public static AndroidCameraDeviceInfo create() { + int numberOfCameras; + Camera.CameraInfo[] cameraInfos; + try { + numberOfCameras = Camera.getNumberOfCameras(); + cameraInfos = new Camera.CameraInfo[numberOfCameras]; + for (int i = 0; i < numberOfCameras; i++) { + cameraInfos[i] = new Camera.CameraInfo(); + Camera.getCameraInfo(i, cameraInfos[i]); + } + } catch (RuntimeException ex) { + Log.e(TAG, "Exception while creating CameraDeviceInfo", ex); + return null; + } + + int firstFront = NO_DEVICE; + int firstBack = NO_DEVICE; + // Get the first (smallest) back and first front camera id. + for (int i = numberOfCameras - 1; i >= 0; i--) { + if (cameraInfos[i].facing == Camera.CameraInfo.CAMERA_FACING_BACK) { + firstBack = i; + } else { + if (cameraInfos[i].facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + firstFront = i; + } + } + } + + return new AndroidCameraDeviceInfo(cameraInfos, numberOfCameras, firstBack, firstFront); + } + + @Override + public Characteristics getCharacteristics(int cameraId) { + Camera.CameraInfo info = mCameraInfos[cameraId]; + if (info != null) { + return new AndroidCharacteristics(info); + } else { + return null; + } + } + + @Override + public int getNumberOfCameras() { + return mNumberOfCameras; + } + + @Override + public int getFirstBackCameraId() { + return mFirstBackCameraId; + } + + @Override + public int getFirstFrontCameraId() { + return mFirstFrontCameraId; + } + + private static class AndroidCharacteristics extends Characteristics { + private Camera.CameraInfo mCameraInfo; + + AndroidCharacteristics(Camera.CameraInfo cameraInfo) { + mCameraInfo = cameraInfo; + } + + @Override + public boolean isFacingBack() { + return mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK; + } + + @Override + public boolean isFacingFront() { + return mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT; + } + + @Override + public int getSensorOrientation() { + return mCameraInfo.orientation; + } + + @Override + public boolean canDisableShutterSound() { + return mCameraInfo.canDisableShutterSound; + } + } + } + + private static class ParametersCache { + private Parameters mParameters; + private Camera mCamera; + + public ParametersCache(Camera camera) { + mCamera = camera; + } + + public synchronized void invalidate() { + mParameters = null; + } + + /** + * Access parameters from the cache. If cache is empty, block by + * retrieving parameters directly from Camera, but if cache is present, + * returns immediately. + */ + public synchronized Parameters getBlocking() { + if (mParameters == null) { + mParameters = mCamera.getParameters(); + } + return mParameters; + } + } + + /** + * The handler on which the actual camera operations happen. + */ + private class CameraHandler extends HistoryHandler { + + private Camera mCamera; + private int mCameraId; + private ParametersCache mParameterCache; + + private class CaptureCallbacks { + public final ShutterCallback mShutter; + public final PictureCallback mRaw; + public final PictureCallback mPostView; + public final PictureCallback mJpeg; + + CaptureCallbacks(ShutterCallback shutter, PictureCallback raw, PictureCallback postView, + PictureCallback jpeg) { + mShutter = shutter; + mRaw = raw; + mPostView = postView; + mJpeg = jpeg; + } + } + + CameraHandler(Looper looper) { + super(looper); + } + + private void startFaceDetection() { + mCamera.startFaceDetection(); + } + + private void stopFaceDetection() { + mCamera.stopFaceDetection(); + } + + private void setFaceDetectionListener(FaceDetectionListener listener) { + mCamera.setFaceDetectionListener(listener); + } + + private void setPreviewTexture(Object surfaceTexture) { + try { + mCamera.setPreviewTexture((SurfaceTexture) surfaceTexture); + } catch (IOException e) { + Log.e(TAG, "Could not set preview texture", e); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void enableShutterSound(boolean enable) { + mCamera.enableShutterSound(enable); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void setAutoFocusMoveCallback( + android.hardware.Camera camera, Object cb) { + try { + camera.setAutoFocusMoveCallback((AutoFocusMoveCallback) cb); + } catch (RuntimeException ex) { + Log.w(TAG, ex.getMessage()); + } + } + + private void capture(final CaptureCallbacks cb) { + try { + mCamera.takePicture(cb.mShutter, cb.mRaw, cb.mPostView, cb.mJpeg); + } catch (RuntimeException e) { + // TODO: output camera state and focus state for debugging. + Log.e(TAG, "take picture failed."); + throw e; + } + } + + public void requestTakePicture( + final ShutterCallback shutter, + final PictureCallback raw, + final PictureCallback postView, + final PictureCallback jpeg) { + final CaptureCallbacks callbacks = new CaptureCallbacks(shutter, raw, postView, jpeg); + obtainMessage(CameraActions.CAPTURE_PHOTO, callbacks).sendToTarget(); + } + + /** + * This method does not deal with the API level check. Everyone should + * check first for supported operations before sending message to this handler. + */ + @Override + public void handleMessage(final Message msg) { + super.handleMessage(msg); + try { + switch (msg.what) { + case CameraActions.OPEN_CAMERA: { + final CameraOpenCallback openCallback = (CameraOpenCallback) msg.obj; + final int cameraId = msg.arg1; + if (mCameraState.getState() != AndroidCameraStateHolder.CAMERA_UNOPENED) { + openCallback.onDeviceOpenedAlready(cameraId, generateHistoryString(cameraId)); + break; + } + + Log.i(TAG, "Opening camera " + cameraId + " with camera1 API"); + mCamera = android.hardware.Camera.open(cameraId); + if (mCamera != null) { + mCameraId = cameraId; + mParameterCache = new ParametersCache(mCamera); + + mCharacteristics = + AndroidCameraDeviceInfo.create().getCharacteristics(cameraId); + mCapabilities = new AndroidCameraCapabilities( + mParameterCache.getBlocking()); + + mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE); + if (openCallback != null) { + openCallback.onCameraOpened( + new AndroidCameraProxyImpl(cameraId, mCamera, + mCharacteristics, mCapabilities)); + } + } else { + if (openCallback != null) { + openCallback.onDeviceOpenFailure(cameraId, generateHistoryString(cameraId)); + } + } + break; + } + + case CameraActions.RELEASE: { + if (mCamera != null) { + mCamera.release(); + mCameraState.setState(AndroidCameraStateHolder.CAMERA_UNOPENED); + mCamera = null; + } else { + Log.w(TAG, "Releasing camera without any camera opened."); + } + break; + } + + case CameraActions.RECONNECT: { + final CameraOpenCallbackForward cbForward = + (CameraOpenCallbackForward) msg.obj; + final int cameraId = msg.arg1; + try { + mCamera.reconnect(); + } catch (IOException ex) { + if (cbForward != null) { + cbForward.onReconnectionFailure(AndroidCameraAgentImpl.this, + generateHistoryString(mCameraId)); + } + break; + } + + mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE); + if (cbForward != null) { + cbForward.onCameraOpened( + new AndroidCameraProxyImpl(cameraId, mCamera, mCharacteristics, + mCapabilities)); + } + break; + } + + case CameraActions.UNLOCK: { + mCamera.unlock(); + mCameraState.setState(AndroidCameraStateHolder.CAMERA_UNLOCKED); + break; + } + + case CameraActions.LOCK: { + mCamera.lock(); + mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE); + break; + } + + case CameraActions.SET_PREVIEW_TEXTURE_ASYNC: { + setPreviewTexture(msg.obj); + break; + } + + case CameraActions.SET_PREVIEW_DISPLAY_ASYNC: { + try { + mCamera.setPreviewDisplay((SurfaceHolder) msg.obj); + } catch (IOException e) { + throw new RuntimeException(e); + } + break; + } + + case CameraActions.START_PREVIEW_ASYNC: { + final CameraStartPreviewCallbackForward cbForward = + (CameraStartPreviewCallbackForward) msg.obj; + mCamera.startPreview(); + if (cbForward != null) { + cbForward.onPreviewStarted(); + } + break; + } + + case CameraActions.STOP_PREVIEW: { + mCamera.stopPreview(); + break; + } + + case CameraActions.SET_PREVIEW_CALLBACK_WITH_BUFFER: { + mCamera.setPreviewCallbackWithBuffer((PreviewCallback) msg.obj); + break; + } + + case CameraActions.SET_ONE_SHOT_PREVIEW_CALLBACK: { + mCamera.setOneShotPreviewCallback((PreviewCallback) msg.obj); + break; + } + + case CameraActions.ADD_CALLBACK_BUFFER: { + mCamera.addCallbackBuffer((byte[]) msg.obj); + break; + } + + case CameraActions.AUTO_FOCUS: { + mCameraState.setState(AndroidCameraStateHolder.CAMERA_FOCUSING); + mCamera.autoFocus((AutoFocusCallback) msg.obj); + break; + } + + case CameraActions.CANCEL_AUTO_FOCUS: { + mCamera.cancelAutoFocus(); + mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE); + break; + } + + case CameraActions.SET_AUTO_FOCUS_MOVE_CALLBACK: { + setAutoFocusMoveCallback(mCamera, msg.obj); + break; + } + + case CameraActions.SET_DISPLAY_ORIENTATION: { + // Update preview orientation + mCamera.setDisplayOrientation( + mCharacteristics.getPreviewOrientation(msg.arg1)); + // Only set the JPEG capture orientation if requested to do so; otherwise, + // capture in the sensor's physical orientation + Parameters parameters = mParameterCache.getBlocking(); + parameters.setRotation( + msg.arg2 > 0 ? mCharacteristics.getJpegOrientation(msg.arg1) : 0); + mCamera.setParameters(parameters); + break; + } + + case CameraActions.SET_ZOOM_CHANGE_LISTENER: { + mCamera.setZoomChangeListener((OnZoomChangeListener) msg.obj); + break; + } + + case CameraActions.SET_FACE_DETECTION_LISTENER: { + setFaceDetectionListener((FaceDetectionListener) msg.obj); + break; + } + + case CameraActions.START_FACE_DETECTION: { + startFaceDetection(); + break; + } + + case CameraActions.STOP_FACE_DETECTION: { + stopFaceDetection(); + break; + } + + case CameraActions.SET_ERROR_CALLBACK: { + mCamera.setErrorCallback((ErrorCallback) msg.obj); + break; + } + + case CameraActions.APPLY_SETTINGS: { + Parameters parameters = mParameterCache.getBlocking(); + CameraSettings settings = (CameraSettings) msg.obj; + applySettingsToParameters(settings, parameters); + mCamera.setParameters(parameters); + mParameterCache.invalidate(); + break; + } + + case CameraActions.SET_PARAMETERS: { + Parameters parameters = mParameterCache.getBlocking(); + parameters.unflatten((String) msg.obj); + mCamera.setParameters(parameters); + mParameterCache.invalidate(); + break; + } + + case CameraActions.GET_PARAMETERS: { + Parameters[] parametersHolder = (Parameters[]) msg.obj; + Parameters parameters = mParameterCache.getBlocking(); + parametersHolder[0] = parameters; + break; + } + + case CameraActions.SET_PREVIEW_CALLBACK: { + mCamera.setPreviewCallback((PreviewCallback) msg.obj); + break; + } + + case CameraActions.ENABLE_SHUTTER_SOUND: { + enableShutterSound((msg.arg1 == 1) ? true : false); + break; + } + + case CameraActions.REFRESH_PARAMETERS: { + mParameterCache.invalidate();; + break; + } + + case CameraActions.CAPTURE_PHOTO: { + mCameraState.setState(AndroidCameraStateHolder.CAMERA_CAPTURING); + capture((CaptureCallbacks) msg.obj); + break; + } + + default: { + throw new RuntimeException("Invalid CameraProxy message=" + msg.what); + } + } + } catch (final RuntimeException e) { + if (msg.what != CameraActions.RELEASE && mCamera != null) { + try { + mCamera.release(); + mCameraState.setState(AndroidCameraStateHolder.CAMERA_UNOPENED); + } catch (Exception ex) { + Log.e(TAG, "Fail to release the camera."); + } + mCamera = null; + } else { + if (mCamera == null) { + if (msg.what == CameraActions.OPEN_CAMERA) { + final int cameraId = msg.arg1; + if (msg.obj != null) { + ((CameraOpenCallback) msg.obj).onDeviceOpenFailure( + msg.arg1, generateHistoryString(cameraId)); + } + } else { + Log.w(TAG, "Cannot handle message " + msg.what + ", mCamera is null."); + } + return; + } + } + synchronized (mCameraExceptionCallback) { + mCameraExceptionCallbackHandler.post(new Runnable() { + @Override + public void run() { + mCameraExceptionCallback.onCameraException(e); + } + }); + } + } + } + + private void applySettingsToParameters(final CameraSettings settings, + final Parameters parameters) { + final CameraCapabilities.Stringifier stringifier = mCapabilities.getStringifier(); + Size photoSize = settings.getCurrentPhotoSize(); + parameters.setPictureSize(photoSize.width(), photoSize.height()); + Size previewSize = settings.getCurrentPreviewSize(); + parameters.setPreviewSize(previewSize.width(), previewSize.height()); + if (settings.getPreviewFrameRate() == -1) { + parameters.setPreviewFpsRange(settings.getPreviewFpsRangeMin(), + settings.getPreviewFpsRangeMax()); + } else { + parameters.setPreviewFrameRate(settings.getPreviewFrameRate()); + } + parameters.setPreviewFormat(settings.getCurrentPreviewFormat()); + parameters.setJpegQuality(settings.getPhotoJpegCompressionQuality()); + if (mCapabilities.supports(CameraCapabilities.Feature.ZOOM)) { + // Should use settings.getCurrentZoomRatio() instead here. + parameters.setZoom(settings.getCurrentZoomIndex()); + } + parameters.setExposureCompensation(settings.getExposureCompensationIndex()); + if (mCapabilities.supports(CameraCapabilities.Feature.AUTO_EXPOSURE_LOCK)) { + parameters.setAutoExposureLock(settings.isAutoExposureLocked()); + } + parameters.setFocusMode(stringifier.stringify(settings.getCurrentFocusMode())); + if (mCapabilities.supports(CameraCapabilities.Feature.AUTO_WHITE_BALANCE_LOCK)) { + parameters.setAutoWhiteBalanceLock(settings.isAutoWhiteBalanceLocked()); + } + if (mCapabilities.supports(CameraCapabilities.Feature.FOCUS_AREA)) { + if (settings.getFocusAreas().size() != 0) { + parameters.setFocusAreas(settings.getFocusAreas()); + } + } + if (mCapabilities.supports(CameraCapabilities.Feature.METERING_AREA)) { + if (settings.getMeteringAreas().size() != 0) { + parameters.setMeteringAreas(settings.getMeteringAreas()); + } + } + if (settings.getCurrentFlashMode() != CameraCapabilities.FlashMode.NO_FLASH) { + parameters.setFlashMode(stringifier.stringify(settings.getCurrentFlashMode())); + } + if (settings.getCurrentSceneMode() != CameraCapabilities.SceneMode.NO_SCENE_MODE) { + if (settings.getCurrentSceneMode() != null) { + parameters + .setSceneMode(stringifier.stringify(settings.getCurrentSceneMode())); + } + } + parameters.setRecordingHint(settings.isRecordingHintEnabled()); + Size jpegThumbSize = settings.getExifThumbnailSize(); + parameters.setJpegThumbnailSize(jpegThumbSize.width(), jpegThumbSize.height()); + parameters.setPictureFormat(settings.getCurrentPhotoFormat()); + + CameraSettings.GpsData gpsData = settings.getGpsData(); + if (gpsData == null) { + parameters.removeGpsData(); + } else { + parameters.setGpsTimestamp(gpsData.timeStamp); + if (gpsData.processingMethod != null) { + // It's a hack since we always use GPS time stamp but does + // not use other fields sometimes. Setting processing + // method to null means the other fields should not be used. + parameters.setGpsAltitude(gpsData.altitude); + parameters.setGpsLatitude(gpsData.latitude); + parameters.setGpsLongitude(gpsData.longitude); + parameters.setGpsProcessingMethod(gpsData.processingMethod); + } + } + + } + } + + /** + * A class which implements {@link CameraAgent.CameraProxy} and + * camera handler thread. + */ + private class AndroidCameraProxyImpl extends CameraAgent.CameraProxy { + private final int mCameraId; + /* TODO: remove this Camera instance. */ + private final Camera mCamera; + private final CameraDeviceInfo.Characteristics mCharacteristics; + private final AndroidCameraCapabilities mCapabilities; + + private AndroidCameraProxyImpl(int cameraId, Camera camera, + CameraDeviceInfo.Characteristics characteristics, + AndroidCameraCapabilities capabilities) { + mCamera = camera; + mCameraId = cameraId; + mCharacteristics = characteristics; + mCapabilities = capabilities; + } + + @Deprecated + @Override + public android.hardware.Camera getCamera() { + return mCamera; + } + + @Override + public int getCameraId() { + return mCameraId; + } + + @Override + public CameraDeviceInfo.Characteristics getCharacteristics() { + return mCharacteristics; + } + + @Override + public CameraCapabilities getCapabilities() { + return new AndroidCameraCapabilities(mCapabilities); + } + + @Override + public void setPreviewDataCallback( + final Handler handler, final CameraPreviewDataCallback cb) { + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraHandler.obtainMessage(CameraActions.SET_PREVIEW_CALLBACK, + PreviewCallbackForward.getNewInstance( + handler, AndroidCameraProxyImpl.this, cb)) + .sendToTarget(); + } + }); + } + + @Override + public void setOneShotPreviewCallback(final Handler handler, + final CameraPreviewDataCallback cb) { + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraHandler.obtainMessage(CameraActions.SET_ONE_SHOT_PREVIEW_CALLBACK, + PreviewCallbackForward + .getNewInstance(handler, AndroidCameraProxyImpl.this, cb)) + .sendToTarget(); + } + }); + } + + @Override + public void setPreviewDataCallbackWithBuffer( + final Handler handler, final CameraPreviewDataCallback cb) { + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraHandler.obtainMessage(CameraActions.SET_PREVIEW_CALLBACK_WITH_BUFFER, + PreviewCallbackForward + .getNewInstance(handler, AndroidCameraProxyImpl.this, cb)) + .sendToTarget(); + } + }); + } + + @Override + public void autoFocus(final Handler handler, final CameraAFCallback cb) { + final AutoFocusCallback afCallback = new AutoFocusCallback() { + @Override + public void onAutoFocus(final boolean b, Camera camera) { + if (mCameraState.getState() != AndroidCameraStateHolder.CAMERA_FOCUSING) { + Log.w(TAG, "onAutoFocus callback returning when not focusing"); + } else { + mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE); + } + handler.post(new Runnable() { + @Override + public void run() { + cb.onAutoFocus(b, AndroidCameraProxyImpl.this); + } + }); + } + }; + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraState.waitForStates(AndroidCameraStateHolder.CAMERA_IDLE); + mCameraHandler.obtainMessage(CameraActions.AUTO_FOCUS, afCallback) + .sendToTarget(); + } + }); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Override + public void setAutoFocusMoveCallback( + final Handler handler, final CameraAFMoveCallback cb) { + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraHandler.obtainMessage(CameraActions.SET_AUTO_FOCUS_MOVE_CALLBACK, + AFMoveCallbackForward.getNewInstance( + handler, AndroidCameraProxyImpl.this, cb)) + .sendToTarget(); + } + }); + } + + @Override + public void takePicture( + final Handler handler, final CameraShutterCallback shutter, + final CameraPictureCallback raw, final CameraPictureCallback post, + final CameraPictureCallback jpeg) { + final PictureCallback jpegCallback = new PictureCallback() { + @Override + public void onPictureTaken(final byte[] data, Camera camera) { + if (mCameraState.getState() != AndroidCameraStateHolder.CAMERA_CAPTURING) { + Log.w(TAG, "picture callback returning when not capturing"); + } else { + mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE); + } + handler.post(new Runnable() { + @Override + public void run() { + jpeg.onPictureTaken(data, AndroidCameraProxyImpl.this); + } + }); + } + }; + + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraState.waitForStates(AndroidCameraStateHolder.CAMERA_IDLE | + AndroidCameraStateHolder.CAMERA_UNLOCKED); + mCameraHandler.requestTakePicture(ShutterCallbackForward + .getNewInstance(handler, AndroidCameraProxyImpl.this, shutter), + PictureCallbackForward + .getNewInstance(handler, AndroidCameraProxyImpl.this, raw), + PictureCallbackForward + .getNewInstance(handler, AndroidCameraProxyImpl.this, post), + jpegCallback + ); + } + }); + } + + @Override + public void setZoomChangeListener(final OnZoomChangeListener listener) { + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraHandler.obtainMessage(CameraActions.SET_ZOOM_CHANGE_LISTENER, listener) + .sendToTarget(); + } + }); + } + + @Override + public void setFaceDetectionCallback(final Handler handler, + final CameraFaceDetectionCallback cb) { + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraHandler.obtainMessage(CameraActions.SET_FACE_DETECTION_LISTENER, + FaceDetectionCallbackForward + .getNewInstance(handler, AndroidCameraProxyImpl.this, cb)) + .sendToTarget(); + } + }); + } + + @Override + public void setErrorCallback(final Handler handler, final CameraErrorCallback cb) { + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraHandler.obtainMessage(CameraActions.SET_ERROR_CALLBACK, + ErrorCallbackForward.getNewInstance( + handler, AndroidCameraProxyImpl.this, cb)) + .sendToTarget(); + } + }); + } + + @Deprecated + @Override + public void setParameters(final Parameters params) { + if (params == null) { + Log.v(TAG, "null parameters in setParameters()"); + return; + } + final String flattenedParameters = params.flatten(); + mDispatchThread.runJob(new Runnable() { + @Override + public void run() { + mCameraState.waitForStates(AndroidCameraStateHolder.CAMERA_IDLE | + AndroidCameraStateHolder.CAMERA_UNLOCKED); + mCameraHandler.obtainMessage(CameraActions.SET_PARAMETERS, flattenedParameters) + .sendToTarget(); + } + }); + } + + @Deprecated + @Override + public Parameters getParameters() { + final WaitDoneBundle bundle = new WaitDoneBundle(); + final Parameters[] parametersHolder = new Parameters[1]; + mDispatchThread.runJobSync(new Runnable() { + @Override + public void run() { + Message getParametersMessage = mCameraHandler.obtainMessage( + CameraActions.GET_PARAMETERS, parametersHolder); + mCameraHandler.sendMessage(getParametersMessage); + mCameraHandler.post(bundle.mUnlockRunnable); + } + }, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "get parameters"); + return parametersHolder[0]; + } + + @Override + public CameraSettings getSettings() { + return new AndroidCameraSettings(mCapabilities, getParameters()); + } + + @Override + public boolean applySettings(CameraSettings settings) { + return applySettingsHelper(settings, AndroidCameraStateHolder.CAMERA_IDLE | + AndroidCameraStateHolder.CAMERA_UNLOCKED); + } + + @Override + public String dumpDeviceSettings() { + Parameters parameters = getParameters(); + if (parameters != null) { + String flattened = getParameters().flatten(); + StringTokenizer tokenizer = new StringTokenizer(flattened, ";"); + String dumpedSettings = new String(); + while (tokenizer.hasMoreElements()) { + dumpedSettings += tokenizer.nextToken() + '\n'; + } + + return dumpedSettings; + } else { + return "[no parameters retrieved]"; + } + } + + @Override + public Handler getCameraHandler() { + return AndroidCameraAgentImpl.this.getCameraHandler(); + } + + @Override + public DispatchThread getDispatchThread() { + return AndroidCameraAgentImpl.this.getDispatchThread(); + } + + @Override + public CameraStateHolder getCameraState() { + return mCameraState; + } + } + + private static class AndroidCameraStateHolder extends CameraStateHolder { + /* Camera states */ + // These states are defined bitwise so we can easily to specify a set of + // states together. + public static final int CAMERA_UNOPENED = 1; + public static final int CAMERA_IDLE = 1 << 1; + public static final int CAMERA_UNLOCKED = 1 << 2; + public static final int CAMERA_CAPTURING = 1 << 3; + public static final int CAMERA_FOCUSING = 1 << 4; + + public AndroidCameraStateHolder() { + this(CAMERA_UNOPENED); + } + + public AndroidCameraStateHolder(int state) { + super(state); + } + } + + /** + * A helper class to forward AutoFocusCallback to another thread. + */ + private static class AFCallbackForward implements AutoFocusCallback { + private final Handler mHandler; + private final CameraProxy mCamera; + private final CameraAFCallback mCallback; + + /** + * Returns a new instance of {@link AFCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link AFCallbackForward}, + * or null if any parameter is null. + */ + public static AFCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraAFCallback cb) { + if (handler == null || camera == null || cb == null) { + return null; + } + return new AFCallbackForward(handler, camera, cb); + } + + private AFCallbackForward( + Handler h, CameraProxy camera, CameraAFCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onAutoFocus(final boolean b, Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onAutoFocus(b, mCamera); + } + }); + } + } + + /** + * A helper class to forward ErrorCallback to another thread. + */ + private static class ErrorCallbackForward implements Camera.ErrorCallback { + private final Handler mHandler; + private final CameraProxy mCamera; + private final CameraErrorCallback mCallback; + + /** + * Returns a new instance of {@link AFCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link AFCallbackForward}, + * or null if any parameter is null. + */ + public static ErrorCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraErrorCallback cb) { + if (handler == null || camera == null || cb == null) { + return null; + } + return new ErrorCallbackForward(handler, camera, cb); + } + + private ErrorCallbackForward( + Handler h, CameraProxy camera, CameraErrorCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onError(final int error, Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onError(error, mCamera); + } + }); + } + } + + /** A helper class to forward AutoFocusMoveCallback to another thread. */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private static class AFMoveCallbackForward implements AutoFocusMoveCallback { + private final Handler mHandler; + private final CameraAFMoveCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link AFMoveCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link AFMoveCallbackForward}, + * or null if any parameter is null. + */ + public static AFMoveCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraAFMoveCallback cb) { + if (handler == null || camera == null || cb == null) { + return null; + } + return new AFMoveCallbackForward(handler, camera, cb); + } + + private AFMoveCallbackForward( + Handler h, CameraProxy camera, CameraAFMoveCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onAutoFocusMoving( + final boolean moving, android.hardware.Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onAutoFocusMoving(moving, mCamera); + } + }); + } + } + + /** + * A helper class to forward ShutterCallback to to another thread. + */ + private static class ShutterCallbackForward implements ShutterCallback { + private final Handler mHandler; + private final CameraShutterCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link ShutterCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link ShutterCallbackForward}, + * or null if any parameter is null. + */ + public static ShutterCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraShutterCallback cb) { + if (handler == null || camera == null || cb == null) { + return null; + } + return new ShutterCallbackForward(handler, camera, cb); + } + + private ShutterCallbackForward( + Handler h, CameraProxy camera, CameraShutterCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onShutter() { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onShutter(mCamera); + } + }); + } + } + + /** + * A helper class to forward PictureCallback to another thread. + */ + private static class PictureCallbackForward implements PictureCallback { + private final Handler mHandler; + private final CameraPictureCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link PictureCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link PictureCallbackForward}, + * or null if any parameters is null. + */ + public static PictureCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraPictureCallback cb) { + if (handler == null || camera == null || cb == null) { + return null; + } + return new PictureCallbackForward(handler, camera, cb); + } + + private PictureCallbackForward( + Handler h, CameraProxy camera, CameraPictureCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onPictureTaken( + final byte[] data, android.hardware.Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onPictureTaken(data, mCamera); + } + }); + } + } + + /** + * A helper class to forward PreviewCallback to another thread. + */ + private static class PreviewCallbackForward implements PreviewCallback { + private final Handler mHandler; + private final CameraPreviewDataCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link PreviewCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link PreviewCallbackForward}, + * or null if any parameters is null. + */ + public static PreviewCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraPreviewDataCallback cb) { + if (handler == null || camera == null || cb == null) { + return null; + } + return new PreviewCallbackForward(handler, camera, cb); + } + + private PreviewCallbackForward( + Handler h, CameraProxy camera, CameraPreviewDataCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onPreviewFrame( + final byte[] data, android.hardware.Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onPreviewFrame(data, mCamera); + } + }); + } + } + + private static class FaceDetectionCallbackForward implements FaceDetectionListener { + private final Handler mHandler; + private final CameraFaceDetectionCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link FaceDetectionCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link FaceDetectionCallbackForward}, + * or null if any parameter is null. + */ + public static FaceDetectionCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraFaceDetectionCallback cb) { + if (handler == null || camera == null || cb == null) { + return null; + } + return new FaceDetectionCallbackForward(handler, camera, cb); + } + + private FaceDetectionCallbackForward( + Handler h, CameraProxy camera, CameraFaceDetectionCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onFaceDetection( + final Camera.Face[] faces, Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onFaceDetection(faces, mCamera); + } + }); + } + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraCapabilities.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraCapabilities.java new file mode 100644 index 0000000..acff9c6 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraCapabilities.java @@ -0,0 +1,255 @@ +/* + * 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.hardware.Camera; + +import com.android.ex.camera2.portability.debug.Log; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * The subclass of {@link CameraCapabilities} for Android Camera 1 API. + */ +class AndroidCameraCapabilities extends CameraCapabilities { + + private static Log.Tag TAG = new Log.Tag("AndCamCapabs"); + + private FpsComparator mFpsComparator = new FpsComparator(); + private SizeComparator mSizeComparator = new SizeComparator(); + + AndroidCameraCapabilities(Camera.Parameters p) { + super(new Stringifier()); + mMaxExposureCompensation = p.getMaxExposureCompensation(); + mMinExposureCompensation = p.getMinExposureCompensation(); + mExposureCompensationStep = p.getExposureCompensationStep(); + mMaxNumOfFacesSupported = p.getMaxNumDetectedFaces(); + mMaxNumOfMeteringArea = p.getMaxNumMeteringAreas(); + mPreferredPreviewSizeForVideo = new Size(p.getPreferredPreviewSizeForVideo()); + mSupportedPreviewFormats.addAll(p.getSupportedPreviewFormats()); + mSupportedPhotoFormats.addAll(p.getSupportedPictureFormats()); + mMaxZoomIndex = p.getMaxZoom(); + mZoomRatioList.addAll(p.getZoomRatios()); + mMaxZoomRatio = mZoomRatioList.get(mMaxZoomIndex); + mHorizontalViewAngle = p.getHorizontalViewAngle(); + mVerticalViewAngle = p.getVerticalViewAngle(); + buildPreviewFpsRange(p); + buildPreviewSizes(p); + buildVideoSizes(p); + buildPictureSizes(p); + buildSceneModes(p); + buildFlashModes(p); + buildFocusModes(p); + buildWhiteBalances(p); + + if (p.isZoomSupported()) { + mSupportedFeatures.add(Feature.ZOOM); + } + if (p.isVideoSnapshotSupported()) { + mSupportedFeatures.add(Feature.VIDEO_SNAPSHOT); + } + if (p.isAutoExposureLockSupported()) { + mSupportedFeatures.add(Feature.AUTO_EXPOSURE_LOCK); + } + if (p.isAutoWhiteBalanceLockSupported()) { + mSupportedFeatures.add(Feature.AUTO_WHITE_BALANCE_LOCK); + } + if (supports(FocusMode.AUTO)) { + mMaxNumOfFocusAreas = p.getMaxNumFocusAreas(); + if (mMaxNumOfFocusAreas > 0) { + mSupportedFeatures.add(Feature.FOCUS_AREA); + } + } + if (mMaxNumOfMeteringArea > 0) { + mSupportedFeatures.add(Feature.METERING_AREA); + } + } + + AndroidCameraCapabilities(AndroidCameraCapabilities src) { + super(src); + } + + private void buildPreviewFpsRange(Camera.Parameters p) { + List<int[]> supportedPreviewFpsRange = p.getSupportedPreviewFpsRange(); + if (supportedPreviewFpsRange != null) { + mSupportedPreviewFpsRange.addAll(supportedPreviewFpsRange); + } + Collections.sort(mSupportedPreviewFpsRange, mFpsComparator); + } + + private void buildPreviewSizes(Camera.Parameters p) { + List<Camera.Size> supportedPreviewSizes = p.getSupportedPreviewSizes(); + if (supportedPreviewSizes != null) { + for (Camera.Size s : supportedPreviewSizes) { + mSupportedPreviewSizes.add(new Size(s.width, s.height)); + } + } + Collections.sort(mSupportedPreviewSizes, mSizeComparator); + } + + private void buildVideoSizes(Camera.Parameters p) { + List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes(); + if (supportedVideoSizes != null) { + for (Camera.Size s : supportedVideoSizes) { + mSupportedVideoSizes.add(new Size(s.width, s.height)); + } + } + Collections.sort(mSupportedVideoSizes, mSizeComparator); + } + + private void buildPictureSizes(Camera.Parameters p) { + List<Camera.Size> supportedPictureSizes = p.getSupportedPictureSizes(); + if (supportedPictureSizes != null) { + for (Camera.Size s : supportedPictureSizes) { + mSupportedPhotoSizes.add(new Size(s.width, s.height)); + } + } + Collections.sort(mSupportedPhotoSizes, mSizeComparator); + + } + + private void buildSceneModes(Camera.Parameters p) { + List<String> supportedSceneModes = p.getSupportedSceneModes(); + if (supportedSceneModes != null) { + for (String scene : supportedSceneModes) { + if (Camera.Parameters.SCENE_MODE_AUTO.equals(scene)) { + mSupportedSceneModes.add(SceneMode.AUTO); + } else if (Camera.Parameters.SCENE_MODE_ACTION.equals(scene)) { + mSupportedSceneModes.add(SceneMode.ACTION); + } else if (Camera.Parameters.SCENE_MODE_BARCODE.equals(scene)) { + mSupportedSceneModes.add(SceneMode.BARCODE); + } else if (Camera.Parameters.SCENE_MODE_BEACH.equals(scene)) { + mSupportedSceneModes.add(SceneMode.BEACH); + } else if (Camera.Parameters.SCENE_MODE_CANDLELIGHT.equals(scene)) { + mSupportedSceneModes.add(SceneMode.CANDLELIGHT); + } else if (Camera.Parameters.SCENE_MODE_FIREWORKS.equals(scene)) { + mSupportedSceneModes.add(SceneMode.FIREWORKS); + } else if (Camera.Parameters.SCENE_MODE_HDR.equals(scene)) { + mSupportedSceneModes.add(SceneMode.HDR); + } else if (Camera.Parameters.SCENE_MODE_LANDSCAPE.equals(scene)) { + mSupportedSceneModes.add(SceneMode.LANDSCAPE); + } else if (Camera.Parameters.SCENE_MODE_NIGHT.equals(scene)) { + mSupportedSceneModes.add(SceneMode.NIGHT); + } else if (Camera.Parameters.SCENE_MODE_NIGHT_PORTRAIT.equals(scene)) { + mSupportedSceneModes.add(SceneMode.NIGHT_PORTRAIT); + } else if (Camera.Parameters.SCENE_MODE_PARTY.equals(scene)) { + mSupportedSceneModes.add(SceneMode.PARTY); + } else if (Camera.Parameters.SCENE_MODE_PORTRAIT.equals(scene)) { + mSupportedSceneModes.add(SceneMode.PORTRAIT); + } else if (Camera.Parameters.SCENE_MODE_SNOW.equals(scene)) { + mSupportedSceneModes.add(SceneMode.SNOW); + } else if (Camera.Parameters.SCENE_MODE_SPORTS.equals(scene)) { + mSupportedSceneModes.add(SceneMode.SPORTS); + } else if (Camera.Parameters.SCENE_MODE_STEADYPHOTO.equals(scene)) { + mSupportedSceneModes.add(SceneMode.STEADYPHOTO); + } else if (Camera.Parameters.SCENE_MODE_SUNSET.equals(scene)) { + mSupportedSceneModes.add(SceneMode.SUNSET); + } else if (Camera.Parameters.SCENE_MODE_THEATRE.equals(scene)) { + mSupportedSceneModes.add(SceneMode.THEATRE); + } + } + } + } + + private void buildFlashModes(Camera.Parameters p) { + List<String> supportedFlashModes = p.getSupportedFlashModes(); + if (supportedFlashModes == null) { + // Camera 1 will return NULL if no flash mode is supported. + mSupportedFlashModes.add(FlashMode.NO_FLASH); + } else { + for (String flash : supportedFlashModes) { + if (Camera.Parameters.FLASH_MODE_AUTO.equals(flash)) { + mSupportedFlashModes.add(FlashMode.AUTO); + } else if (Camera.Parameters.FLASH_MODE_OFF.equals(flash)) { + mSupportedFlashModes.add(FlashMode.OFF); + } else if (Camera.Parameters.FLASH_MODE_ON.equals(flash)) { + mSupportedFlashModes.add(FlashMode.ON); + } else if (Camera.Parameters.FLASH_MODE_RED_EYE.equals(flash)) { + mSupportedFlashModes.add(FlashMode.RED_EYE); + } else if (Camera.Parameters.FLASH_MODE_TORCH.equals(flash)) { + mSupportedFlashModes.add(FlashMode.TORCH); + } + } + } + } + + private void buildFocusModes(Camera.Parameters p) { + List<String> supportedFocusModes = p.getSupportedFocusModes(); + if (supportedFocusModes != null) { + for (String focus : supportedFocusModes) { + if (Camera.Parameters.FOCUS_MODE_AUTO.equals(focus)) { + mSupportedFocusModes.add(FocusMode.AUTO); + } else if (Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(focus)) { + mSupportedFocusModes.add(FocusMode.CONTINUOUS_PICTURE); + } else if (Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO.equals(focus)) { + mSupportedFocusModes.add(FocusMode.CONTINUOUS_VIDEO); + } else if (Camera.Parameters.FOCUS_MODE_EDOF.equals(focus)) { + mSupportedFocusModes.add(FocusMode.EXTENDED_DOF); + } else if (Camera.Parameters.FOCUS_MODE_FIXED.equals(focus)) { + mSupportedFocusModes.add(FocusMode.FIXED); + } else if (Camera.Parameters.FOCUS_MODE_INFINITY.equals(focus)) { + mSupportedFocusModes.add(FocusMode.INFINITY); + } else if (Camera.Parameters.FOCUS_MODE_MACRO.equals(focus)) { + mSupportedFocusModes.add(FocusMode.MACRO); + } + } + } + } + + private void buildWhiteBalances(Camera.Parameters p) { + List<String> supportedWhiteBalances = p.getSupportedFocusModes(); + if (supportedWhiteBalances != null) { + for (String wb : supportedWhiteBalances) { + if (Camera.Parameters.WHITE_BALANCE_AUTO.equals(wb)) { + mSupportedWhiteBalances.add(WhiteBalance.AUTO); + } else if (Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT.equals(wb)) { + mSupportedWhiteBalances.add(WhiteBalance.CLOUDY_DAYLIGHT); + } else if (Camera.Parameters.WHITE_BALANCE_DAYLIGHT.equals(wb)) { + mSupportedWhiteBalances.add(WhiteBalance.DAYLIGHT); + } else if (Camera.Parameters.WHITE_BALANCE_FLUORESCENT.equals(wb)) { + mSupportedWhiteBalances.add(WhiteBalance.FLUORESCENT); + } else if (Camera.Parameters.WHITE_BALANCE_INCANDESCENT.equals(wb)) { + mSupportedWhiteBalances.add(WhiteBalance.INCANDESCENT); + } else if (Camera.Parameters.WHITE_BALANCE_SHADE.equals(wb)) { + mSupportedWhiteBalances.add(WhiteBalance.SHADE); + } else if (Camera.Parameters.WHITE_BALANCE_TWILIGHT.equals(wb)) { + mSupportedWhiteBalances.add(WhiteBalance.TWILIGHT); + } else if (Camera.Parameters.WHITE_BALANCE_WARM_FLUORESCENT.equals(wb)) { + mSupportedWhiteBalances.add(WhiteBalance.WARM_FLUORESCENT); + } + } + } + } + + private static class FpsComparator implements Comparator<int[]> { + @Override + public int compare(int[] fps1, int[] fps2) { + return (fps1[0] == fps2[0] ? fps1[1] - fps2[1] : fps1[0] - fps2[0]); + } + } + + private static class SizeComparator implements Comparator<Size> { + + @Override + public int compare(Size size1, Size size2) { + return (size1.width() == size2.width() ? size1.height() - size2.height() : + size1.width() - size2.width()); + } + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java new file mode 100644 index 0000000..ceab7fe --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraSettings.java @@ -0,0 +1,75 @@ +/* + * 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.hardware.Camera; + +/** + * The subclass of {@link CameraSettings} for Android Camera 1 API. + */ +public class AndroidCameraSettings extends CameraSettings { + private static final String TRUE = "true"; + private static final String RECORDING_HINT = "recording-hint"; + + public AndroidCameraSettings(CameraCapabilities capabilities, Camera.Parameters params) { + CameraCapabilities.Stringifier stringifier = capabilities.getStringifier(); + + // Preview + Camera.Size paramPreviewSize = params.getPreviewSize(); + setPreviewSize(new Size(paramPreviewSize.width, paramPreviewSize.height)); + setPreviewFrameRate(params.getPreviewFrameRate()); + int[] previewFpsRange = new int[2]; + params.getPreviewFpsRange(previewFpsRange); + setPreviewFpsRange(previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], + previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); + setPreviewFormat(params.getPreviewFormat()); + + // Capture: Focus, flash, zoom, exposure, scene mode. + if (capabilities.supports(CameraCapabilities.Feature.ZOOM)) { + setZoomRatio(params.getZoomRatios().get(params.getZoom()) / 100f); + setZoomIndex(params.getZoom()); + } else { + setZoomRatio(1.0f); + setZoomIndex(0); + } + setExposureCompensationIndex(params.getExposureCompensation()); + setFlashMode(stringifier.flashModeFromString(params.getFlashMode())); + setFocusMode(stringifier.focusModeFromString(params.getFocusMode())); + setSceneMode(stringifier.sceneModeFromString(params.getSceneMode())); + + // Video capture. + if (capabilities.supports(CameraCapabilities.Feature.VIDEO_STABILIZATION)) { + setVideoStabilization(isVideoStabilizationEnabled()); + } + setRecordingHintEnabled(TRUE.equals(params.get(RECORDING_HINT))); + + // Output: Photo size, compression quality + setPhotoJpegCompressionQuality(params.getJpegQuality()); + Camera.Size paramPictureSize = params.getPictureSize(); + setPhotoSize(new Size(paramPictureSize.width, paramPictureSize.height)); + setPhotoFormat(params.getPictureFormat()); + } + + public AndroidCameraSettings(AndroidCameraSettings other) { + super(other); + } + + @Override + public CameraSettings copy() { + return new AndroidCameraSettings(this); + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraActions.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraActions.java new file mode 100644 index 0000000..aae122b --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraActions.java @@ -0,0 +1,55 @@ +/* + * 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; + +class CameraActions { + // Camera initialization/finalization + public static final int OPEN_CAMERA = 1; + public static final int RELEASE = 2; + public static final int RECONNECT = 3; + public static final int UNLOCK = 4; + public static final int LOCK = 5; + // Preview + public static final int SET_PREVIEW_TEXTURE_ASYNC = 101; + public static final int START_PREVIEW_ASYNC = 102; + public static final int STOP_PREVIEW = 103; + public static final int SET_PREVIEW_CALLBACK_WITH_BUFFER = 104; + public static final int ADD_CALLBACK_BUFFER = 105; + public static final int SET_PREVIEW_DISPLAY_ASYNC = 106; + public static final int SET_PREVIEW_CALLBACK = 107; + public static final int SET_ONE_SHOT_PREVIEW_CALLBACK = 108; + // Parameters + public static final int SET_PARAMETERS = 201; + public static final int GET_PARAMETERS = 202; + public static final int REFRESH_PARAMETERS = 203; + public static final int APPLY_SETTINGS = 204; + // Focus, Zoom + public static final int AUTO_FOCUS = 301; + public static final int CANCEL_AUTO_FOCUS = 302; + public static final int SET_AUTO_FOCUS_MOVE_CALLBACK = 303; + public static final int SET_ZOOM_CHANGE_LISTENER = 304; + // Face detection + public static final int SET_FACE_DETECTION_LISTENER = 461; + public static final int START_FACE_DETECTION = 462; + public static final int STOP_FACE_DETECTION = 463; + public static final int SET_ERROR_CALLBACK = 464; + // Presentation + public static final int ENABLE_SHUTTER_SOUND = 501; + public static final int SET_DISPLAY_ORIENTATION = 502; + // Capture + public static final int CAPTURE_PHOTO = 601; +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java new file mode 100644 index 0000000..dd4f77c --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java @@ -0,0 +1,830 @@ +/* + * Copyright (C) 2012 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.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.OnZoomChangeListener; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.view.SurfaceHolder; + +import com.android.ex.camera2.portability.debug.Log; + +/** + * An interface which provides possible camera device operations. + * + * The client should call {@code CameraAgent.openCamera} to get an instance + * of {@link CameraAgent.CameraProxy} to control the camera. Classes + * implementing this interface should have its own one unique {@code Thread} + * other than the main thread for camera operations. Camera device callbacks + * are wrapped since the client should not deal with + * {@code android.hardware.Camera} directly. + * + * TODO: provide callback interfaces for: + * {@code android.hardware.Camera.ErrorCallback}, + * {@code android.hardware.Camera.OnZoomChangeListener}, and + */ +public abstract class CameraAgent { + public static final long CAMERA_OPERATION_TIMEOUT_MS = 2500; + + private static final Log.Tag TAG = new Log.Tag("CamAgnt"); + + public static class CameraStartPreviewCallbackForward + implements CameraStartPreviewCallback { + private final Handler mHandler; + private final CameraStartPreviewCallback mCallback; + + public static CameraStartPreviewCallbackForward getNewInstance( + Handler handler, CameraStartPreviewCallback cb) { + if (handler == null || cb == null) { + return null; + } + return new CameraStartPreviewCallbackForward(handler, cb); + } + + private CameraStartPreviewCallbackForward(Handler h, + CameraStartPreviewCallback cb) { + mHandler = h; + mCallback = cb; + } + + @Override + public void onPreviewStarted() { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onPreviewStarted(); + }}); + } + } + + /** + * A callback helps to invoke the original callback on another + * {@link android.os.Handler}. + */ + public static class CameraOpenCallbackForward implements CameraOpenCallback { + private final Handler mHandler; + private final CameraOpenCallback mCallback; + + /** + * Returns a new instance of {@link FaceDetectionCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param cb The callback to be invoked. + * @return The instance of the {@link FaceDetectionCallbackForward}, or + * null if any parameter is null. + */ + public static CameraOpenCallbackForward getNewInstance( + Handler handler, CameraOpenCallback cb) { + if (handler == null || cb == null) { + return null; + } + return new CameraOpenCallbackForward(handler, cb); + } + + private CameraOpenCallbackForward(Handler h, CameraOpenCallback cb) { + // Given that we are using the main thread handler, we can create it + // here instead of holding onto the PhotoModule objects. In this + // way, we can avoid memory leak. + mHandler = new Handler(Looper.getMainLooper()); + mCallback = cb; + } + + @Override + public void onCameraOpened(final CameraProxy camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onCameraOpened(camera); + }}); + } + + @Override + public void onCameraDisabled(final int cameraId) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onCameraDisabled(cameraId); + }}); + } + + @Override + public void onDeviceOpenFailure(final int cameraId, final String info) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onDeviceOpenFailure(cameraId, info); + }}); + } + + @Override + public void onDeviceOpenedAlready(final int cameraId, final String info) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onDeviceOpenedAlready(cameraId, info); + }}); + } + + @Override + public void onReconnectionFailure(final CameraAgent mgr, final String info) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onReconnectionFailure(mgr, info); + }}); + } + } + + /** + * A handler for all camera api runtime exceptions. + * The default behavior is to throw the runtime exception. + */ + public static interface CameraExceptionCallback { + public void onCameraException(RuntimeException e); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.ErrorCallback} + */ + public static interface CameraErrorCallback { + public void onError(int error, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.AutoFocusCallback}. + */ + public static interface CameraAFCallback { + public void onAutoFocus(boolean focused, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.AutoFocusMoveCallback}. + */ + public static interface CameraAFMoveCallback { + public void onAutoFocusMoving(boolean moving, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.ShutterCallback}. + */ + public static interface CameraShutterCallback { + public void onShutter(CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.PictureCallback}. + */ + public static interface CameraPictureCallback { + public void onPictureTaken(byte[] data, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.PreviewCallback}. + */ + public static interface CameraPreviewDataCallback { + public void onPreviewFrame(byte[] data, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.FaceDetectionListener}. + */ + public static interface CameraFaceDetectionCallback { + /** + * Callback for face detection. + * + * @param faces Recognized face in the preview. + * @param camera The camera which the preview image comes from. + */ + public void onFaceDetection(Camera.Face[] faces, CameraProxy camera); + } + + /** + * An interface to be called when the camera preview has started. + */ + public static interface CameraStartPreviewCallback { + /** + * Callback when the preview starts. + */ + public void onPreviewStarted(); + } + + /** + * An interface to be called for any events when opening or closing the + * camera device. This error callback is different from the one defined + * in the framework, {@link android.hardware.Camera.ErrorCallback}, which + * is used after the camera is opened. + */ + public static interface CameraOpenCallback { + /** + * Callback when camera open succeeds. + */ + public void onCameraOpened(CameraProxy camera); + + /** + * Callback when {@link com.android.camera.CameraDisabledException} is + * caught. + * + * @param cameraId The disabled camera. + */ + public void onCameraDisabled(int cameraId); + + /** + * Callback when {@link com.android.camera.CameraHardwareException} is + * caught. + * + * @param cameraId The camera with the hardware failure. + * @param info The extra info regarding this failure. + */ + public void onDeviceOpenFailure(int cameraId, String info); + + /** + * Callback when trying to open the camera which is already opened. + * + * @param cameraId The camera which is causing the open error. + */ + public void onDeviceOpenedAlready(int cameraId, String info); + + /** + * Callback when {@link java.io.IOException} is caught during + * {@link android.hardware.Camera#reconnect()}. + * + * @param mgr The {@link CameraAgent} + * with the reconnect failure. + */ + public void onReconnectionFailure(CameraAgent mgr, String info); + } + + /** + * Opens the camera of the specified ID asynchronously. The camera device + * will be opened in the camera handler thread and will be returned through + * the {@link CameraAgent.CameraOpenCallback# + * onCameraOpened(com.android.camera.cameradevice.CameraAgent.CameraProxy)}. + * + * @param handler The {@link android.os.Handler} in which the callback + * was handled. + * @param callback The callback for the result. + * @param cameraId The camera ID to open. + */ + public void openCamera(final Handler handler, final int cameraId, + final CameraOpenCallback callback) { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().obtainMessage(CameraActions.OPEN_CAMERA, cameraId, 0, + CameraOpenCallbackForward.getNewInstance(handler, callback)).sendToTarget(); + }}); + } + + /** + * Closes the camera device. + * + * @param camera The camera to close. {@code null} means all. + * @param synced Whether this call should be synchronous. + */ + public void closeCamera(CameraProxy camera, boolean synced) { + if (synced) { + final WaitDoneBundle bundle = new WaitDoneBundle(); + + getDispatchThread().runJobSync(new Runnable() { + @Override + public void run() { + getCameraHandler().obtainMessage(CameraActions.RELEASE).sendToTarget(); + getCameraHandler().post(bundle.mUnlockRunnable); + }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "camera release"); + } else { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().removeCallbacksAndMessages(null); + getCameraHandler().obtainMessage(CameraActions.RELEASE).sendToTarget(); + }}); + } + } + + /** + * Sets a callback for handling camera api runtime exceptions on + * a handler. + */ + public abstract void setCameraDefaultExceptionCallback(CameraExceptionCallback callback, + Handler handler); + + /** + * Recycles the resources used by this instance. CameraAgent will be in + * an unusable state after calling this. + */ + public abstract void recycle(); + + /** + * @return The camera devices info. + */ + public abstract CameraDeviceInfo getCameraDeviceInfo(); + + /** + * @return The handler to which camera tasks should be posted. + */ + protected abstract Handler getCameraHandler(); + + /** + * @return The thread used on which client callbacks are served. + */ + protected abstract DispatchThread getDispatchThread(); + + /** + * An interface that takes camera operation requests and post messages to the + * camera handler thread. All camera operations made through this interface is + * asynchronous by default except those mentioned specifically. + */ + public static abstract class CameraProxy { + + /** + * Returns the underlying {@link android.hardware.Camera} object used + * by this proxy. This method should only be used when handing the + * camera device over to {@link android.media.MediaRecorder} for + * recording. + */ + @Deprecated + public abstract android.hardware.Camera getCamera(); + + /** + * @return The camera ID associated to by this + * {@link CameraAgent.CameraProxy}. + */ + public abstract int getCameraId(); + + /** + * @return The camera characteristics. + */ + public abstract CameraDeviceInfo.Characteristics getCharacteristics(); + + /** + * @return The camera capabilities. + */ + public abstract CameraCapabilities getCapabilities(); + + /** + * Reconnects to the camera device. On success, the camera device will + * be returned through {@link CameraAgent + * .CameraOpenCallback#onCameraOpened(com.android.camera.cameradevice.CameraAgent + * .CameraProxy)}. + * @see android.hardware.Camera#reconnect() + * + * @param handler The {@link android.os.Handler} in which the callback + * was handled. + * @param cb The callback when any error happens. + */ + public void reconnect(final Handler handler, final CameraOpenCallback cb) { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().obtainMessage(CameraActions.RECONNECT, getCameraId(), 0, + CameraOpenCallbackForward.getNewInstance(handler, cb)).sendToTarget(); + }}); + } + + /** + * Unlocks the camera device. + * + * @see android.hardware.Camera#unlock() + */ + public void unlock() { + final WaitDoneBundle bundle = new WaitDoneBundle(); + getDispatchThread().runJobSync(new Runnable() { + @Override + public void run() { + getCameraHandler().sendEmptyMessage(CameraActions.UNLOCK); + getCameraHandler().post(bundle.mUnlockRunnable); + }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "camera unlock"); + } + + /** + * Locks the camera device. + * @see android.hardware.Camera#lock() + */ + public void lock() { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().sendEmptyMessage(CameraActions.LOCK); + }}); + } + + /** + * Sets the {@link android.graphics.SurfaceTexture} for preview. + * + * @param surfaceTexture The {@link SurfaceTexture} for preview. + */ + public void setPreviewTexture(final SurfaceTexture surfaceTexture) { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler() + .obtainMessage(CameraActions.SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture) + .sendToTarget(); + }}); + } + + /** + * Blocks until a {@link android.graphics.SurfaceTexture} has been set + * for preview. + * + * @param surfaceTexture The {@link SurfaceTexture} for preview. + */ + public void setPreviewTextureSync(final SurfaceTexture surfaceTexture) { + final WaitDoneBundle bundle = new WaitDoneBundle(); + getDispatchThread().runJobSync(new Runnable() { + @Override + public void run() { + getCameraHandler() + .obtainMessage(CameraActions.SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture) + .sendToTarget(); + getCameraHandler().post(bundle.mUnlockRunnable); + }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "set preview texture"); + } + + /** + * Sets the {@link android.view.SurfaceHolder} for preview. + * + * @param surfaceHolder The {@link SurfaceHolder} for preview. + */ + public void setPreviewDisplay(final SurfaceHolder surfaceHolder) { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler() + .obtainMessage(CameraActions.SET_PREVIEW_DISPLAY_ASYNC, surfaceHolder) + .sendToTarget(); + }}); + } + + /** + * Starts the camera preview. + */ + public void startPreview() { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler() + .obtainMessage(CameraActions.START_PREVIEW_ASYNC, null).sendToTarget(); + }}); + } + + /** + * Starts the camera preview and executes a callback on a handler once + * the preview starts. + */ + public void startPreviewWithCallback(final Handler h, final CameraStartPreviewCallback cb) { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().obtainMessage(CameraActions.START_PREVIEW_ASYNC, + CameraStartPreviewCallbackForward.getNewInstance(h, cb)) + .sendToTarget(); + }}); + } + + /** + * Stops the camera preview synchronously. + * {@code stopPreview()} must be synchronous to ensure that the caller can + * continues to release resources related to camera preview. + */ + public void stopPreview() { + final WaitDoneBundle bundle = new WaitDoneBundle(); + getDispatchThread().runJobSync(new Runnable() { + @Override + public void run() { + getCameraHandler().sendEmptyMessage(CameraActions.STOP_PREVIEW); + getCameraHandler().post(bundle.mUnlockRunnable); + }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "stop preview"); + } + + /** + * Sets the callback for preview data. + * + * @param handler The {@link android.os.Handler} in which the callback was handled. + * @param cb The callback to be invoked when the preview data is available. + * @see android.hardware.Camera#setPreviewCallback(android.hardware.Camera.PreviewCallback) + */ + public abstract void setPreviewDataCallback(Handler handler, CameraPreviewDataCallback cb); + + /** + * Sets the one-time callback for preview data. + * + * @param handler The {@link android.os.Handler} in which the callback was handled. + * @param cb The callback to be invoked when the preview data for + * next frame is available. + * @see android.hardware.Camera#setPreviewCallback(android.hardware.Camera.PreviewCallback) + */ + public abstract void setOneShotPreviewCallback(Handler handler, + CameraPreviewDataCallback cb); + + /** + * Sets the callback for preview data. + * + * @param handler The handler in which the callback will be invoked. + * @param cb The callback to be invoked when the preview data is available. + * @see android.hardware.Camera#setPreviewCallbackWithBuffer(android.hardware.Camera.PreviewCallback) + */ + public abstract void setPreviewDataCallbackWithBuffer(Handler handler, + CameraPreviewDataCallback cb); + + /** + * Adds buffer for the preview callback. + * + * @param callbackBuffer The buffer allocated for the preview data. + */ + public void addCallbackBuffer(final byte[] callbackBuffer) { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler() + .obtainMessage(CameraActions.ADD_CALLBACK_BUFFER, callbackBuffer) + .sendToTarget(); + } + }); + } + + /** + * Starts the auto-focus process. The result will be returned through the callback. + * + * @param handler The handler in which the callback will be invoked. + * @param cb The auto-focus callback. + */ + public abstract void autoFocus(Handler handler, CameraAFCallback cb); + + /** + * Cancels the auto-focus process. + */ + public void cancelAutoFocus() { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().removeMessages(CameraActions.AUTO_FOCUS); + getCameraHandler().sendEmptyMessage(CameraActions.CANCEL_AUTO_FOCUS); + }}); + } + + /** + * Sets the auto-focus callback + * + * @param handler The handler in which the callback will be invoked. + * @param cb The callback to be invoked when the preview data is available. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public abstract void setAutoFocusMoveCallback(Handler handler, CameraAFMoveCallback cb); + + /** + * Instrument the camera to take a picture. + * + * @param handler The handler in which the callback will be invoked. + * @param shutter The callback for shutter action, may be null. + * @param raw The callback for uncompressed data, may be null. + * @param postview The callback for postview image data, may be null. + * @param jpeg The callback for jpeg image data, may be null. + * @see android.hardware.Camera#takePicture( + * android.hardware.Camera.ShutterCallback, + * android.hardware.Camera.PictureCallback, + * android.hardware.Camera.PictureCallback) + */ + public abstract void takePicture( + Handler handler, + CameraShutterCallback shutter, + CameraPictureCallback raw, + CameraPictureCallback postview, + CameraPictureCallback jpeg); + + /** + * Sets the display orientation for camera to adjust the preview and JPEG orientation. + * + * @param degrees The counterclockwise rotation in degrees, relative to the device's natural + * orientation. Should be 0, 90, 180 or 270. + */ + public void setDisplayOrientation(final int degrees) { + setDisplayOrientation(degrees, true); + } + + /** + * Sets the display orientation for camera to adjust the preview—and, optionally, + * JPEG—orientations. + * <p>If capture rotation is not requested, future captures will be returned in the sensor's + * physical rotation, which does not necessarily match the device's natural orientation.</p> + * + * @param degrees The counterclockwise rotation in degrees, relative to the device's natural + * orientation. Should be 0, 90, 180 or 270. + * @param capture Whether to adjust the JPEG capture orientation as well as the preview one. + */ + public void setDisplayOrientation(final int degrees, final boolean capture) { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler() + .obtainMessage(CameraActions.SET_DISPLAY_ORIENTATION, degrees, + capture ? 1 : 0) + .sendToTarget(); + }}); + } + + /** + * Sets the listener for zoom change. + * + * @param listener The listener. + */ + public abstract void setZoomChangeListener(OnZoomChangeListener listener); + + /** + * Sets the face detection listener. + * + * @param handler The handler in which the callback will be invoked. + * @param callback The callback for face detection results. + */ + public abstract void setFaceDetectionCallback(Handler handler, + CameraFaceDetectionCallback callback); + + /** + * Starts the face detection. + */ + public void startFaceDetection() { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().sendEmptyMessage(CameraActions.START_FACE_DETECTION); + }}); + } + + /** + * Stops the face detection. + */ + public void stopFaceDetection() { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().sendEmptyMessage(CameraActions.STOP_FACE_DETECTION); + }}); + } + + /** + * Registers an error callback. + * + * @param handler The handler on which the callback will be invoked. + * @param cb The error callback. + * @see android.hardware.Camera#setErrorCallback(android.hardware.Camera.ErrorCallback) + */ + public abstract void setErrorCallback(Handler handler, CameraErrorCallback cb); + + /** + * Sets the camera parameters. + * + * @param params The camera parameters to use. + */ + @Deprecated + public abstract void setParameters(Camera.Parameters params); + + /** + * Gets the current camera parameters synchronously. This method is + * synchronous since the caller has to wait for the camera to return + * the parameters. If the parameters are already cached, it returns + * immediately. + */ + @Deprecated + public abstract Camera.Parameters getParameters(); + + /** + * Gets the current camera settings synchronously. + * <p>This method is synchronous since the caller has to wait for the + * camera to return the parameters. If the parameters are already + * cached, it returns immediately.</p> + */ + public abstract CameraSettings getSettings(); + + /** + * Default implementation of {@link #applySettings(CameraSettings)} + * that is only missing the set of states it needs to wait for + * before applying the settings. + * + * @param settings The settings to use on the device. + * @param statesToAwait Bitwise OR of the required camera states. + * @return Whether the settings can be applied. + */ + protected boolean applySettingsHelper(CameraSettings settings, + final int statesToAwait) { + if (settings == null) { + Log.v(TAG, "null parameters in applySettings()"); + return false; + } + if (!getCapabilities().supports(settings)) { + return false; + } + + final CameraSettings copyOfSettings = settings.copy(); + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraState().waitForStates(statesToAwait); + getCameraHandler().obtainMessage(CameraActions.APPLY_SETTINGS, copyOfSettings) + .sendToTarget(); + }}); + return true; + } + + /** + * Applies the settings to the camera device. + * + * @param settings The settings to use on the device. + * @return Whether the settings can be applied. + */ + public abstract boolean applySettings(CameraSettings settings); + + /** + * Forces {@code CameraProxy} to update the cached version of the camera + * settings regardless of the dirty bit. + */ + public void refreshSettings() { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler().sendEmptyMessage(CameraActions.REFRESH_PARAMETERS); + }}); + } + + /** + * Enables/Disables the camera shutter sound. + * + * @param enable {@code true} to enable the shutter sound, + * {@code false} to disable it. + */ + public void enableShutterSound(final boolean enable) { + getDispatchThread().runJob(new Runnable() { + @Override + public void run() { + getCameraHandler() + .obtainMessage(CameraActions.ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0) + .sendToTarget(); + }}); + } + + /** + * Dumps the current settings of the camera device. + * + * <p>The content varies based on the underlying camera API settings + * implementation.</p> + * + * @return The content of the device settings represented by a string. + */ + public abstract String dumpDeviceSettings(); + + /** + * @return The handler to which camera tasks should be posted. + */ + public abstract Handler getCameraHandler(); + + /** + * @return The thread used on which client callbacks are served. + */ + public abstract DispatchThread getDispatchThread(); + + /** + * @return The state machine tracking the camera API's current mode. + */ + public abstract CameraStateHolder getCameraState(); + } + + public static class WaitDoneBundle { + public final Runnable mUnlockRunnable; + public final Object mWaitLock; + + WaitDoneBundle() { + mWaitLock = new Object(); + mUnlockRunnable = new Runnable() { + @Override + public void run() { + synchronized (mWaitLock) { + mWaitLock.notifyAll(); + } + }}; + } + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgentFactory.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgentFactory.java new file mode 100644 index 0000000..77ad0c3 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgentFactory.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2013 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.content.Context; +import android.os.Build; + +import com.android.ex.camera2.portability.debug.Log; +import com.android.ex.camera2.portability.util.SystemProperties; + +/** + * A factory class for {@link CameraAgent}. + * + * <p>The choice of framework API to use can be made automatically based on the + * system API level, explicitly forced by the client app, or overridden entirely + * by setting the system property com.camera2.portability.fwk_api to 1 or 2.</p> + */ +public class CameraAgentFactory { + private static final Log.Tag TAG = new Log.Tag("CamAgntFact"); + + /** Android release replacing the Camera class with the camera2 package. */ + private static final int FIRST_SDK_WITH_API_2 = 21; + + // The debugging override, which overrides *all* API level selections if set + // to API_LEVEL_OVERRIDE_API{1,2}; otherwise, this has no effect. Note that + // we check this once when the library is first loaded so that #recycle() + // doesn't try to clean up the wrong type of CameraAgent. + private static final String API_LEVEL_OVERRIDE_KEY = "camera2.portability.force_api"; + private static final String API_LEVEL_OVERRIDE_DEFAULT = "0"; + private static final String API_LEVEL_OVERRIDE_API1 = "1"; + private static final String API_LEVEL_OVERRIDE_API2 = "2"; + private static final String API_LEVEL_OVERRIDE_VALUE = + SystemProperties.get(API_LEVEL_OVERRIDE_KEY, API_LEVEL_OVERRIDE_DEFAULT); + + private static CameraAgent sAndroidCameraAgent; + private static CameraAgent sAndroidCamera2Agent; + private static int sAndroidCameraAgentClientCount; + private static int sAndroidCamera2AgentClientCount; + + /** + * Used to indicate which camera framework should be used. + */ + public static enum CameraApi { + /** Automatically select based on the device's SDK level. */ + AUTO, + + /** Use the {@link android.hardware.Camera} class. */ + API_1, + + /** Use the {@link android.hardware.camera2} package. */ + API_2 + }; + + private static CameraApi highestSupportedApi() { + // TODO: Check SDK_INT instead of RELEASE before L launch + if (Build.VERSION.SDK_INT >= FIRST_SDK_WITH_API_2 || Build.VERSION.CODENAME.equals("L")) { + return CameraApi.API_2; + } else { + return CameraApi.API_1; + } + } + + private static CameraApi validateApiChoice(CameraApi choice) { + if (API_LEVEL_OVERRIDE_VALUE.equals(API_LEVEL_OVERRIDE_API1)) { + Log.d(TAG, "API level overridden by system property: forced to 1"); + return CameraApi.API_1; + } else if (API_LEVEL_OVERRIDE_VALUE.equals(API_LEVEL_OVERRIDE_API2)) { + Log.d(TAG, "API level overridden by system property: forced to 2"); + return CameraApi.API_2; + } + + if (choice == null) { + Log.w(TAG, "null API level request, so assuming AUTO"); + choice = CameraApi.AUTO; + } + if (choice == CameraApi.AUTO) { + choice = highestSupportedApi(); + } + + return choice; + } + + /** + * Returns the android camera implementation of + * {@link com.android.camera.cameradevice.CameraAgent}. + * + * <p>To clean up the resources allocated by this call, be sure to invoke + * {@link #recycle(boolean)} with the same {@code api} value provided + * here.</p> + * + * @param context The application context. + * @param api Which camera framework to use. + * @return The {@link CameraAgent} to control the camera device. + * + * @throws UnsupportedOperationException If {@code CameraApi.API_2} was + * requested on an unsupported device. + */ + public static synchronized CameraAgent getAndroidCameraAgent(Context context, CameraApi api) { + api = validateApiChoice(api); + + if (api == CameraApi.API_1) { + if (sAndroidCameraAgent == null) { + sAndroidCameraAgent = new AndroidCameraAgentImpl(); + sAndroidCameraAgentClientCount = 1; + } else { + ++sAndroidCameraAgentClientCount; + } + return sAndroidCameraAgent; + } else { // API_2 + if (highestSupportedApi() == CameraApi.API_1) { + throw new UnsupportedOperationException("Camera API_2 unavailable on this device"); + } + + if (sAndroidCamera2Agent == null) { + sAndroidCamera2Agent = new AndroidCamera2AgentImpl(context); + sAndroidCamera2AgentClientCount = 1; + } else { + ++sAndroidCamera2AgentClientCount; + } + return sAndroidCamera2Agent; + } + } + + /** + * Recycles the resources. Always call this method when the activity is + * stopped. + * + * @param api Which camera framework handle to recycle. + * + * @throws UnsupportedOperationException If {@code CameraApi.API_2} was + * requested on an unsupported device. + */ + public static synchronized void recycle(CameraApi api) { + api = validateApiChoice(api); + + if (api == CameraApi.API_1) { + if (--sAndroidCameraAgentClientCount == 0 && sAndroidCameraAgent != null) { + sAndroidCameraAgent.recycle(); + sAndroidCameraAgent = null; + } + } else { // API_2 + if (highestSupportedApi() == CameraApi.API_1) { + throw new UnsupportedOperationException("Camera API_2 unavailable on this device"); + } + + if (--sAndroidCamera2AgentClientCount == 0 && sAndroidCamera2Agent != null) { + sAndroidCamera2Agent.recycle(); + sAndroidCamera2Agent = null; + } + } + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilities.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilities.java new file mode 100644 index 0000000..6a4c72c --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilities.java @@ -0,0 +1,765 @@ +/* + * 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 com.android.ex.camera2.portability.debug.Log; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * This class holds all the static information of a camera's capabilities. + * <p> + * The design of this class is thread-safe and can be passed around regardless + * of which thread using it. + * </p> + */ +public class CameraCapabilities { + + private static Log.Tag TAG = new Log.Tag("CamCapabs"); + + /* All internal states are declared final and should be thread-safe. */ + + protected final ArrayList<int[]> mSupportedPreviewFpsRange = new ArrayList<int[]>(); + protected final ArrayList<Size> mSupportedPreviewSizes = new ArrayList<Size>(); + protected final TreeSet<Integer> mSupportedPreviewFormats = new TreeSet<Integer>(); + protected final ArrayList<Size> mSupportedVideoSizes = new ArrayList<Size>(); + protected final ArrayList<Size> mSupportedPhotoSizes = new ArrayList<Size>(); + protected final TreeSet<Integer> mSupportedPhotoFormats = new TreeSet<Integer>(); + protected final EnumSet<SceneMode> mSupportedSceneModes = EnumSet.noneOf(SceneMode.class); + protected final EnumSet<FlashMode> mSupportedFlashModes = EnumSet.noneOf(FlashMode.class); + protected final EnumSet<FocusMode> mSupportedFocusModes = EnumSet.noneOf(FocusMode.class); + protected final EnumSet<WhiteBalance> mSupportedWhiteBalances = + EnumSet.noneOf(WhiteBalance.class); + protected final EnumSet<Feature> mSupportedFeatures = EnumSet.noneOf(Feature.class); + protected Size mPreferredPreviewSizeForVideo; + protected int mMinExposureCompensation; + protected int mMaxExposureCompensation; + protected float mExposureCompensationStep; + protected int mMaxNumOfFacesSupported; + protected int mMaxNumOfFocusAreas; + protected int mMaxNumOfMeteringArea; + protected int mMaxZoomRatio; + protected float mHorizontalViewAngle; + protected float mVerticalViewAngle; + private final Stringifier mStringifier; + protected final ArrayList<Integer> mZoomRatioList = new ArrayList<Integer>(); + protected int mMaxZoomIndex; + + /** + * Focus modes. + */ + public enum FocusMode { + /** + * Continuous auto focus mode intended for taking pictures. + * @see {@link android.hardware.Camera.Parameters#FOCUS_MODE_AUTO}. + */ + AUTO, + /** + * Continuous auto focus mode intended for taking pictures. + * @see {@link android.hardware.Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. + */ + CONTINUOUS_PICTURE, + /** + * Continuous auto focus mode intended for video recording. + * @see {@link android.hardware.Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO}. + */ + CONTINUOUS_VIDEO, + /** + * Extended depth of field (EDOF). + * @see {@link android.hardware.Camera.Parameters#FOCUS_MODE_EDOF}. + */ + EXTENDED_DOF, + /** + * Focus is fixed. + * @see {@link android.hardware.Camera.Parameters#FOCUS_MODE_FIXED}. + */ + FIXED, + /** + * Focus is set at infinity. + * @see {@link android.hardware.Camera.Parameters#FOCUS_MODE_INFINITY}. + */ + // TODO: Unsupported on API 2 + INFINITY, + /** + * Macro (close-up) focus mode. + * @see {@link android.hardware.Camera.Parameters#FOCUS_MODE_MACRO}. + */ + MACRO, + } + + /** + * Flash modes. + */ + public enum FlashMode { + /** + * No flash. + */ + NO_FLASH, + /** + * Flash will be fired automatically when required. + * @see {@link android.hardware.Camera.Parameters#FLASH_MODE_OFF}. + */ + AUTO, + /** + * Flash will not be fired. + * @see {@link android.hardware.Camera.Parameters#FLASH_MODE_OFF}. + */ + OFF, + /** + * Flash will always be fired during snapshot. + * @see {@link android.hardware.Camera.Parameters#FLASH_MODE_ON}. + */ + ON, + /** + * Constant emission of light during preview, auto-focus and snapshot. + * @see {@link android.hardware.Camera.Parameters#FLASH_MODE_TORCH}. + */ + TORCH, + /** + * Flash will be fired in red-eye reduction mode. + * @see {@link android.hardware.Camera.Parameters#FLASH_MODE_RED_EYE}. + */ + RED_EYE, + } + + /** + * Scene modes. + */ + public enum SceneMode { + /** + * No supported scene mode. + */ + NO_SCENE_MODE, + /** + * Scene mode is off. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_AUTO}. + */ + AUTO, + /** + * Take photos of fast moving objects. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_ACTION}. + */ + ACTION, + /** + * Applications are looking for a barcode. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_BARCODE}. + */ + BARCODE, + /** + * Take pictures on the beach. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_BEACH}. + */ + BEACH, + /** + * Capture the naturally warm color of scenes lit by candles. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_CANDLELIGHT}. + */ + CANDLELIGHT, + /** + * For shooting firework displays. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_FIREWORKS}. + */ + FIREWORKS, + /** + * Capture a scene using high dynamic range imaging techniques. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_HDR}. + */ + // TODO: Unsupported on API 2 + HDR, + /** + * Take pictures on distant objects. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_LANDSCAPE}. + */ + LANDSCAPE, + /** + * Take photos at night. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_NIGHT}. + */ + NIGHT, + /** + * Take people pictures at night. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_NIGHT_PORTRAIT}. + */ + // TODO: Unsupported on API 2 + NIGHT_PORTRAIT, + /** + * Take indoor low-light shot. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_PARTY}. + */ + PARTY, + /** + * Take people pictures. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_PORTRAIT}. + */ + PORTRAIT, + /** + * Take pictures on the snow. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_SNOW}. + */ + SNOW, + /** + * Take photos of fast moving objects. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_SPORTS}. + */ + SPORTS, + /** + * Avoid blurry pictures (for example, due to hand shake). + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_STEADYPHOTO}. + */ + STEADYPHOTO, + /** + * Take sunset photos. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_SUNSET}. + */ + SUNSET, + /** + * Take photos in a theater. + * @see {@link android.hardware.Camera.Parameters#SCENE_MODE_THEATRE}. + */ + THEATRE, + } + + /** + * White blances. + */ + public enum WhiteBalance { + /** + * @see {@link android.hardware.Camera.Parameters#WHITE_BALANCE_AUTO}. + */ + AUTO, + /** + * @see {@link android.hardware.Camera.Parameters#WHITE_BALANCE_CLOUDY_DAYLIGHT}. + */ + CLOUDY_DAYLIGHT, + /** + * @see {@link android.hardware.Camera.Parameters#WHITE_BALANCE_DAYLIGHT}. + */ + DAYLIGHT, + /** + * @see {@link android.hardware.Camera.Parameters#WHITE_BALANCE_FLUORESCENT}. + */ + FLUORESCENT, + /** + * @see {@link android.hardware.Camera.Parameters#WHITE_BALANCE_INCANDESCENT}. + */ + INCANDESCENT, + /** + * @see {@link android.hardware.Camera.Parameters#WHITE_BALANCE_SHADE}. + */ + SHADE, + /** + * @see {@link android.hardware.Camera.Parameters#WHITE_BALANCE_TWILIGHT}. + */ + TWILIGHT, + /** + * @see {@link android.hardware.Camera.Parameters#WHITE_BALANCE_WARM_FLUORESCENT}. + */ + WARM_FLUORESCENT, + } + + /** + * Features. + */ + public enum Feature { + /** + * Support zoom-related methods. + */ + ZOOM, + /** + * Support for photo capturing during video recording. + */ + VIDEO_SNAPSHOT, + /** + * Support for focus area settings. + */ + FOCUS_AREA, + /** + * Support for metering area settings. + */ + METERING_AREA, + /** + * Support for automatic exposure lock. + */ + AUTO_EXPOSURE_LOCK, + /** + * Support for automatic white balance lock. + */ + AUTO_WHITE_BALANCE_LOCK, + /** + * Support for video stabilization. + */ + VIDEO_STABILIZATION, + } + + /** + * A interface stringifier to convert abstract representations to API + * related string representation. + */ + public static class Stringifier { + /** + * Converts the string to hyphen-delimited lowercase for compatibility with multiple APIs. + * + * @param enumCase The name of an enum constant. + * @return The converted string. + */ + private static String toApiCase(String enumCase) { + return enumCase.toLowerCase().replaceAll("_", "-"); + } + + /** + * Conerts the string to underscore-delimited uppercase to match the enum constant names. + * + * @param apiCase An API-related string representation. + * @return The converted string. + */ + private static String toEnumCase(String apiCase) { + return apiCase.toUpperCase().replaceAll("-", "_"); + } + + /** + * Converts the focus mode to API-related string representation. + * + * @param focus The focus mode to convert. + * @return The string used by the camera framework API to represent the + * focus mode. + */ + public String stringify(FocusMode focus) { + return toApiCase(focus.name()); + } + + /** + * Converts the API-related string representation of the focus mode to the + * abstract representation. + * + * @param val The string representation. + * @return The focus mode represented by the input string, or the focus + * mode with the lowest ordinal if it cannot be converted. + */ + public FocusMode focusModeFromString(String val) { + if (val == null) { + return FocusMode.values()[0]; + } + try { + return FocusMode.valueOf(toEnumCase(val)); + } catch (IllegalArgumentException ex) { + return FocusMode.values()[0]; + } + } + + /** + * Converts the flash mode to API-related string representation. + * + * @param flash The focus mode to convert. + * @return The string used by the camera framework API to represent the + * flash mode. + */ + public String stringify(FlashMode flash) { + return toApiCase(flash.name()); + } + + /** + * Converts the API-related string representation of the flash mode to the + * abstract representation. + * + * @param val The string representation. + * @return The flash mode represented by the input string, or the flash + * mode with the lowest ordinal if it cannot be converted. + */ + public FlashMode flashModeFromString(String val) { + if (val == null) { + return FlashMode.values()[0]; + } + try { + return FlashMode.valueOf(toEnumCase(val)); + } catch (IllegalArgumentException ex) { + return FlashMode.values()[0]; + } + } + + /** + * Converts the scene mode to API-related string representation. + * + * @param scene The focus mode to convert. + * @return The string used by the camera framework API to represent the + * scene mode. + */ + public String stringify(SceneMode scene) { + return toApiCase(scene.name()); + } + + /** + * Converts the API-related string representation of the scene mode to the + * abstract representation. + * + * @param val The string representation. + * @return The scene mode represented by the input string, or the scene + * mode with the lowest ordinal if it cannot be converted. + */ + public SceneMode sceneModeFromString(String val) { + if (val == null) { + return SceneMode.values()[0]; + } + try { + return SceneMode.valueOf(toEnumCase(val)); + } catch (IllegalArgumentException ex) { + return SceneMode.values()[0]; + } + } + + /** + * Converts the white balance to API-related string representation. + * + * @param wb The focus mode to convert. + * @return The string used by the camera framework API to represent the + * white balance. + */ + public String stringify(WhiteBalance wb) { + return toApiCase(wb.name()); + } + + /** + * Converts the API-related string representation of the white balance to + * the abstract representation. + * + * @param val The string representation. + * @return The white balance represented by the input string, or the + * white balance with the lowest ordinal if it cannot be + * converted. + */ + public WhiteBalance whiteBalanceFromString(String val) { + if (val == null) { + return WhiteBalance.values()[0]; + } + try { + return WhiteBalance.valueOf(toEnumCase(val)); + } catch (IllegalArgumentException ex) { + return WhiteBalance.values()[0]; + } + } + } + + /** + * Constructor. + * @param stringifier The API-specific stringifier for this instance. + */ + CameraCapabilities(Stringifier stringifier) { + mStringifier = stringifier; + } + + /** + * Copy constructor. + * @param src The source instance. + */ + public CameraCapabilities(CameraCapabilities src) { + mSupportedPreviewFpsRange.addAll(src.mSupportedPreviewFpsRange); + mSupportedPreviewSizes.addAll(src.mSupportedPreviewSizes); + mSupportedPreviewFormats.addAll(src.mSupportedPreviewFormats); + mSupportedVideoSizes.addAll(src.mSupportedVideoSizes); + mSupportedPhotoSizes.addAll(src.mSupportedPhotoSizes); + mSupportedPhotoFormats.addAll(src.mSupportedPhotoFormats); + mSupportedSceneModes.addAll(src.mSupportedSceneModes); + mSupportedFlashModes.addAll(src.mSupportedFlashModes); + mSupportedFocusModes.addAll(src.mSupportedFocusModes); + mSupportedWhiteBalances.addAll(src.mSupportedWhiteBalances); + mSupportedFeatures.addAll(src.mSupportedFeatures); + mPreferredPreviewSizeForVideo = src.mPreferredPreviewSizeForVideo; + mMaxExposureCompensation = src.mMaxExposureCompensation; + mMinExposureCompensation = src.mMinExposureCompensation; + mExposureCompensationStep = src.mExposureCompensationStep; + mMaxNumOfFacesSupported = src.mMaxNumOfFacesSupported; + mMaxNumOfFocusAreas = src.mMaxNumOfFocusAreas; + mMaxNumOfMeteringArea = src.mMaxNumOfMeteringArea; + mMaxZoomIndex = src.mMaxZoomIndex; + mZoomRatioList.addAll(src.mZoomRatioList); + mMaxZoomRatio = src.mMaxZoomRatio; + mHorizontalViewAngle = src.mHorizontalViewAngle; + mVerticalViewAngle = src.mVerticalViewAngle; + mStringifier = src.mStringifier; + } + + public float getHorizontalViewAngle() { + return mHorizontalViewAngle; + } + + public float getVerticalViewAngle() { + return mVerticalViewAngle; + } + + /** + * @return the supported picture formats. See {@link android.graphics.ImageFormat}. + */ + public Set<Integer> getSupportedPhotoFormats() { + return new TreeSet<Integer>(mSupportedPhotoFormats); + } + + /** + * Gets the supported preview formats. + * @return The supported preview {@link android.graphics.ImageFormat}s. + */ + public Set<Integer> getSupportedPreviewFormats() { + return new TreeSet<Integer>(mSupportedPreviewFormats); + } + + /** + * Gets the supported picture sizes. + */ + public List<Size> getSupportedPhotoSizes() { + return new ArrayList<Size>(mSupportedPhotoSizes); + } + + /** + * @return The supported preview fps (frame-per-second) ranges. The returned + * list is sorted by maximum fps then minimum fps in a descending order. + * The values are multiplied by 1000. + */ + public final List<int[]> getSupportedPreviewFpsRange() { + return new ArrayList<int[]>(mSupportedPreviewFpsRange); + } + + /** + * @return The supported preview sizes. The list is sorted by width then + * height in a descending order. + */ + public final List<Size> getSupportedPreviewSizes() { + return new ArrayList<Size>(mSupportedPreviewSizes); + } + + public final Size getPreferredPreviewSizeForVideo() { + return new Size(mPreferredPreviewSizeForVideo); + } + + /** + * @return The supported video frame sizes that can be used by MediaRecorder. + * The list is sorted by width then height in a descending order. + */ + public final List<Size> getSupportedVideoSizes() { + return new ArrayList<Size>(mSupportedVideoSizes); + } + + /** + * @return The supported scene modes. + */ + public final Set<SceneMode> getSupportedSceneModes() { + return new HashSet<SceneMode>(mSupportedSceneModes); + } + + /** + * @return Whether the scene mode is supported. + */ + public final boolean supports(SceneMode scene) { + return (scene != null && mSupportedSceneModes.contains(scene)); + } + + public boolean supports(final CameraSettings settings) { + if (zoomCheck(settings) && exposureCheck(settings) && focusCheck(settings) && + flashCheck(settings) && photoSizeCheck(settings) && previewSizeCheck(settings) && + videoStabilizationCheck(settings)) { + return true; + } + return false; + } + + /** + * @return The supported flash modes. + */ + public final Set<FlashMode> getSupportedFlashModes() { + return new HashSet<FlashMode>(mSupportedFlashModes); + } + + /** + * @return Whether the flash mode is supported. + */ + public final boolean supports(FlashMode flash) { + return (flash != null && mSupportedFlashModes.contains(flash)); + } + + /** + * @return The supported focus modes. + */ + public final Set<FocusMode> getSupportedFocusModes() { + return new HashSet<FocusMode>(mSupportedFocusModes); + } + + /** + * @return Whether the focus mode is supported. + */ + public final boolean supports(FocusMode focus) { + return (focus != null && mSupportedFocusModes.contains(focus)); + } + + /** + * @return The supported white balanceas. + */ + public final Set<WhiteBalance> getSupportedWhiteBalance() { + return new HashSet<WhiteBalance>(mSupportedWhiteBalances); + } + + /** + * @return Whether the white balance is supported. + */ + public boolean supports(WhiteBalance wb) { + return (wb != null && mSupportedWhiteBalances.contains(wb)); + } + + public final Set<Feature> getSupportedFeature() { + return new HashSet<Feature>(mSupportedFeatures); + } + + public boolean supports(Feature ft) { + return (ft != null && mSupportedFeatures.contains(ft)); + } + + /** + * @return The maximal supported zoom ratio. + */ + public float getMaxZoomRatio() { + return mMaxZoomRatio; + } + + // We'll replace these old style methods with new ones. + @Deprecated + public int getMaxZoomIndex() { + return mMaxZoomIndex; + } + + @Deprecated + public List<Integer> getZoomRatioList() { + return new ArrayList<Integer>(mZoomRatioList); + } + + /** + * @return The min exposure compensation index. The EV is the compensation + * index multiplied by the step value. If unsupported, both this method and + * {@link #getMaxExposureCompensation()} return 0. + */ + public final int getMinExposureCompensation() { + return mMinExposureCompensation; + } + + /** + * @return The max exposure compensation index. The EV is the compensation + * index multiplied by the step value. If unsupported, both this method and + * {@link #getMinExposureCompensation()} return 0. + */ + public final int getMaxExposureCompensation() { + return mMaxExposureCompensation; + } + + /** + * @return The exposure compensation step. The EV is the compensation index + * multiplied by the step value. + */ + public final float getExposureCompensationStep() { + return mExposureCompensationStep; + } + + /** + * @return The max number of faces supported by the face detection. 0 if + * unsupported. + */ + public final int getMaxNumOfFacesSupported() { + return mMaxNumOfFacesSupported; + } + + /** + * @return The stringifier used by this instance. + */ + public Stringifier getStringifier() { + return mStringifier; + } + + private boolean zoomCheck(final CameraSettings settings) { + final float ratio = settings.getCurrentZoomRatio(); + final int index = settings.getCurrentZoomIndex(); + if (!supports(Feature.ZOOM)) { + if (ratio != 1.0f || index != 0) { + Log.v(TAG, "Zoom is not supported"); + return false; + } + } else { + if (settings.getCurrentZoomRatio() > getMaxZoomRatio() || + index > getMaxZoomIndex()) { + Log.v(TAG, "Zoom ratio is not supported: ratio = " + + settings.getCurrentZoomRatio() + ", index = " + index); + return false; + } + } + return true; + } + + private boolean exposureCheck(final CameraSettings settings) { + final int index = settings.getExposureCompensationIndex(); + if (index > getMaxExposureCompensation() || index < getMinExposureCompensation()) { + Log.v(TAG, "Exposure compensation index is not supported. Min = " + + getMinExposureCompensation() + ", max = " + getMaxExposureCompensation() + "," + + " setting = " + index); + return false; + } + return true; + } + + private boolean focusCheck(final CameraSettings settings) { + FocusMode focusMode = settings.getCurrentFocusMode(); + if (!supports(focusMode)) { + Log.v(TAG, + "Focus mode not supported:" + (focusMode != null ? focusMode.name() : "null")); + return false; + } + return true; + } + + private boolean flashCheck(final CameraSettings settings) { + FlashMode flashMode = settings.getCurrentFlashMode(); + if (!supports(flashMode)) { + Log.v(TAG, + "Flash mode not supported:" + (flashMode != null ? flashMode.name() : "null")); + return false; + } + return true; + } + + private boolean photoSizeCheck(final CameraSettings settings) { + Size photoSize = settings.getCurrentPhotoSize(); + if (mSupportedPhotoSizes.contains(photoSize)) { + return true; + } + Log.v(TAG, "Unsupported photo size:" + photoSize); + return false; + } + + private boolean previewSizeCheck(final CameraSettings settings) { + final Size previewSize = settings.getCurrentPreviewSize(); + if (mSupportedPreviewSizes.contains(previewSize)) { + return true; + } + Log.v(TAG, "Unsupported preview size:" + previewSize); + return false; + } + + private boolean videoStabilizationCheck(final CameraSettings settings) { + if (!settings.isVideoStabilizationEnabled() || supports(Feature.VIDEO_STABILIZATION)) { + return true; + } + Log.v(TAG, "Video stabilization is not supported"); + return false; + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilitiesFactory.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilitiesFactory.java new file mode 100644 index 0000000..6c2bc54 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraCapabilitiesFactory.java @@ -0,0 +1,34 @@ +/* + * 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.hardware.Camera; + +import com.android.ex.camera2.portability.debug.Log; + +public class CameraCapabilitiesFactory { + + private static Log.Tag TAG = new Log.Tag("CamCapabsFact"); + + public static CameraCapabilities createFrom(Camera.Parameters p) { + if (p == null) { + Log.w(TAG, "Null parameter passed in."); + return null; + } + return new AndroidCameraCapabilities(p); + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java new file mode 100644 index 0000000..72a641e --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java @@ -0,0 +1,210 @@ +/* + * 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.graphics.Matrix; +import android.graphics.RectF; + +import com.android.ex.camera2.portability.debug.Log; + +/** + * The device info for all attached cameras. + */ +public interface CameraDeviceInfo { + + static final int NO_DEVICE = -1; + + /** + * @param cameraId Which device to interrogate. + * @return The static characteristics of the specified device, or {@code null} on error. + */ + Characteristics getCharacteristics(int cameraId); + + /** + * @return The total number of the available camera devices. + */ + int getNumberOfCameras(); + + /** + * @return The first (lowest) ID of the back cameras or {@code NO_DEVICE} + * if not available. + */ + int getFirstBackCameraId(); + + /** + * @return The first (lowest) ID of the front cameras or {@code NO_DEVICE} + * if not available. + */ + int getFirstFrontCameraId(); + + /** + * Device characteristics for a single camera. + */ + public abstract class Characteristics { + private static final Log.Tag TAG = new Log.Tag("CamDvcInfChar"); + + /** + * @return Whether the camera faces the back of the device. + */ + public abstract boolean isFacingBack(); + + /** + * @return Whether the camera faces the device's screen. + */ + public abstract boolean isFacingFront(); + + /** + * @return The camera sensor orientation, or the counterclockwise angle + * from its natural position that the device must be held at + * for the sensor to be right side up (in degrees, always a + * multiple of 90, and between 0 and 270, inclusive). + */ + public abstract int getSensorOrientation(); + + /** + * @param currentDisplayOrientation + * The current display orientation, measured counterclockwise + * from to the device's natural orientation (in degrees, always + * a multiple of 90, and between 0 and 270, inclusive). + * @return + * The relative preview image orientation, or the clockwise + * rotation angle that must be applied to display preview + * frames in the matching orientation, accounting for implicit + * mirroring, if applicable (in degrees, always a multiple of + * 90, and between 0 and 270, inclusive). + */ + public int getPreviewOrientation(int currentDisplayOrientation) { + // Drivers tend to mirror the image during front camera preview. + return getRelativeImageOrientation(currentDisplayOrientation, true); + } + + /** + * @param currentDisplayOrientation + * The current display orientation, measured counterclockwise + * from to the device's natural orientation (in degrees, always + * a multiple of 90, and between 0 and 270, inclusive). + * @return + * The relative capture image orientation, or the clockwise + * rotation angle that must be applied to display these frames + * in the matching orientation (in degrees, always a multiple + * of 90, and between 0 and 270, inclusive). + */ + public int getJpegOrientation(int currentDisplayOrientation) { + // Don't mirror during capture! + return getRelativeImageOrientation(currentDisplayOrientation, false); + } + + /** + * @param currentDisplayOrientaiton + * {@link #getPreviewOrientation}, {@link #getJpegOrientation} + * @param compensateForMirroring + * Whether to account for mirroring in the case of front-facing + * cameras, which is necessary iff the OS/driver is + * automatically reflecting the image. + * @return + * {@link #getPreviewOrientation}, {@link #getJpegOrientation} + * + * @see android.hardware.Camera.setDisplayOrientation + */ + protected int getRelativeImageOrientation(int currentDisplayOrientation, + boolean compensateForMirroring) { + if (!orientationIsValid(currentDisplayOrientation)) { + return 0; + } + + int result = 0; + if (isFacingFront()) { + result = (getSensorOrientation() + currentDisplayOrientation) % 360; + if (compensateForMirroring) { + result = (360 - result) % 360; + } + } else if (isFacingBack()) { + result = (getSensorOrientation() - currentDisplayOrientation + 360) % 360; + } else { + Log.e(TAG, "Camera is facing unhandled direction"); + } + return result; + } + + /** + * @param currentDisplayOrientation + * The current display orientation, measured counterclockwise + * from to the device's natural orientation (in degrees, always + * a multiple of 90, and between 0 and 270, inclusive). + * @param surfaceDimensions + * The dimensions of the {@link android.view.Surface} on which + * the preview image is being rendered. It usually only makes + * sense for the upper-left corner to be at the origin. + * @return + * The transform matrix that should be applied to the + * {@link android.view.Surface} in order for the image to + * display properly in the device's current orientation. + */ + public Matrix getPreviewTransform(int currentDisplayOrientation, RectF surfaceDimensions) { + return getPreviewTransform(currentDisplayOrientation, surfaceDimensions, + new RectF(surfaceDimensions)); + } + + /** + * @param currentDisplayOrientation + * The current display orientation, measured counterclockwise + * from to the device's natural orientation (in degrees, always + * a multiple of 90, and between 0 and 270, inclusive). + * @param surfaceDimensions + * The dimensions of the {@link android.view.Surface} on which + * the preview image is being rendered. It usually only makes + * sense for the upper-left corner to be at the origin. + * @param desiredBounds + * The boundaries within the {@link android.view.Surface} where + * the final image should appear. These can be used to + * translate and scale the output, but note that the image will + * be stretched to fit, possibly changing its aspect ratio. + * @return + * The transform matrix that should be applied to the + * {@link android.view.Surface} in order for the image to + * display properly in the device's current orientation. + */ + public Matrix getPreviewTransform(int currentDisplayOrientation, RectF surfaceDimensions, + RectF desiredBounds) { + if (!orientationIsValid(currentDisplayOrientation) || + surfaceDimensions.equals(desiredBounds)) { + return new Matrix(); + } + + Matrix transform = new Matrix(); + transform.setRectToRect(surfaceDimensions, desiredBounds, Matrix.ScaleToFit.FILL); + return transform; + } + + /** + * @return Whether the shutter sound can be disabled. + */ + public abstract boolean canDisableShutterSound(); + + protected static boolean orientationIsValid(int angle) { + if (angle % 90 != 0) { + Log.e(TAG, "Provided display orientation is not divisible by 90"); + return false; + } + if (angle < 0 || angle > 270) { + Log.e(TAG, "Provided display orientation is outside expected range"); + return false; + } + return true; + } + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java new file mode 100644 index 0000000..26d0f85 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java @@ -0,0 +1,471 @@ +/* + * 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.hardware.Camera; + +import com.android.ex.camera2.portability.debug.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * A class which stores the camera settings. + */ +public abstract class CameraSettings { + private static final Log.Tag TAG = new Log.Tag("CamSet"); + + // Attempts to provide a value outside this range will be ignored. + private static final int MIN_JPEG_COMPRESSION_QUALITY = 1; + private static final int MAX_JPEG_COMPRESSION_QUALITY = 100; + + protected final Map<String, String> mGeneralSetting = new TreeMap<>(); + protected final List<Camera.Area> mMeteringAreas = new ArrayList<>(); + protected final List<Camera.Area> mFocusAreas = new ArrayList<>(); + protected int mPreviewFpsRangeMin; + protected int mPreviewFpsRangeMax; + protected int mPreviewFrameRate; + protected Size mCurrentPreviewSize; + private int mCurrentPreviewFormat; + protected Size mCurrentPhotoSize; + protected byte mJpegCompressQuality; + protected int mCurrentPhotoFormat; + protected float mCurrentZoomRatio; + protected int mCurrentZoomIndex; + protected int mExposureCompensationIndex; + protected CameraCapabilities.FlashMode mCurrentFlashMode; + protected CameraCapabilities.FocusMode mCurrentFocusMode; + protected CameraCapabilities.SceneMode mCurrentSceneMode; + protected CameraCapabilities.WhiteBalance mWhiteBalance; + protected boolean mVideoStabilizationEnabled; + protected boolean mAutoExposureLocked; + protected boolean mAutoWhiteBalanceLocked; + protected boolean mRecordingHintEnabled; + protected GpsData mGpsData; + protected Size mExifThumbnailSize = new Size(0,0); + + /** + * An immutable class storing GPS related information. + * <p>It's a hack since we always use GPS time stamp but does not use other + * fields sometimes. Setting processing method to null means the other + * fields should not be used.</p> + */ + public static class GpsData { + public final double latitude; + public final double longitude; + public final double altitude; + public final long timeStamp; + public final String processingMethod; + + /** Constructor. */ + public GpsData(double latitude, double longitude, double altitude, long timeStamp, + String processingMethod) { + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + this.timeStamp = timeStamp; + this.processingMethod = processingMethod; + } + + /** Copy constructor. */ + public GpsData(GpsData src) { + this.latitude = src.latitude; + this.longitude = src.longitude; + this.altitude = src.altitude; + this.timeStamp = src.timeStamp; + this.processingMethod = src.processingMethod; + } + } + + protected CameraSettings() { + } + + /** + * Copy constructor. + * + * @param src The source settings. + * @return The copy of the source. + */ + protected CameraSettings(CameraSettings src) { + mGeneralSetting.putAll(src.mGeneralSetting); + mMeteringAreas.addAll(src.mMeteringAreas); + mFocusAreas.addAll(src.mFocusAreas); + mPreviewFpsRangeMin = src.mPreviewFpsRangeMin; + mPreviewFpsRangeMax = src.mPreviewFpsRangeMax; + mPreviewFrameRate = src.mPreviewFrameRate; + mCurrentPreviewSize = + (src.mCurrentPreviewSize == null ? null : new Size(src.mCurrentPreviewSize)); + mCurrentPreviewFormat = src.mCurrentPreviewFormat; + mCurrentPhotoSize = + (src.mCurrentPhotoSize == null ? null : new Size(src.mCurrentPhotoSize)); + mJpegCompressQuality = src.mJpegCompressQuality; + mCurrentPhotoFormat = src.mCurrentPhotoFormat; + mCurrentZoomRatio = src.mCurrentZoomRatio; + mCurrentZoomIndex = src.mCurrentZoomIndex; + mExposureCompensationIndex = src.mExposureCompensationIndex; + mCurrentFlashMode = src.mCurrentFlashMode; + mCurrentFocusMode = src.mCurrentFocusMode; + mCurrentSceneMode = src.mCurrentSceneMode; + mWhiteBalance = src.mWhiteBalance; + mVideoStabilizationEnabled = src.mVideoStabilizationEnabled; + mAutoExposureLocked = src.mAutoExposureLocked; + mAutoWhiteBalanceLocked = src.mAutoWhiteBalanceLocked; + mRecordingHintEnabled = src.mRecordingHintEnabled; + mGpsData = src.mGpsData; + mExifThumbnailSize = src.mExifThumbnailSize; + } + + /** + * @return A copy of this object, as an instance of the implementing class. + */ + public abstract CameraSettings copy(); + + /** General setting **/ + @Deprecated + public void setSetting(String key, String value) { + mGeneralSetting.put(key, value); + } + + /** Preview **/ + + /** + * Sets the preview FPS range. This call will invalidate prior calls to + * {@link #setPreviewFrameRate(int)}. + * + * @param min The min FPS. + * @param max The max FPS. + */ + public void setPreviewFpsRange(int min, int max) { + if (min > max) { + int temp = max; + max = min; + min = temp; + } + mPreviewFpsRangeMax = max; + mPreviewFpsRangeMin = min; + mPreviewFrameRate = -1; + } + + /** + * @return The min of the preview FPS range. + */ + public int getPreviewFpsRangeMin() { + return mPreviewFpsRangeMin; + } + + /** + * @return The max of the preview FPS range. + */ + public int getPreviewFpsRangeMax() { + return mPreviewFpsRangeMax; + } + + /** + * Sets the preview FPS. This call will invalidate prior calls to + * {@link #setPreviewFpsRange(int, int)}. + * + * @param frameRate The target frame rate. + */ + public void setPreviewFrameRate(int frameRate) { + if (frameRate > 0) { + mPreviewFrameRate = frameRate; + mPreviewFpsRangeMax = frameRate; + mPreviewFpsRangeMin = frameRate; + } + } + + public int getPreviewFrameRate() { + return mPreviewFrameRate; + } + + /** + * @return The current preview size. + */ + public Size getCurrentPreviewSize() { + return new Size(mCurrentPreviewSize); + } + + /** + * @param previewSize The size to use for preview. + */ + public void setPreviewSize(Size previewSize) { + mCurrentPreviewSize = new Size(previewSize); + } + + /** + * Sets the preview format. + * + * @param format + * @see {@link android.graphics.ImageFormat}. + */ + public void setPreviewFormat(int format) { + mCurrentPreviewFormat = format; + } + + /** + * @return The preview format. + * @see {@link android.graphics.ImageFormat}. + */ + public int getCurrentPreviewFormat() { + return mCurrentPreviewFormat; + } + + /** Picture **/ + + /** + * @return The current photo size. + */ + public Size getCurrentPhotoSize() { + return new Size(mCurrentPhotoSize); + } + + /** + * Sets the size for the photo. + * + * @param photoSize The photo size. + */ + public void setPhotoSize(Size photoSize) { + mCurrentPhotoSize = new Size(photoSize); + } + + /** + * Sets the format for the photo. + * + * @param format The format for the photos taken. + * @see {@link android.graphics.ImageFormat}. + */ + public void setPhotoFormat(int format) { + mCurrentPhotoFormat = format; + } + + /** + * @return The format for the photos taken. + * @see {@link android.graphics.ImageFormat}. + */ + public int getCurrentPhotoFormat() { + return mCurrentPhotoFormat; + } + + /** + * Sets the JPEG compression quality. + * + * @param quality The quality for JPEG. + */ + public void setPhotoJpegCompressionQuality(int quality) { + if (quality < MIN_JPEG_COMPRESSION_QUALITY || quality > MAX_JPEG_COMPRESSION_QUALITY) { + Log.w(TAG, "Ignoring JPEG quality that falls outside the expected range"); + return; + } + // This is safe because the positive numbers go up to 127. + mJpegCompressQuality = (byte) quality; + } + + public int getPhotoJpegCompressionQuality() { + return mJpegCompressQuality; + } + + /** Zoom **/ + + /** + * @return The current zoom ratio. The min is 1.0f. + */ + public float getCurrentZoomRatio() { + return mCurrentZoomRatio; + } + + /** + * Sets the zoom ratio. + * @param ratio The new zoom ratio. Should be in the range between 1.0 to + * the value returned from {@link + * com.android.camera.cameradevice.CameraCapabilities#getMaxZoomRatio()}. + * @throws java.lang.UnsupportedOperationException if the ratio is not + * supported. + */ + public void setZoomRatio(float ratio) { + mCurrentZoomRatio = ratio; + } + + @Deprecated + public int getCurrentZoomIndex() { + return mCurrentZoomIndex; + } + + @Deprecated + public void setZoomIndex(int index) { + mCurrentZoomIndex = index; + } + + /** Exposure **/ + + public void setExposureCompensationIndex(int index) { + mExposureCompensationIndex = index; + } + + /** + * @return The exposure compensation, with 0 meaning unadjusted. + */ + public int getExposureCompensationIndex() { + return mExposureCompensationIndex; + } + + public void setAutoExposureLock(boolean locked) { + mAutoExposureLocked = locked; + } + + public boolean isAutoExposureLocked() { + return mAutoExposureLocked; + } + + public void setMeteringAreas(List<Camera.Area> areas) { + mMeteringAreas.clear(); + if (areas != null) { + mMeteringAreas.addAll(areas); + } + } + + public List<Camera.Area> getMeteringAreas() { + return new ArrayList<Camera.Area>(mMeteringAreas); + } + + /** Flash **/ + + public CameraCapabilities.FlashMode getCurrentFlashMode() { + return mCurrentFlashMode; + } + + public void setFlashMode(CameraCapabilities.FlashMode flashMode) { + mCurrentFlashMode = flashMode; + } + + /** Focus **/ + + /** + * Sets the focus mode. + * @param focusMode The focus mode to use. + */ + public void setFocusMode(CameraCapabilities.FocusMode focusMode) { + mCurrentFocusMode = focusMode; + } + + /** + * @return The current focus mode. + */ + public CameraCapabilities.FocusMode getCurrentFocusMode() { + return mCurrentFocusMode; + } + + /** + * @param areas The areas to focus. + */ + public void setFocusAreas(List<Camera.Area> areas) { + mFocusAreas.clear(); + if (areas != null) { + mFocusAreas.addAll(areas); + } + } + + public List<Camera.Area> getFocusAreas() { + return new ArrayList<Camera.Area>(mFocusAreas); + } + + /** White balance **/ + + public void setWhiteBalance(CameraCapabilities.WhiteBalance whiteBalance) { + mWhiteBalance = whiteBalance; + } + + public CameraCapabilities.WhiteBalance getWhiteBalance() { + return mWhiteBalance; + } + + public void setAutoWhiteBalanceLock(boolean locked) { + mAutoWhiteBalanceLocked = locked; + } + + public boolean isAutoWhiteBalanceLocked() { + return mAutoWhiteBalanceLocked; + } + + /** Scene mode **/ + + /** + * @return The current scene mode. + */ + public CameraCapabilities.SceneMode getCurrentSceneMode() { + return mCurrentSceneMode; + } + + /** + * Sets the scene mode for capturing. + * + * @param sceneMode The scene mode to use. + * @throws java.lang.UnsupportedOperationException if it's not supported. + */ + public void setSceneMode(CameraCapabilities.SceneMode sceneMode) { + mCurrentSceneMode = sceneMode; + } + + /** Other Features **/ + + public void setVideoStabilization(boolean enabled) { + mVideoStabilizationEnabled = enabled; + } + + public boolean isVideoStabilizationEnabled() { + return mVideoStabilizationEnabled; + } + + public void setRecordingHintEnabled(boolean hintEnabled) { + mRecordingHintEnabled = hintEnabled; + } + + public boolean isRecordingHintEnabled() { + return mRecordingHintEnabled; + } + + public void setGpsData(GpsData data) { + mGpsData = new GpsData(data); + } + + public GpsData getGpsData() { + return (mGpsData == null ? null : new GpsData(mGpsData)); + } + + public void clearGpsData() { + mGpsData = null; + } + + /** + * Sets the size of the thumbnail in EXIF header. + * + * @param s The size for the thumbnail. {@code null} will clear the size to + * (0,0). + */ + public void setExifThumbnailSize(Size s) { + if (s != null) { + mExifThumbnailSize = s; + } else { + mExifThumbnailSize = new Size(0,0); + } + } + + public Size getExifThumbnailSize() { + return new Size(mExifThumbnailSize); + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraStateHolder.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraStateHolder.java new file mode 100644 index 0000000..35ae51c --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraStateHolder.java @@ -0,0 +1,110 @@ +/* + * 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.os.SystemClock; + +import com.android.ex.camera2.portability.debug.Log; + +public abstract class CameraStateHolder { + private static final Log.Tag TAG = new Log.Tag("CamStateHolder"); + + private int mState; + + public CameraStateHolder(int state) { + setState(state); + } + + public synchronized void setState(int state) { + mState = state; + this.notifyAll(); + } + + public synchronized int getState() { + return mState; + } + + private static interface ConditionChecker { + /** + * @return Whether the condition holds. + */ + boolean success(); + } + + /** + * A helper method used by {@link #waitToAvoidStates(int)} and + * {@link #waitForStates(int)}. This method will wait until the + * condition is successful. + * + * @param stateChecker The state checker to be used. + * @param timeoutMs The timeout limit in milliseconds. + * @return {@code false} if the wait is interrupted or timeout limit is + * reached. + */ + private boolean waitForCondition(ConditionChecker stateChecker, + long timeoutMs) { + long timeBound = SystemClock.uptimeMillis() + timeoutMs; + synchronized (this) { + while (!stateChecker.success()) { + try { + this.wait(timeoutMs); + } catch (InterruptedException ex) { + if (SystemClock.uptimeMillis() > timeBound) { + // Timeout. + Log.w(TAG, "Timeout waiting."); + } + return false; + } + } + } + return true; + } + + /** + * Block the current thread until the state becomes one of the + * specified. + * + * @param states Expected states. + * @return {@code false} if the wait is interrupted or timeout limit is + * reached. + */ + public boolean waitForStates(final int states) { + return waitForCondition(new ConditionChecker() { + @Override + public boolean success() { + return (states | getState()) == states; + } + }, CameraAgent.CAMERA_OPERATION_TIMEOUT_MS); + } + + /** + * Block the current thread until the state becomes NOT one of the + * specified. + * + * @param states States to avoid. + * @return {@code false} if the wait is interrupted or timeout limit is + * reached. + */ + public boolean waitToAvoidStates(final int states) { + return waitForCondition(new ConditionChecker() { + @Override + public boolean success() { + return (states & getState()) == 0; + } + }, CameraAgent.CAMERA_OPERATION_TIMEOUT_MS); + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/DispatchThread.java b/camera2/portability/src/com/android/ex/camera2/portability/DispatchThread.java new file mode 100644 index 0000000..bc77259 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/DispatchThread.java @@ -0,0 +1,155 @@ +/* + * 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.os.Handler; +import android.os.HandlerThread; +import android.os.SystemClock; + +import com.android.ex.camera2.portability.debug.Log; + +import java.util.LinkedList; +import java.util.Queue; + +public class DispatchThread extends Thread { + private static final Log.Tag TAG = new Log.Tag("DispatchThread"); + private static final long MAX_MESSAGE_QUEUE_LENGTH = 256; + + private final Queue<Runnable> mJobQueue; + private Boolean mIsEnded; + private Handler mCameraHandler; + private HandlerThread mCameraHandlerThread; + + public DispatchThread(Handler cameraHandler, HandlerThread cameraHandlerThread) { + super("Camera Job Dispatch Thread"); + mJobQueue = new LinkedList<Runnable>(); + mIsEnded = new Boolean(false); + mCameraHandler = cameraHandler; + mCameraHandlerThread = cameraHandlerThread; + } + + /** + * Queues up the job. + * + * @param job The job to run. + */ + public void runJob(Runnable job) { + if (isEnded()) { + throw new IllegalStateException( + "Trying to run job on interrupted dispatcher thread"); + } + synchronized (mJobQueue) { + if (mJobQueue.size() == MAX_MESSAGE_QUEUE_LENGTH) { + throw new RuntimeException("Camera master thread job queue full"); + } + + mJobQueue.add(job); + mJobQueue.notifyAll(); + } + } + + /** + * Queues up the job and wait for it to be done. + * + * @param job The job to run. + * @param timeoutMs Timeout limit in milliseconds. + * @param jobMsg The message to log when the job runs timeout. + * @return Whether the job finishes before timeout. + */ + public void runJobSync(final Runnable job, Object waitLock, long timeoutMs, String jobMsg) { + String timeoutMsg = "Timeout waiting " + timeoutMs + "ms for " + jobMsg; + synchronized (waitLock) { + long timeoutBound = SystemClock.uptimeMillis() + timeoutMs; + try { + runJob(job); + waitLock.wait(timeoutMs); + if (SystemClock.uptimeMillis() > timeoutBound) { + throw new IllegalStateException(timeoutMsg); + } + } catch (InterruptedException ex) { + if (SystemClock.uptimeMillis() > timeoutBound) { + throw new IllegalStateException(timeoutMsg); + } + } + } + } + + /** + * Gracefully ends this thread. Will stop after all jobs are processed. + */ + public void end() { + synchronized (mIsEnded) { + mIsEnded = true; + } + synchronized(mJobQueue) { + mJobQueue.notifyAll(); + } + } + + private boolean isEnded() { + synchronized (mIsEnded) { + return mIsEnded; + } + } + + @Override + public void run() { + while(true) { + Runnable job = null; + synchronized (mJobQueue) { + while (mJobQueue.size() == 0 && !isEnded()) { + try { + mJobQueue.wait(); + } catch (InterruptedException ex) { + Log.w(TAG, "Dispatcher thread wait() interrupted, exiting"); + break; + } + } + + job = mJobQueue.poll(); + } + + if (job == null) { + // mJobQueue.poll() returning null means wait() is + // interrupted and the queue is empty. + if (isEnded()) { + break; + } + continue; + } + + job.run(); + + synchronized (DispatchThread.this) { + mCameraHandler.post(new Runnable() { + @Override + public void run() { + synchronized (DispatchThread.this) { + DispatchThread.this.notifyAll(); + } + } + }); + try { + DispatchThread.this.wait(); + } catch (InterruptedException ex) { + // TODO: do something here. + } + } + } + mCameraHandlerThread.quitSafely(); + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/HistoryHandler.java b/camera2/portability/src/com/android/ex/camera2/portability/HistoryHandler.java new file mode 100644 index 0000000..ec2a555 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/HistoryHandler.java @@ -0,0 +1,58 @@ +/* + * 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.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.util.LinkedList; + +class HistoryHandler extends Handler { + private static final int MAX_HISTORY_SIZE = 400; + + final LinkedList<Integer> mMsgHistory; + + HistoryHandler(Looper looper) { + super(looper); + mMsgHistory = new LinkedList<Integer>(); + // We add a -1 at the beginning to mark the very beginning of the + // history. + mMsgHistory.offerLast(-1); + } + + String generateHistoryString(int cameraId) { + String info = new String("HIST"); + info += "_ID" + cameraId; + for (Integer msg : mMsgHistory) { + info = info + '_' + msg.toString(); + } + info += "_HEND"; + return info; + } + + /** + * Subclasses' implementations should call this one before doing their work. + */ + @Override + public void handleMessage(Message msg) { + mMsgHistory.offerLast(msg.what); + while (mMsgHistory.size() > MAX_HISTORY_SIZE) { + mMsgHistory.pollFirst(); + } + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/Size.java b/camera2/portability/src/com/android/ex/camera2/portability/Size.java new file mode 100644 index 0000000..042c443 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/Size.java @@ -0,0 +1,178 @@ +/* + * 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.graphics.Point; +import android.hardware.Camera; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * An immutable simple size container. + */ +public class Size { + public static final String DELIMITER = ","; + + /** + * An helper method to build a list of this class from a list of + * {@link android.hardware.Camera.Size}. + * + * @param cameraSizes Source. + * @return The built list. + */ + public static List<Size> buildListFromCameraSizes(List<Camera.Size> cameraSizes) { + ArrayList<Size> list = new ArrayList<Size>(cameraSizes.size()); + for (Camera.Size cameraSize : cameraSizes) { + list.add(new Size(cameraSize)); + } + return list; + } + + /** + * A helper method to build a list of this class from a list of {@link android.util.Size}. + * + * @param cameraSizes Source. + * @return The built list. + */ + public static List<Size> buildListFromAndroidSizes(List<android.util.Size> androidSizes) { + ArrayList<Size> list = new ArrayList<Size>(androidSizes.size()); + for (android.util.Size androidSize : androidSizes) { + list.add(new Size(androidSize)); + } + return list; + } + + /** + * Encode List of this class as comma-separated list of integers. + * + * @param sizes List of this class to encode. + * @return encoded string. + */ + public static String listToString(List<Size> sizes) { + ArrayList<Integer> flatSizes = new ArrayList<>(); + for (Size s : sizes) { + flatSizes.add(s.width()); + flatSizes.add(s.height()); + } + return TextUtils.join(DELIMITER, flatSizes); + } + + /** + * Decode comma-separated even-length list of integers into a List of this class. + * + * @param encodedSizes encoded string. + * @return List of this class. + */ + public static List<Size> stringToList(String encodedSizes) { + String[] flatSizes = TextUtils.split(encodedSizes, DELIMITER); + ArrayList<Size> list = new ArrayList<>(); + for (int i = 0; i < flatSizes.length; i += 2) { + int width = Integer.parseInt(flatSizes[i]); + int height = Integer.parseInt(flatSizes[i + 1]); + list.add(new Size(width,height)); + } + return list; + } + + private final Point val; + + /** + * Constructor. + */ + public Size(int width, int height) { + val = new Point(width, height); + } + + /** + * Copy constructor. + */ + public Size(Size other) { + if (other == null) { + val = new Point(0, 0); + } else { + val = new Point(other.width(), other.height()); + } + } + + /** + * Constructor from a source {@link android.hardware.Camera.Size}. + * + * @param other The source size. + */ + public Size(Camera.Size other) { + if (other == null) { + val = new Point(0, 0); + } else { + val = new Point(other.width, other.height); + } + } + + /** + * Constructor from a source {@link android.util.Size}. + * + * @param other The source size. + */ + public Size(android.util.Size other) { + if (other == null) { + val = new Point(0, 0); + } else { + val = new Point(other.getWidth(), other.getHeight()); + } + } + + /** + * Constructor from a source {@link android.graphics.Point}. + * + * @param p The source size. + */ + public Size(Point p) { + if (p == null) { + val = new Point(0, 0); + } else { + val = new Point(p); + } + } + + public int width() { + return val.x; + } + + public int height() { + return val.y; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Size) { + Size other = (Size) o; + return val.equals(other.val); + } + return false; + } + + @Override + public int hashCode() { + return val.hashCode(); + } + + @Override + public String toString() { + return "Size: (" + this.width() + " x " + this.height() + ")"; + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/debug/Log.java b/camera2/portability/src/com/android/ex/camera2/portability/debug/Log.java new file mode 100644 index 0000000..07e758e --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/debug/Log.java @@ -0,0 +1,135 @@ +/* + * 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.debug; + +public class Log { + /** + * All Camera logging using this class will use this tag prefix. + * Additionally, the prefix itself is checked in isLoggable and + * serves as an override. So, to toggle all logs allowed by the + * current {@link Configuration}, you can set properties: + * + * adb shell setprop log.tag.CAM2PORT_ VERBOSE + * adb shell setprop log.tag.CAM2PORT_ "" + */ + public static final String CAMERA_LOGTAG_PREFIX = "CAM2PORT_"; + private static final Log.Tag TAG = new Log.Tag("Log"); + + /** + * This class restricts the length of the log tag to be less than the + * framework limit and also prepends the common tag prefix defined by + * {@code CAMERA_LOGTAG_PREFIX}. + */ + public static final class Tag { + + // The length limit from Android framework is 23. + private static final int MAX_TAG_LEN = 23 - CAMERA_LOGTAG_PREFIX.length(); + + final String mValue; + + public Tag(String tag) { + final int lenDiff = tag.length() - MAX_TAG_LEN; + if (lenDiff > 0) { + w(TAG, "Tag " + tag + " is " + lenDiff + " chars longer than limit."); + } + mValue = CAMERA_LOGTAG_PREFIX + (lenDiff > 0 ? tag.substring(0, MAX_TAG_LEN) : tag); + } + + @Override + public String toString() { + return mValue; + } + } + + public static void d(Tag tag, String msg) { + if (isLoggable(tag, android.util.Log.DEBUG)) { + android.util.Log.d(tag.toString(), msg); + } + } + + public static void d(Tag tag, String msg, Throwable tr) { + if (isLoggable(tag, android.util.Log.DEBUG)) { + android.util.Log.d(tag.toString(), msg, tr); + } + } + + public static void e(Tag tag, String msg) { + if (isLoggable(tag, android.util.Log.ERROR)) { + android.util.Log.e(tag.toString(), msg); + } + } + + public static void e(Tag tag, String msg, Throwable tr) { + if (isLoggable(tag, android.util.Log.ERROR)) { + android.util.Log.e(tag.toString(), msg, tr); + } + } + + public static void i(Tag tag, String msg) { + if (isLoggable(tag, android.util.Log.INFO)) { + android.util.Log.i(tag.toString(), msg); + } + } + + public static void i(Tag tag, String msg, Throwable tr) { + if (isLoggable(tag, android.util.Log.INFO)) { + android.util.Log.i(tag.toString(), msg, tr); + } + } + + public static void v(Tag tag, String msg) { + if (isLoggable(tag, android.util.Log.VERBOSE)) { + android.util.Log.v(tag.toString(), msg); + } + } + + public static void v(Tag tag, String msg, Throwable tr) { + if (isLoggable(tag, android.util.Log.VERBOSE)) { + android.util.Log.v(tag.toString(), msg, tr); + } + } + + public static void w(Tag tag, String msg) { + if (isLoggable(tag, android.util.Log.WARN)) { + android.util.Log.w(tag.toString(), msg); + } + } + + public static void w(Tag tag, String msg, Throwable tr) { + if (isLoggable(tag, android.util.Log.WARN)) { + android.util.Log.w(tag.toString(), msg, tr); + } + } + + private static boolean isLoggable(Tag tag, int level) { + try { + if (LogHelper.getOverrideLevel() != 0) { + // Override system log level and output. VERBOSE is smaller than + // ERROR, so the comparison checks that the override value is smaller + // than the desired output level. This applies to all tags. + return LogHelper.getOverrideLevel() <= level; + } else { + // The prefix can be used as an override tag to see all camera logs + return android.util.Log.isLoggable(CAMERA_LOGTAG_PREFIX, level) + || android.util.Log.isLoggable(tag.toString(), level); + } + } catch (IllegalArgumentException ex) { + e(TAG, "Tag too long:" + tag); + return false; + } + } +} diff --git a/camera2/portability/src/com/android/ex/camera2/portability/debug/LogHelper.java b/camera2/portability/src/com/android/ex/camera2/portability/debug/LogHelper.java new file mode 100644 index 0000000..3ec0260 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/debug/LogHelper.java @@ -0,0 +1,32 @@ +/* + * 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.debug; + +import android.content.Context; + +public class LogHelper { + public static void initialize(Context context) { + // Do nothing. + } + + /** + * Return a valid log level from {@link android.util.Log} to override + * the system log level. Return 0 to instead defer to system log level. + */ + public static int getOverrideLevel() { + return 0; + } +}
\ No newline at end of file diff --git a/camera2/portability/src/com/android/ex/camera2/portability/util/SystemProperties.java b/camera2/portability/src/com/android/ex/camera2/portability/util/SystemProperties.java new file mode 100644 index 0000000..81493f3 --- /dev/null +++ b/camera2/portability/src/com/android/ex/camera2/portability/util/SystemProperties.java @@ -0,0 +1,51 @@ +/* + * 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.util; + +import com.android.ex.camera2.portability.debug.Log; + +import java.lang.reflect.Method; + +/** + * Mirrors hidden class {@link android.os.SystemProperties} (available since API Level 1). + */ +public final class SystemProperties { + private static final Log.Tag TAG = new Log.Tag("SysProps"); + + /** + * Gets system properties set by <code>adb shell setprop <em>key</em> <em>value</em></code> + * + * @param key the property key. + * @param defaultValue the value to return if the property is undefined or empty (this parameter + * may be {@code null}). + * @return the system property value or the default value. + */ + public static String get(String key, String defaultValue) { + try { + final Class<?> systemProperties = Class.forName("android.os.SystemProperties"); + final Method get = systemProperties.getMethod("get", String.class, String.class); + return (String) get.invoke(null, key, defaultValue); + } catch (Exception e) { + // This should never happen + Log.e(TAG, "Exception while getting system property: ", e); + return defaultValue; + } + } + + private SystemProperties() { + } +} diff --git a/camera2/portability/tests/Android.mk b/camera2/portability/tests/Android.mk new file mode 100644 index 0000000..f0b24e0 --- /dev/null +++ b/camera2/portability/tests/Android.mk @@ -0,0 +1,25 @@ +# 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PACKAGE_NAME := android-ex-camera2-portability-tests +LOCAL_MODULE_TAGS := tests +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under,src) $(call all-java-files-under,../../utils/tests) +LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2-portability android-ex-camera2-utils \ + android-support-test mockito-target + +include $(BUILD_PACKAGE) diff --git a/camera2/portability/tests/AndroidManifest.xml b/camera2/portability/tests/AndroidManifest.xml new file mode 100644 index 0000000..65cf709 --- /dev/null +++ b/camera2/portability/tests/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.ex.camera2.portability.tests"> + <uses-permission android:name="android.permission.CAMERA" /> + <application> + <uses-library android:name="android.test.runner" /> + </application> + <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.ex.camera2.portability.tests" /> +</manifest> diff --git a/camera2/portability/tests/src/com/android/ex/camera2/portability/Camera2PortabilityTest.java b/camera2/portability/tests/src/com/android/ex/camera2/portability/Camera2PortabilityTest.java new file mode 100644 index 0000000..034fac7 --- /dev/null +++ b/camera2/portability/tests/src/com/android/ex/camera2/portability/Camera2PortabilityTest.java @@ -0,0 +1,208 @@ +/* + * 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 static android.hardware.camera2.CaptureRequest.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.support.test.InjectContext; + +import com.android.ex.camera2.portability.CameraCapabilities.FlashMode; +import com.android.ex.camera2.portability.CameraCapabilities.FocusMode; +import com.android.ex.camera2.portability.CameraCapabilities.SceneMode; +import com.android.ex.camera2.portability.CameraCapabilities.Stringifier; +import com.android.ex.camera2.portability.CameraCapabilities.WhiteBalance; +import com.android.ex.camera2.utils.Camera2DeviceTester; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; + +public class Camera2PortabilityTest extends Camera2DeviceTester { + @Test + public void cameraCapabilitiesStringifier() { + Stringifier strfy = new Stringifier(); + for(FocusMode val : FocusMode.values()) { + assertEquals(val, strfy.focusModeFromString(strfy.stringify(val))); + } + for(FlashMode val : FlashMode.values()) { + assertEquals(val, strfy.flashModeFromString(strfy.stringify(val))); + } + for(SceneMode val : SceneMode.values()) { + assertEquals(val, strfy.sceneModeFromString(strfy.stringify(val))); + } + for(WhiteBalance val : WhiteBalance.values()) { + assertEquals(val, strfy.whiteBalanceFromString(strfy.stringify(val))); + } + } + + @Test + public void cameraCapabilitiesStringifierNull() { + Stringifier strfy = new Stringifier(); + assertEquals(strfy.focusModeFromString(null), FocusMode.AUTO); + assertEquals(strfy.flashModeFromString(null), FlashMode.NO_FLASH); + assertEquals(strfy.sceneModeFromString(null), SceneMode.NO_SCENE_MODE); + assertEquals(strfy.whiteBalanceFromString(null), WhiteBalance.AUTO); + } + + @Test + public void cameraCapabilitiesStringifierInvalid() { + Stringifier strfy = new Stringifier(); + assertEquals(strfy.focusModeFromString("crap"), FocusMode.AUTO); + assertEquals(strfy.flashModeFromString("crap"), FlashMode.NO_FLASH); + assertEquals(strfy.sceneModeFromString("crap"), SceneMode.NO_SCENE_MODE); + assertEquals(strfy.whiteBalanceFromString("crap"), WhiteBalance.AUTO); + } + + private CameraCharacteristics buildFrameworkCharacteristics() throws CameraAccessException { + CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + String id = manager.getCameraIdList()[0]; + return manager.getCameraCharacteristics(id); + } + + private void camera2SettingsCheckSingleOption(AndroidCamera2Settings setts, + Key<?> apiKey, int apiVal) { + assertEquals(apiVal, setts.getRequestSettings().get(apiKey)); + } + + @Test + public void camera2SettingsSetOptionsAndGetRequestSettings() throws CameraAccessException { + AndroidCamera2Settings set = new AndroidCamera2Settings( + mCamera, CameraDevice.TEMPLATE_PREVIEW, null, null, null); + + // Focus modes + set.setFocusMode(FocusMode.AUTO); + camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_AUTO); + set.setFocusMode(FocusMode.CONTINUOUS_PICTURE); + camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_CONTINUOUS_PICTURE); + set.setFocusMode(FocusMode.CONTINUOUS_VIDEO); + camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_CONTINUOUS_VIDEO); + set.setFocusMode(FocusMode.EXTENDED_DOF); + camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_EDOF); + set.setFocusMode(FocusMode.FIXED); + camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_OFF); + set.setFocusMode(FocusMode.MACRO); + camera2SettingsCheckSingleOption(set, CONTROL_AF_MODE, CONTROL_AF_MODE_MACRO); + + // Scene modes + set.setSceneMode(SceneMode.AUTO); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_DISABLED); + set.setSceneMode(SceneMode.ACTION); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_ACTION); + set.setSceneMode(SceneMode.BARCODE); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_BARCODE); + set.setSceneMode(SceneMode.BEACH); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_BEACH); + set.setSceneMode(SceneMode.CANDLELIGHT); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_CANDLELIGHT); + set.setSceneMode(SceneMode.FIREWORKS); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_FIREWORKS); + set.setSceneMode(SceneMode.LANDSCAPE); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_LANDSCAPE); + set.setSceneMode(SceneMode.NIGHT); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_NIGHT); + set.setSceneMode(SceneMode.PARTY); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_PARTY); + set.setSceneMode(SceneMode.PORTRAIT); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_PORTRAIT); + set.setSceneMode(SceneMode.SNOW); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_SNOW); + set.setSceneMode(SceneMode.SPORTS); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_SPORTS); + set.setSceneMode(SceneMode.STEADYPHOTO); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_STEADYPHOTO); + set.setSceneMode(SceneMode.SUNSET); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_SUNSET); + set.setSceneMode(SceneMode.THEATRE); + camera2SettingsCheckSingleOption(set, CONTROL_SCENE_MODE, CONTROL_SCENE_MODE_THEATRE); + + // White balances + set.setWhiteBalance(WhiteBalance.AUTO); + camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_AUTO); + set.setWhiteBalance(WhiteBalance.CLOUDY_DAYLIGHT); + camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_CLOUDY_DAYLIGHT); + set.setWhiteBalance(WhiteBalance.DAYLIGHT); + camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_DAYLIGHT); + set.setWhiteBalance(WhiteBalance.FLUORESCENT); + camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_FLUORESCENT); + set.setWhiteBalance(WhiteBalance.INCANDESCENT); + camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_INCANDESCENT); + set.setWhiteBalance(WhiteBalance.SHADE); + camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_SHADE); + set.setWhiteBalance(WhiteBalance.TWILIGHT); + camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_TWILIGHT); + set.setWhiteBalance(WhiteBalance.WARM_FLUORESCENT); + camera2SettingsCheckSingleOption(set, CONTROL_AWB_MODE, CONTROL_AWB_MODE_WARM_FLUORESCENT); + } + + // TODO: Add a test checking whether stringification matches API 1 representation + + @Test + public void camera2CapabilitiesFocusModeFromInt() throws CameraAccessException { + CameraCharacteristics chars = buildFrameworkCharacteristics(); + AndroidCamera2Capabilities intstr = new AndroidCamera2Capabilities(chars); + + // Focus modes + assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_AUTO), FocusMode.AUTO); + assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_CONTINUOUS_PICTURE), + FocusMode.CONTINUOUS_PICTURE); + assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_CONTINUOUS_VIDEO), + FocusMode.CONTINUOUS_VIDEO); + assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_EDOF), FocusMode.EXTENDED_DOF); + assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_OFF), FocusMode.FIXED); + assertEquals(intstr.focusModeFromInt(CONTROL_AF_MODE_MACRO), FocusMode.MACRO); + + // Scene modes + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_DISABLED), SceneMode.AUTO); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_ACTION), SceneMode.ACTION); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_BARCODE), SceneMode.BARCODE); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_BEACH), SceneMode.BEACH); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_CANDLELIGHT), SceneMode.CANDLELIGHT); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_FIREWORKS), SceneMode.FIREWORKS); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_LANDSCAPE), SceneMode.LANDSCAPE); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_NIGHT), SceneMode.NIGHT); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_PARTY), SceneMode.PARTY); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_PORTRAIT), SceneMode.PORTRAIT); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_SNOW), SceneMode.SNOW); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_SPORTS), SceneMode.SPORTS); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_STEADYPHOTO), + SceneMode.STEADYPHOTO); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_SUNSET), SceneMode.SUNSET); + assertEquals(intstr.sceneModeFromInt(CONTROL_SCENE_MODE_THEATRE), SceneMode.THEATRE); + + // White balances + assertEquals(intstr.whiteBalanceFromInt(CONTROL_AWB_MODE_AUTO), WhiteBalance.AUTO); + assertEquals(intstr.whiteBalanceFromInt(CONTROL_AWB_MODE_CLOUDY_DAYLIGHT), + WhiteBalance.CLOUDY_DAYLIGHT); + assertEquals(intstr.whiteBalanceFromInt(CONTROL_AWB_MODE_DAYLIGHT), WhiteBalance.DAYLIGHT); + assertEquals(intstr.whiteBalanceFromInt(CONTROL_AWB_MODE_FLUORESCENT), + WhiteBalance.FLUORESCENT); + assertEquals(intstr.whiteBalanceFromInt(CONTROL_AWB_MODE_INCANDESCENT), + WhiteBalance.INCANDESCENT); + assertEquals(intstr.whiteBalanceFromInt(CONTROL_AWB_MODE_SHADE), WhiteBalance.SHADE); + assertEquals(intstr.whiteBalanceFromInt(CONTROL_AWB_MODE_TWILIGHT), WhiteBalance.TWILIGHT); + assertEquals(intstr.whiteBalanceFromInt(CONTROL_AWB_MODE_WARM_FLUORESCENT), + WhiteBalance.WARM_FLUORESCENT); + } +} diff --git a/camera2/public/src/com/android/ex/camera2/blocking/BlockingCameraManager.java b/camera2/public/src/com/android/ex/camera2/blocking/BlockingCameraManager.java index de036e1..02dbbba 100644 --- a/camera2/public/src/com/android/ex/camera2/blocking/BlockingCameraManager.java +++ b/camera2/public/src/com/android/ex/camera2/blocking/BlockingCameraManager.java @@ -275,26 +275,6 @@ public class BlockingCameraManager { } @Override - public void onUnconfigured(CameraDevice camera) { - if (mProxy != null) mProxy.onUnconfigured(camera); - } - - @Override - public void onIdle(CameraDevice camera) { - if (mProxy != null) mProxy.onIdle(camera); - } - - @Override - public void onActive(CameraDevice camera) { - if (mProxy != null) mProxy.onActive(camera); - } - - @Override - public void onBusy(CameraDevice camera) { - if (mProxy != null) mProxy.onBusy(camera); - } - - @Override public void onClosed(CameraDevice camera) { if (mProxy != null) mProxy.onClosed(camera); } diff --git a/camera2/public/src/com/android/ex/camera2/blocking/BlockingCaptureListener.java b/camera2/public/src/com/android/ex/camera2/blocking/BlockingCaptureListener.java new file mode 100644 index 0000000..eae85d1 --- /dev/null +++ b/camera2/public/src/com/android/ex/camera2/blocking/BlockingCaptureListener.java @@ -0,0 +1,164 @@ +/* + * Copyright 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.blocking; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.util.Log; + +import com.android.ex.camera2.utils.StateChangeListener; +import com.android.ex.camera2.utils.StateWaiter; + +/** + * A camera capture listener that implements blocking operations on state changes for a + * particular capture request. + * + * <p>Provides a waiter that can be used to block until the next unobserved state of the + * requested type arrives.</p> + * + * <p>Pass-through all StateListener changes to the proxy.</p> + * + * @see #getStateWaiter + */ +public class BlockingCaptureListener extends CameraCaptureSession.CaptureListener { + + /** + * {@link #onCaptureStarted} has been called. + */ + public static final int CAPTURE_STARTED = 0; + + /** + * {@link #onCaptureProgressed} has been + * called. + */ + public static final int CAPTURE_PROGRESSED = 1; + + /** + * {@link #onCaptureCompleted} has + * been called. + */ + public static final int CAPTURE_COMPLETED = 2; + + /** + * {@link #onCaptureFailed} has been + * called. + */ + public static final int CAPTURE_FAILED = 3; + + /** + * {@link #onCaptureSequenceCompleted} has been called. + */ + public static final int CAPTURE_SEQUENCE_COMPLETED = 4; + + /** + * {@link #onCaptureSequenceAborted} has been called. + */ + public static final int CAPTURE_SEQUENCE_ABORTED = 5; + + private static final String[] sStateNames = { + "CAPTURE_STARTED", + "CAPTURE_PROGRESSED", + "CAPTURE_COMPLETED", + "CAPTURE_FAILED", + "CAPTURE_SEQUENCE_COMPLETED", + "CAPTURE_SEQUENCE_ABORTED" + }; + + private static final String TAG = "BlockingCaptureListener"; + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + private final CameraCaptureSession.CaptureListener mProxy; + + private final StateWaiter mStateWaiter = new StateWaiter(sStateNames); + private final StateChangeListener mStateChangeListener = mStateWaiter.getListener(); + + /** + * Create a blocking capture listener without forwarding the capture listener invocations + * to another capture listener. + */ + public BlockingCaptureListener() { + mProxy = null; + } + + /** + * Create a blocking capture listener; forward original listener invocations + * into {@code listener}. + * + * @param listener a non-{@code null} listener to forward invocations into + * + * @throws NullPointerException if {@code listener} was {@code null} + */ + public BlockingCaptureListener(CameraCaptureSession.CaptureListener listener) { + if (listener == null) { + throw new NullPointerException("listener must not be null"); + } + mProxy = listener; + } + + /** + * Acquire the state waiter; can be used to block until a set of state transitions have + * been reached. + * + * <p>Only one thread should wait at a time.</p> + */ + public StateWaiter getStateWaiter() { + return mStateWaiter; + } + + @Override + public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, + long timestamp) { + if (mProxy != null) mProxy.onCaptureStarted(session, request, timestamp); + mStateChangeListener.onStateChanged(CAPTURE_STARTED); + } + + @Override + public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, + CaptureResult partialResult) { + if (mProxy != null) mProxy.onCaptureProgressed(session, request, partialResult); + mStateChangeListener.onStateChanged(CAPTURE_PROGRESSED); + } + + @Override + public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) { + if (mProxy != null) mProxy.onCaptureCompleted(session, request, result); + mStateChangeListener.onStateChanged(CAPTURE_COMPLETED); + } + + @Override + public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, + CaptureFailure failure) { + if (mProxy != null) mProxy.onCaptureFailed(session, request, failure); + mStateChangeListener.onStateChanged(CAPTURE_FAILED); + } + + @Override + public void onCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId, + long frameNumber) { + if (mProxy != null) mProxy.onCaptureSequenceCompleted(session, sequenceId, frameNumber); + mStateChangeListener.onStateChanged(CAPTURE_SEQUENCE_COMPLETED); + } + + @Override + public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) { + if (mProxy != null) mProxy.onCaptureSequenceAborted(session, sequenceId); + mStateChangeListener.onStateChanged(CAPTURE_SEQUENCE_ABORTED); + } +} diff --git a/camera2/public/src/com/android/ex/camera2/blocking/BlockingSessionListener.java b/camera2/public/src/com/android/ex/camera2/blocking/BlockingSessionListener.java new file mode 100644 index 0000000..26bb652 --- /dev/null +++ b/camera2/public/src/com/android/ex/camera2/blocking/BlockingSessionListener.java @@ -0,0 +1,228 @@ +/* + * Copyright 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.blocking; + +import android.hardware.camera2.CameraCaptureSession; +import android.os.ConditionVariable; +import android.util.Log; + +import com.android.ex.camera2.exceptions.TimeoutRuntimeException; +import com.android.ex.camera2.utils.StateChangeListener; +import com.android.ex.camera2.utils.StateWaiter; + +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + + +/** + * A camera session listener that implements blocking operations on session state changes. + * + * <p>Provides a waiter that can be used to block until the next unobserved state of the + * requested type arrives.</p> + * + * <p>Pass-through all StateListener changes to the proxy.</p> + * + * @see #getStateWaiter + */ +public class BlockingSessionListener extends CameraCaptureSession.StateListener { + /** + * Session is configured, ready for captures + */ + public static final int SESSION_CONFIGURED = 0; + + /** + * Session has failed to configure, can't do any captures + */ + public static final int SESSION_CONFIGURE_FAILED = 1; + + /** + * Session is ready + */ + public static final int SESSION_READY = 2; + + /** + * Session is active (transitory) + */ + public static final int SESSION_ACTIVE = 3; + + /** + * Session is closed + */ + public static final int SESSION_CLOSED = 4; + + private final int NUM_STATES = 5; + + /* + * Private fields + */ + private static final String TAG = "BlockingSessionListener"; + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + private final CameraCaptureSession.StateListener mProxy; + private final SessionFuture mSessionFuture = new SessionFuture(); + + private final StateWaiter mStateWaiter = new StateWaiter(sStateNames); + private final StateChangeListener mStateChangeListener = mStateWaiter.getListener(); + + private static final String[] sStateNames = { + "SESSION_CONFIGURED", + "SESSION_CONFIGURE_FAILED", + "SESSION_READY", + "SESSION_ACTIVE", + "SESSION_CLOSED" + }; + + /** + * Create a blocking session listener without forwarding the session listener invocations + * to another session listener. + */ + public BlockingSessionListener() { + mProxy = null; + } + + /** + * Create a blocking session listener; forward original listener invocations + * into {@code listener}. + * + * @param listener a non-{@code null} listener to forward invocations into + * + * @throws NullPointerException if {@code listener} was {@code null} + */ + public BlockingSessionListener(CameraCaptureSession.StateListener listener) { + if (listener == null) { + throw new NullPointerException("listener must not be null"); + } + mProxy = listener; + } + + /** + * Acquire the state waiter; can be used to block until a set of state transitions have + * been reached. + * + * <p>Only one thread should wait at a time.</p> + */ + public StateWaiter getStateWaiter() { + return mStateWaiter; + } + + /** + * Return session if already have it; otherwise wait until any of the session listener + * invocations fire and the session is available. + * + * <p>Does not consume any of the states from the state waiter.</p> + * + * @param timeoutMs how many milliseconds to wait for + * @return a non-{@code null} {@link CameraCaptureSession} instance + * + * @throws TimeoutRuntimeException if waiting for more than {@long timeoutMs} + */ + public CameraCaptureSession waitAndGetSession(long timeoutMs) { + try { + return mSessionFuture.get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + throw new TimeoutRuntimeException( + String.format("Failed to get session after %s milliseconds", timeoutMs), e); + } + } + + /* + * CameraCaptureSession.StateListener implementation + */ + + @Override + public void onActive(CameraCaptureSession session) { + mSessionFuture.setSession(session); + if (mProxy != null) mProxy.onActive(session); + mStateChangeListener.onStateChanged(SESSION_ACTIVE); + } + + @Override + public void onClosed(CameraCaptureSession session) { + mSessionFuture.setSession(session); + if (mProxy != null) mProxy.onClosed(session); + mStateChangeListener.onStateChanged(SESSION_CLOSED); + } + + @Override + public void onConfigured(CameraCaptureSession session) { + mSessionFuture.setSession(session); + if (mProxy != null) mProxy.onConfigured(session); + mStateChangeListener.onStateChanged(SESSION_CONFIGURED); + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + mSessionFuture.setSession(session); + if (mProxy != null) mProxy.onConfigureFailed(session); + mStateChangeListener.onStateChanged(SESSION_CONFIGURE_FAILED); + } + + @Override + public void onReady(CameraCaptureSession session) { + mSessionFuture.setSession(session); + if (mProxy != null) mProxy.onReady(session); + mStateChangeListener.onStateChanged(SESSION_READY); + } + + private static class SessionFuture implements Future<CameraCaptureSession> { + private volatile CameraCaptureSession mSession; + ConditionVariable mCondVar = new ConditionVariable(/*opened*/false); + + public void setSession(CameraCaptureSession session) { + mSession = session; + mCondVar.open(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; // don't allow canceling this task + } + + @Override + public boolean isCancelled() { + return false; // can never cancel this task + } + + @Override + public boolean isDone() { + return mSession != null; + } + + @Override + public CameraCaptureSession get() { + mCondVar.block(); + return mSession; + } + + @Override + public CameraCaptureSession get(long timeout, TimeUnit unit) throws TimeoutException { + long timeoutMs = unit.convert(timeout, TimeUnit.MILLISECONDS); + if (!mCondVar.block(timeoutMs)) { + throw new TimeoutException( + "Failed to receive session after " + timeout + " " + unit); + } + + if (mSession == null) { + throw new AssertionError(); + } + return mSession; + } + + } +} diff --git a/camera2/public/src/com/android/ex/camera2/blocking/BlockingStateListener.java b/camera2/public/src/com/android/ex/camera2/blocking/BlockingStateListener.java index 619ba0a..02c2ba3 100644 --- a/camera2/public/src/com/android/ex/camera2/blocking/BlockingStateListener.java +++ b/camera2/public/src/com/android/ex/camera2/blocking/BlockingStateListener.java @@ -64,10 +64,6 @@ public class BlockingStateListener extends CameraDevice.StateListener { private static final String[] mStateNames = { "STATE_UNINITIALIZED", "STATE_OPENED", - "STATE_UNCONFIGURED", - "STATE_IDLE", - "STATE_ACTIVE", - "STATE_BUSY", "STATE_CLOSED", "STATE_DISCONNECTED", "STATE_ERROR" @@ -84,44 +80,24 @@ public class BlockingStateListener extends CameraDevice.StateListener { public static final int STATE_OPENED = 0; /** - * Device is unconfigured - */ - public static final int STATE_UNCONFIGURED = 1; - - /** - * Device is idle - */ - public static final int STATE_IDLE = 2; - - /** - * Device is active (transitory) - */ - public static final int STATE_ACTIVE = 3; - - /** - * Device is busy (transitory) - */ - public static final int STATE_BUSY = 4; - - /** * Device is closed */ - public static final int STATE_CLOSED = 5; + public static final int STATE_CLOSED = 1; /** * Device is disconnected */ - public static final int STATE_DISCONNECTED = 6; + public static final int STATE_DISCONNECTED = 2; /** * Device has encountered a fatal error */ - public static final int STATE_ERROR = 7; + public static final int STATE_ERROR = 3; /** * Total number of reachable states */ - private static int NUM_STATES = 8; + private static int NUM_STATES = 4; public BlockingStateListener() { mProxy = null; @@ -133,50 +109,26 @@ public class BlockingStateListener extends CameraDevice.StateListener { @Override public void onOpened(CameraDevice camera) { - setCurrentState(STATE_OPENED); if (mProxy != null) mProxy.onOpened(camera); + setCurrentState(STATE_OPENED); } @Override public void onDisconnected(CameraDevice camera) { - setCurrentState(STATE_DISCONNECTED); if (mProxy != null) mProxy.onDisconnected(camera); + setCurrentState(STATE_DISCONNECTED); } @Override public void onError(CameraDevice camera, int error) { - setCurrentState(STATE_ERROR); if (mProxy != null) mProxy.onError(camera, error); - } - - @Override - public void onUnconfigured(CameraDevice camera) { - setCurrentState(STATE_UNCONFIGURED); - if (mProxy != null) mProxy.onUnconfigured(camera); - } - - @Override - public void onIdle(CameraDevice camera) { - setCurrentState(STATE_IDLE); - if (mProxy != null) mProxy.onIdle(camera); - } - - @Override - public void onActive(CameraDevice camera) { - setCurrentState(STATE_ACTIVE); - if (mProxy != null) mProxy.onActive(camera); - } - - @Override - public void onBusy(CameraDevice camera) { - setCurrentState(STATE_BUSY); - if (mProxy != null) mProxy.onBusy(camera); + setCurrentState(STATE_ERROR); } @Override public void onClosed(CameraDevice camera) { - setCurrentState(STATE_CLOSED); if (mProxy != null) mProxy.onClosed(camera); + setCurrentState(STATE_CLOSED); } /** diff --git a/camera2/public/src/com/android/ex/camera2/pos/AutoFocusStateMachine.java b/camera2/public/src/com/android/ex/camera2/pos/AutoFocusStateMachine.java index 500fb12..9fa2af2 100644 --- a/camera2/public/src/com/android/ex/camera2/pos/AutoFocusStateMachine.java +++ b/camera2/public/src/com/android/ex/camera2/pos/AutoFocusStateMachine.java @@ -16,7 +16,7 @@ package com.android.ex.camera2.pos; import android.hardware.camera2.CameraDevice; -import android.hardware.camera2.CameraMetadata.Key; +import android.hardware.camera2.CaptureResult.Key; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.util.Log; diff --git a/camera2/public/src/com/android/ex/camera2/utils/StateChangeListener.java b/camera2/public/src/com/android/ex/camera2/utils/StateChangeListener.java new file mode 100644 index 0000000..48cca17 --- /dev/null +++ b/camera2/public/src/com/android/ex/camera2/utils/StateChangeListener.java @@ -0,0 +1,21 @@ +/* + * Copyright 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.utils; + +public interface StateChangeListener { + public void onStateChanged(int state); +} diff --git a/camera2/public/src/com/android/ex/camera2/utils/StateWaiter.java b/camera2/public/src/com/android/ex/camera2/utils/StateWaiter.java new file mode 100644 index 0000000..ec57829 --- /dev/null +++ b/camera2/public/src/com/android/ex/camera2/utils/StateWaiter.java @@ -0,0 +1,220 @@ +/* + * Copyright 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.utils; + +import android.os.SystemClock; +import android.util.Log; + +import com.android.ex.camera2.exceptions.TimeoutRuntimeException; + +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Block until a specific state change occurs. + * + * <p>Provides wait calls that block until the next unobserved state of the + * requested type arrives. Unobserved states are states that have occurred since + * the last wait, or that will be received from the camera device in the + * future.</p> + * + * <p>Thread interruptions are not supported; interrupting a thread that is either + * waiting with {@link #waitForState} / {@link #waitForAnyOfStates} or is currently in + * {@link StateChangeListener#onStateChanged} (provided by {@link #getListener}) will result in an + * {@link UnsupportedOperationException} being raised on that thread.</p> + */ +public final class StateWaiter { + + private static final String TAG = "StateWaiter"; + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + private final String[] mStateNames; + private final int mStateCount; + private final StateChangeListener mListener; + + /** Guard waitForState, waitForAnyState to only have one waiter */ + private final AtomicBoolean mWaiting = new AtomicBoolean(false); + + private final LinkedBlockingQueue<Integer> mQueuedStates = new LinkedBlockingQueue<>(); + + /** + * Create a new state waiter. + * + * <p>All {@code state}/{@code states} arguments used in other methods must be + * in the range of {@code [0, stateNames.length - 1]}.</p> + * + * @param stateNames an array of string names, used to mark the range of the valid states + */ + public StateWaiter(String[] stateNames) { + mStateCount = stateNames.length; + mStateNames = new String[mStateCount]; + System.arraycopy(stateNames, /*srcPos*/0, mStateNames, /*dstPos*/0, mStateCount); + + mListener = new StateChangeListener() { + @Override + public void onStateChanged(int state) { + queueStateTransition(checkStateInRange(state)); + } + }; + } + + public StateChangeListener getListener() { + return mListener; + } + + /** + * Wait until the desired state is observed, checking all state + * transitions since the last time a state was waited on. + * + * <p>Any intermediate state transitions that is not {@code state} are ignored.</p> + * + * <p>Note: Only one waiter allowed at a time!</p> + * + * @param desired state to observe a transition to + * @param timeoutMs how long to wait in milliseconds + * + * @throws IllegalArgumentException if {@code state} was out of range + * @throws TimeoutRuntimeException if the desired state is not observed before timeout. + * @throws IllegalStateException if another thread is already waiting for a state transition + */ + public void waitForState(int state, long timeoutMs) { + Integer[] stateArray = { checkStateInRange(state) }; + + waitForAnyOfStates(Arrays.asList(stateArray), timeoutMs); + } + + /** + * Wait until the one of the desired {@code states} is observed, checking all + * state transitions since the last time a state was waited on. + * + * <p>Any intermediate state transitions that are not in {@code states} are ignored.</p> + * + * <p>Note: Only one waiter allowed at a time!</p> + * + * @param states Set of desired states to observe a transition to. + * @param timeoutMs how long to wait in milliseconds + * + * @return the state reached + * + * @throws IllegalArgumentException if {@code state} was out of range + * @throws TimeoutRuntimeException if none of the states is observed before timeout. + * @throws IllegalStateException if another thread is already waiting for a state transition + */ + public int waitForAnyOfStates(Collection<Integer> states, final long timeoutMs) { + checkStateCollectionInRange(states); + + // Acquire exclusive waiting privileges + if (mWaiting.getAndSet(true)) { + throw new IllegalStateException("Only one waiter allowed at a time"); + } + + Integer nextState = null; + try { + if (VERBOSE) { + StringBuilder s = new StringBuilder("Waiting for state(s) "); + appendStateNames(s, states); + Log.v(TAG, s.toString()); + } + + long timeoutLeft = timeoutMs; + long startMs = SystemClock.elapsedRealtime(); + while ((nextState = mQueuedStates.poll(timeoutLeft, TimeUnit.MILLISECONDS)) != null) { + if (VERBOSE) { + Log.v(TAG, " Saw transition to " + getStateName(nextState)); + } + + if (states.contains(nextState)) { + break; + } + + long endMs = SystemClock.elapsedRealtime(); + timeoutLeft -= (endMs - startMs); + startMs = endMs; + } + } catch (InterruptedException e) { + throw new UnsupportedOperationException("Does not support interrupts on waits", e); + } finally { + // Release exclusive waiting privileges + mWaiting.set(false); + } + + if (!states.contains(nextState)) { + StringBuilder s = new StringBuilder("Timed out after "); + s.append(timeoutMs); + s.append(" ms waiting for state(s) "); + appendStateNames(s, states); + + throw new TimeoutRuntimeException(s.toString()); + } + + return nextState; + } + + /** + * Convert state integer to a String + */ + public String getStateName(int state) { + return mStateNames[checkStateInRange(state)]; + } + + /** + * Append all states to string + */ + public void appendStateNames(StringBuilder s, Collection<Integer> states) { + checkStateCollectionInRange(states); + + boolean start = true; + for (Integer state : states) { + if (!start) { + s.append(" "); + } + + s.append(getStateName(state)); + start = false; + } + } + + private void queueStateTransition(int state) { + if (VERBOSE) Log.v(TAG, "setCurrentState - state now " + getStateName(state)); + + try { + mQueuedStates.put(state); + } catch(InterruptedException e) { + throw new UnsupportedOperationException("Unable to set current state", e); + } + } + + private int checkStateInRange(int state) { + if (state < 0 || state >= mStateCount) { + throw new IllegalArgumentException("State out of range " + state); + } + + return state; + } + + private Collection<Integer> checkStateCollectionInRange(Collection<Integer> states) { + for (int state : states) { + checkStateInRange(state); + } + + return states; + } + +} diff --git a/camera2/utils/Android.mk b/camera2/utils/Android.mk new file mode 100644 index 0000000..36595e1 --- /dev/null +++ b/camera2/utils/Android.mk @@ -0,0 +1,17 @@ +# Copyright 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. + +LOCAL_PATH := $(call my-dir) +include $(LOCAL_PATH)/utils.mk \ + $(call all-subdir-makefiles) diff --git a/camera2/utils/src/com/android/ex/camera2/utils/Camera2CaptureListenerForwarder.java b/camera2/utils/src/com/android/ex/camera2/utils/Camera2CaptureListenerForwarder.java new file mode 100644 index 0000000..35b1c6d --- /dev/null +++ b/camera2/utils/src/com/android/ex/camera2/utils/Camera2CaptureListenerForwarder.java @@ -0,0 +1,99 @@ +/* + * 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.utils; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCaptureSession.CaptureListener; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.os.Handler; + +/** + * Proxy that forwards all updates to another {@link CaptureListener}, invoking + * its callbacks on a separate {@link Handler}. + */ +public class Camera2CaptureListenerForwarder extends CaptureListener { + private CaptureListener mListener; + private Handler mHandler; + + public Camera2CaptureListenerForwarder(CaptureListener listener, Handler handler) { + mListener = listener; + mHandler = handler; + } + + @Override + public void onCaptureCompleted(final CameraCaptureSession session, final CaptureRequest request, + final TotalCaptureResult result) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onCaptureCompleted(session, request, result); + }}); + } + + @Override + public void onCaptureFailed(final CameraCaptureSession session, final CaptureRequest request, + final CaptureFailure failure) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onCaptureFailed(session, request, failure); + }}); + } + + @Override + public void onCaptureProgressed(final CameraCaptureSession session, + final CaptureRequest request, + final CaptureResult partialResult) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onCaptureProgressed(session, request, partialResult); + }}); + } + + @Override + public void onCaptureSequenceAborted(final CameraCaptureSession session, final int sequenceId) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onCaptureSequenceAborted(session, sequenceId); + }}); + } + + @Override + public void onCaptureSequenceCompleted(final CameraCaptureSession session, final int sequenceId, + final long frameNumber) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onCaptureSequenceCompleted(session, sequenceId, frameNumber); + }}); + } + + @Override + public void onCaptureStarted(final CameraCaptureSession session, final CaptureRequest request, + final long timestamp) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onCaptureStarted(session, request, timestamp); + }}); + } +} diff --git a/camera2/utils/src/com/android/ex/camera2/utils/Camera2CaptureListenerSplitter.java b/camera2/utils/src/com/android/ex/camera2/utils/Camera2CaptureListenerSplitter.java new file mode 100644 index 0000000..a13dc04 --- /dev/null +++ b/camera2/utils/src/com/android/ex/camera2/utils/Camera2CaptureListenerSplitter.java @@ -0,0 +1,95 @@ +/* + * 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.utils; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCaptureSession.CaptureListener; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * Junction that allows notifying multiple {@link CaptureListener}s whenever + * the {@link CameraCaptureSession} posts a capture-related update. + */ +public class Camera2CaptureListenerSplitter extends CaptureListener { + private final List<CaptureListener> mRecipients = new LinkedList<>(); + + /** + * @param recipients The listeners to notify. Any {@code null} passed here + * will be completely ignored. + */ + public Camera2CaptureListenerSplitter(CaptureListener... recipients) { + for (CaptureListener listener : recipients) { + if (listener != null) { + mRecipients.add(listener); + } + } + } + + @Override + public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) { + for (CaptureListener target : mRecipients) { + target.onCaptureCompleted(session, request, result); + } + } + + @Override + public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, + CaptureFailure failure) { + for (CaptureListener target : mRecipients) { + target.onCaptureFailed(session, request, failure); + } + } + + @Override + public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, + CaptureResult partialResult) { + for (CaptureListener target : mRecipients) { + target.onCaptureProgressed(session, request, partialResult); + } + } + + @Override + public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) { + for (CaptureListener target : mRecipients) { + target.onCaptureSequenceAborted(session, sequenceId); + } + } + + @Override + public void onCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId, + long frameNumber) { + for (CaptureListener target : mRecipients) { + target.onCaptureSequenceCompleted(session, sequenceId, frameNumber); + } + } + + @Override + public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, + long timestamp) { + for (CaptureListener target : mRecipients) { + target.onCaptureStarted(session, request, timestamp); + } + } +} diff --git a/camera2/utils/src/com/android/ex/camera2/utils/Camera2RequestSettingsSet.java b/camera2/utils/src/com/android/ex/camera2/utils/Camera2RequestSettingsSet.java new file mode 100644 index 0000000..fac8f1a --- /dev/null +++ b/camera2/utils/src/com/android/ex/camera2/utils/Camera2RequestSettingsSet.java @@ -0,0 +1,238 @@ +/* + * 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.utils; + +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureRequest.Builder; +import android.hardware.camera2.CaptureRequest.Key; +import android.view.Surface; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * A set of settings to be used when filing a {@link CaptureRequest}. + */ +public class Camera2RequestSettingsSet { + private final Map<Key<?>, Object> mDictionary; + private long mRevision; + + /** + * Create a new instance with no settings defined. + * + * <p>Creating a request from this object without first specifying any + * properties on it is equivalent to just creating a request directly + * from the template of choice. Its revision identifier is initially + * {@code 0}, and will remain thus until its first modification.</p> + */ + public Camera2RequestSettingsSet() { + mDictionary = new HashMap<>(); + mRevision = 0; + } + + /** + * Perform a deep copy of the defined settings and revision number. + * + * @param other The reference instance. + * + * @throws NullPointerException If {@code other} is {@code null}. + */ + public Camera2RequestSettingsSet(Camera2RequestSettingsSet other) { + if (other == null) { + throw new NullPointerException("Tried to copy null Camera2RequestSettingsSet"); + } + + mDictionary = new HashMap<>(other.mDictionary); + mRevision = other.mRevision; + } + + /** + * Specify a setting, potentially overriding the template's default choice. + * + * <p>Providing a {@code null} {@code value} will indicate a forced use of + * the template's selection for that {@code key}; the difference here is + * that this information will be propagated with unions as documented in + * {@link #union}. This method increments the revision identifier if the new + * choice is different than the existing selection.</p> + * + * @param key Which setting to alter. + * @param value The new selection for that setting, or {@code null} to force + * the use of the template's default selection for this field. + * @return Whether the settings were updated, which only occurs if the + * {@code value} is different from any already stored. + * + * @throws NullPointerException If {@code key} is {@code null}. + */ + public <T> boolean set(Key<T> key, T value) { + if (key == null) { + throw new NullPointerException("Received a null key"); + } + + Object currentValue = get(key); + // Only save the value if it's different from the one we already have + if (!mDictionary.containsKey(key) || !Objects.equals(value, currentValue)) { + mDictionary.put(key, value); + ++mRevision; + return true; + } + return false; + } + + /** + * Unsets a setting, preventing it from being propagated with unions or from + * overriding the default when creating a capture request. + * + * <p>This method increments the revision identifier if a selection had + * previously been made for that parameter.</p> + * + * @param key Which setting to reset. + * @return Whether the settings were updated, which only occurs if the + * specified setting already had a value or was forced to default. + * + * @throws NullPointerException If {@code key} is {@code null}. + */ + public boolean unset(Key<?> key) { + if (key == null) { + throw new NullPointerException("Received a null key"); + } + + if (mDictionary.containsKey(key)) { + mDictionary.remove(key); + ++mRevision; + return true; + } + return false; + } + + /** + * Interrogate the current specialization of a setting. + * + * @param key Which setting to check. + * @return The current selection for that setting, or {@code null} if the + * setting is unset or forced to the template-defined default. + * + * @throws NullPointerException If {@code key} is {@code null}. + */ + @SuppressWarnings("unchecked") + public <T> T get(Key<T> key) { + if (key == null) { + throw new NullPointerException("Received a null key"); + } + return (T) mDictionary.get(key); + } + + /** + * Query this instance for whether it prefers a particular choice for the + * given request parameter. + * + * <p>This method can be used to detect whether a particular field is forced + * to its default value or simply unset. While {@link #get} will return + * {@code null} in both these cases, this method will return {@code true} + * and {@code false}, respectively.</p> + + * @param key Which setting to look for. + * @return Whether that setting has a value that will propagate with unions. + * + * @throws NullPointerException If {@code key} is {@code null}. + */ + public boolean contains(Key<?> key) { + if (key == null) { + throw new NullPointerException("Received a null key"); + } + return mDictionary.containsKey(key); + } + + /** + * Get this set of settings's revision identifier, which can be compared + * against cached past values to determine whether it has been modified. + * <p>Distinct revisions across the same object do not necessarily indicate + * that the object's key/value pairs have changed at all, but the same + * revision on the same object does imply that they've stayed the same.</p> + * + * @return The number of modifications made since the beginning of this + * object's heritage. + */ + public long getRevision() { + return mRevision; + } + + /** + * Add all settings choices defined by {@code moreSettings} to this object. + * + * <p>For any settings defined in both, the choice stored in the argument + * to this method take precedence. Unset settings are not propagated, but + * those forced to default as described in {@link set} are also forced to + * default in {@code this} set. Invoking this method increments {@code this} + * object's revision counter, but leaves the argument's unchanged.</p> + * + * @param moreSettings The source of the additional settings ({@code null} + * is allowed here). + * @return Whether these settings were updated, which can only fail if the + * target itself is also given as the argument. + */ + public boolean union(Camera2RequestSettingsSet moreSettings) { + if (moreSettings == null || moreSettings == this) { + return false; + } + + mDictionary.putAll(moreSettings.mDictionary); + ++mRevision; + return true; + } + + /** + * Create a {@link CaptureRequest} specialized for the specified + * {@link CameraDevice} and targeting the given {@link Surface}s. + * + * @param camera The camera from which to capture. + * @param template A {@link CaptureRequest} template defined in + * {@link CameraDevice}. + * @param targets The location(s) to draw the resulting image onto. + * @return The request, ready to be passed to the camera framework. + * + * @throws CameraAccessException Upon an underlying framework API failure. + * @throws NullPointerException If any argument is {@code null}. + */ + public CaptureRequest createRequest(CameraDevice camera, int template, Surface... targets) + throws CameraAccessException { + if (camera == null) { + throw new NullPointerException("Tried to create request using null CameraDevice"); + } + + Builder reqBuilder = camera.createCaptureRequest(template); + for (Key<?> key : mDictionary.keySet()) { + setRequestFieldIfNonNull(reqBuilder, key); + } + for (Surface target : targets) { + if (target == null) { + throw new NullPointerException("Tried to add null Surface as request target"); + } + reqBuilder.addTarget(target); + } + return reqBuilder.build(); + } + + private <T> void setRequestFieldIfNonNull(Builder requestBuilder, Key<T> key) { + T value = get(key); + if (value != null) { + requestBuilder.set(key, value); + } + } +} diff --git a/camera2/utils/tests/Android.mk b/camera2/utils/tests/Android.mk new file mode 100644 index 0000000..6b3a569 --- /dev/null +++ b/camera2/utils/tests/Android.mk @@ -0,0 +1,24 @@ +# 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PACKAGE_NAME := android-ex-camera2-utils-tests +LOCAL_MODULE_TAGS := tests +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under,src) +LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2-utils android-support-test mockito-target + +include $(BUILD_PACKAGE) diff --git a/camera2/utils/tests/AndroidManifest.xml b/camera2/utils/tests/AndroidManifest.xml new file mode 100644 index 0000000..2f3cc08 --- /dev/null +++ b/camera2/utils/tests/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.ex.camera2.utils.tests"> + <uses-permission android:name="android.permission.CAMERA" /> + <application> + <uses-library android:name="android.test.runner" /> + </application> + <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.ex.camera2.utils.tests" /> +</manifest> diff --git a/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2DeviceTester.java b/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2DeviceTester.java new file mode 100644 index 0000000..4db6dfb --- /dev/null +++ b/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2DeviceTester.java @@ -0,0 +1,93 @@ +/* + * 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.utils; + +import android.content.Context; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.support.test.InjectContext; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +/** + * Subclasses of this have an {@code mCamera} instance variable representing the first camera. + */ +public class Camera2DeviceTester { + private static HandlerThread sThread; + + private static Handler sHandler; + + @BeforeClass + public static void setupBackgroundHandler() { + sThread = new HandlerThread("CameraFramework"); + sThread.start(); + sHandler = new Handler(sThread.getLooper()); + } + + @AfterClass + public static void teardownBackgroundHandler() throws Exception { + sThread.quitSafely(); + sThread.join(); + } + + @InjectContext + public Context mContext; + + private class DeviceCapturer extends CameraDevice.StateListener { + private CameraDevice mCamera; + + public CameraDevice captureCameraDevice() throws Exception { + CameraManager manager = + (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + String id = manager.getCameraIdList()[0]; + synchronized (this) { + manager.openCamera(id, this, sHandler); + wait(); + } + return mCamera; + } + + @Override + public synchronized void onOpened(CameraDevice camera) { + mCamera = camera; + notify(); + } + + @Override + public void onDisconnected(CameraDevice camera) {} + + @Override + public void onError(CameraDevice camera, int error) {} + } + + protected CameraDevice mCamera; + + @Before + public void obtainCameraCaptureRequestBuilderFactory() throws Exception { + mCamera = new DeviceCapturer().captureCameraDevice(); + } + + @After + public void releaseCameraCaptureRequestBuilderFactory() { + mCamera.close(); + } +} diff --git a/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2UtilsTest.java b/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2UtilsTest.java new file mode 100644 index 0000000..bb23e37 --- /dev/null +++ b/camera2/utils/tests/src/com/android/ex/camera2/utils/Camera2UtilsTest.java @@ -0,0 +1,410 @@ +/* + * 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.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.hardware.camera2.CameraCaptureSession.CaptureListener; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureRequest.Key; +import android.view.Surface; + +import org.junit.Test; + +public class Camera2UtilsTest extends Camera2DeviceTester { + private void captureListenerSplitterAllCallbacksReceived(CaptureListener splitter, + CaptureListener... terminals) { + splitter.onCaptureCompleted(null, null, null); + for (CaptureListener each : terminals) { + verify(each).onCaptureCompleted(null, null, null); + } + splitter.onCaptureFailed(null, null, null); + for (CaptureListener each : terminals) { + verify(each).onCaptureFailed(null, null, null); + } + splitter.onCaptureProgressed(null, null, null); + for (CaptureListener each : terminals) { + verify(each).onCaptureProgressed(null, null, null); + } + splitter.onCaptureSequenceAborted(null, 0); + for (CaptureListener each : terminals) { + verify(each).onCaptureSequenceAborted(null, 0); + } + splitter.onCaptureSequenceCompleted(null, 0, 0L); + for (CaptureListener each : terminals) { + verify(each).onCaptureSequenceCompleted(null, 0, 0L); + } + splitter.onCaptureStarted(null, null, 0L); + for (CaptureListener each : terminals) { + verify(each).onCaptureStarted(null, null, 0L); + } + } + + @Test + public void captureListenerSplitter() { + CaptureListener firstBackingListener = mock(CaptureListener.class); + CaptureListener secondBackingListener = mock(CaptureListener.class); + captureListenerSplitterAllCallbacksReceived( + new Camera2CaptureListenerSplitter(firstBackingListener, secondBackingListener), + firstBackingListener, secondBackingListener); + } + + @Test + public void captureListenerSplitterEmpty() { + captureListenerSplitterAllCallbacksReceived(new Camera2CaptureListenerSplitter()); + } + + @Test + public void captureListenerSplitterNoNpe() { + captureListenerSplitterAllCallbacksReceived( + new Camera2CaptureListenerSplitter((CaptureListener) null)); + } + + @Test + public void captureListenerSplitterMultipleNulls() { + captureListenerSplitterAllCallbacksReceived( + new Camera2CaptureListenerSplitter(null, null, null)); + } + + @Test + public void captureListenerSplitterValidAndNull() { + CaptureListener onlyRealBackingListener = mock(CaptureListener.class); + captureListenerSplitterAllCallbacksReceived( + new Camera2CaptureListenerSplitter(null, onlyRealBackingListener), + onlyRealBackingListener); + } + + private <T> void requestSettingsSetAndForget(Camera2RequestSettingsSet s, Key<T> k, T v) { + s.set(k, v); + assertEquals(v, s.get(k)); + } + + @Test + public void requestSettingsSet() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + // Try a boolean + requestSettingsSetAndForget(setUp, CaptureRequest.CONTROL_AE_LOCK, false); + requestSettingsSetAndForget(setUp, CaptureRequest.CONTROL_AE_LOCK, true); + // Try an int + requestSettingsSetAndForget(setUp, CaptureRequest.CONTROL_AE_MODE, 1); + requestSettingsSetAndForget(setUp, CaptureRequest.CONTROL_AE_MODE, -1); + requestSettingsSetAndForget(setUp, CaptureRequest.CONTROL_AE_MODE, 0); + // Try an int[] + requestSettingsSetAndForget(setUp, CaptureRequest.SENSOR_TEST_PATTERN_DATA, new int[] {1}); + requestSettingsSetAndForget(setUp, CaptureRequest.SENSOR_TEST_PATTERN_DATA, + new int[] {2, 2}); + } + + @Test + public void requestSettingsSetNullValue() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + requestSettingsSetAndForget(setUp, CaptureRequest.SENSOR_TEST_PATTERN_DATA, new int[] {1}); + requestSettingsSetAndForget(setUp, CaptureRequest.SENSOR_TEST_PATTERN_DATA, null); + requestSettingsSetAndForget(setUp, CaptureRequest.SENSOR_TEST_PATTERN_DATA, + new int[] {2, 2}); + } + + @Test + public void requestSettingsSetUnsetAndContains() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + assertFalse(setUp.contains(CaptureRequest.CONTROL_AE_LOCK)); + setUp.set(CaptureRequest.CONTROL_AE_LOCK, false); + assertTrue(setUp.contains(CaptureRequest.CONTROL_AE_LOCK)); + setUp.set(CaptureRequest.CONTROL_AE_LOCK, null); + assertTrue(setUp.contains(CaptureRequest.CONTROL_AE_LOCK)); + setUp.unset(CaptureRequest.CONTROL_AE_LOCK); + assertFalse(setUp.contains(CaptureRequest.CONTROL_AE_LOCK)); + + setUp.set(CaptureRequest.CONTROL_AE_LOCK, null); + assertTrue(setUp.contains(CaptureRequest.CONTROL_AE_LOCK)); + setUp.set(CaptureRequest.CONTROL_AE_LOCK, false); + assertTrue(setUp.contains(CaptureRequest.CONTROL_AE_LOCK)); + setUp.unset(CaptureRequest.CONTROL_AE_LOCK); + assertFalse(setUp.contains(CaptureRequest.CONTROL_AE_LOCK)); + } + + @Test + public void requestSettingsSetStartsWithoutChanges() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + assertEquals(0, setUp.getRevision()); + } + + private <T> void requestSettingsSetAndAssertChanged(Camera2RequestSettingsSet settings, + Key<T> key, T value, + boolean shouldHaveChanged) { + long revision = settings.getRevision(); + assertEquals(shouldHaveChanged, settings.set(key, value)); + assertEquals(shouldHaveChanged ? revision + 1 : revision, settings.getRevision()); + } + + @Test + public void requestSettingsSetChangesReportedCorrectly() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + requestSettingsSetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, false, true); + requestSettingsSetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, false, false); + requestSettingsSetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, true, true); + } + + @Test + public void requestSettingsSetDetectsNoopChanges() { + Camera2RequestSettingsSet s = new Camera2RequestSettingsSet(); + int[] one = {1}, two = {2}; + + requestSettingsSetAndAssertChanged(s, CaptureRequest.SENSOR_TEST_PATTERN_DATA, one, true); + requestSettingsSetAndAssertChanged(s, CaptureRequest.SENSOR_TEST_PATTERN_DATA, one, false); + + requestSettingsSetAndAssertChanged(s, CaptureRequest.SENSOR_TEST_PATTERN_DATA, null, true); + requestSettingsSetAndAssertChanged(s, CaptureRequest.SENSOR_TEST_PATTERN_DATA, null, false); + + requestSettingsSetAndAssertChanged(s, CaptureRequest.SENSOR_TEST_PATTERN_DATA, two, true); + requestSettingsSetAndAssertChanged(s, CaptureRequest.SENSOR_TEST_PATTERN_DATA, two, false); + } + + private <T> void requestSettingsUnsetAndAssertChanged(Camera2RequestSettingsSet settings, + Key<T> key, boolean shouldHaveChanged) { + long revision = settings.getRevision(); + assertEquals(shouldHaveChanged, settings.unset(key)); + assertEquals(shouldHaveChanged ? revision + 1 : revision, settings.getRevision()); + } + + @Test + public void requestSettingsSetUnsetMakesChangesAndDetectsNoops() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + requestSettingsUnsetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, false); + + requestSettingsSetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, false, true); + requestSettingsUnsetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, true); + + requestSettingsSetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, false, true); + requestSettingsSetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, false, false); + requestSettingsUnsetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, true); + requestSettingsUnsetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, false); + + requestSettingsSetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, false, true); + requestSettingsSetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, true, true); + requestSettingsUnsetAndAssertChanged(setUp, CaptureRequest.CONTROL_AE_LOCK, true); + } + + @Test(expected=NullPointerException.class) + public void requestSettingsSetNullArgToCopyConstructor() { + Camera2RequestSettingsSet flop = new Camera2RequestSettingsSet(null); + } + + @Test(expected=NullPointerException.class) + public void requestSettingsSetNullArgToSetKey() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + setUp.set(null, null); + } + + @Test(expected=NullPointerException.class) + public void requestSettingsSetNullArgToUnset() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + setUp.unset(null); + } + + @Test(expected=NullPointerException.class) + public void requestSettingsSetNullArgToContains() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + setUp.contains(null); + } + + @Test(expected=NullPointerException.class) + public void requestSettingsSetNullArgToGet() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + setUp.get(null); + } + + @Test(expected=NullPointerException.class) + public void requestSettingsSetNullArgToCreateRequest0() throws Exception { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + setUp.createRequest(null, 0); + } + + @Test(expected=NullPointerException.class) + public void requestSettingsSetNullArgToCreateRequest2() throws Exception { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + setUp.createRequest(mCamera, 0, (Surface) null); + } + + @Test(expected=NullPointerException.class) + public void requestSettingsSetNullArgToCreateRequest02() throws Exception { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + setUp.createRequest(null, 0, (Surface) null); + } + + @Test + public void requestSettingsSetNullArgToUnion() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + assertFalse(setUp.union(null)); + assertEquals(0, setUp.getRevision()); + } + + @Test + public void requestSettingsSetSelfArgToUnion() { + Camera2RequestSettingsSet setUp = new Camera2RequestSettingsSet(); + assertFalse(setUp.union(setUp)); + assertEquals(0, setUp.getRevision()); + } + + @Test + public void requestSettingsSetCopyConstructor() { + Camera2RequestSettingsSet original = new Camera2RequestSettingsSet(); + Camera2RequestSettingsSet unchanged = new Camera2RequestSettingsSet(original); + + requestSettingsSetAndForget(original, CaptureRequest.CONTROL_AE_LOCK, true); + Camera2RequestSettingsSet changed = new Camera2RequestSettingsSet(original); + assertEquals(true, changed.get(CaptureRequest.CONTROL_AE_LOCK)); + } + + @Test + public void requestSettingsSetCopyConstructorPreservesChangedStatus() { + Camera2RequestSettingsSet original = new Camera2RequestSettingsSet(); + Camera2RequestSettingsSet unchanged = new Camera2RequestSettingsSet(original); + assertEquals(original.getRevision(), unchanged.getRevision()); + + requestSettingsSetAndAssertChanged(original, CaptureRequest.CONTROL_AE_LOCK, true, true); + Camera2RequestSettingsSet changed = new Camera2RequestSettingsSet(original); + assertEquals(original.getRevision(), changed.getRevision()); + assertNotSame(original.getRevision(), unchanged.getRevision()); + } + + @Test + public void requestSettingsSetCopyConstructorPerformsDeepCopy() { + Camera2RequestSettingsSet original = new Camera2RequestSettingsSet(); + requestSettingsSetAndForget(original, CaptureRequest.CONTROL_AE_LOCK, true); + + Camera2RequestSettingsSet changed = new Camera2RequestSettingsSet(original); + requestSettingsSetAndForget(changed, CaptureRequest.CONTROL_AE_LOCK, false); + assertEquals(true, original.get(CaptureRequest.CONTROL_AE_LOCK)); + } + + @Test + public void requestSettingsSetNullMeansDefault() throws Exception { + Camera2RequestSettingsSet s = new Camera2RequestSettingsSet(); + CaptureRequest r1 = s.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW); + assertEquals((Object) CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW, + r1.get(CaptureRequest.CONTROL_CAPTURE_INTENT)); + + requestSettingsSetAndForget(s, CaptureRequest.CONTROL_CAPTURE_INTENT, + CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE); + CaptureRequest r2 = s.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW); + assertEquals((Object) CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE, + r2.get(CaptureRequest.CONTROL_CAPTURE_INTENT)); + + requestSettingsSetAndForget(s, CaptureRequest.CONTROL_CAPTURE_INTENT, null); + CaptureRequest r3 = s.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW); + assertEquals((Object) CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW, + r3.get(CaptureRequest.CONTROL_CAPTURE_INTENT)); + + s.unset(CaptureRequest.CONTROL_CAPTURE_INTENT); + CaptureRequest r4 = s.createRequest(mCamera, CameraDevice.TEMPLATE_PREVIEW); + assertEquals((Object) CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW, + r4.get(CaptureRequest.CONTROL_CAPTURE_INTENT)); + } + + @Test + public void requestSettingsSetNullPreservedByUnions() { + Camera2RequestSettingsSet master = new Camera2RequestSettingsSet(); + requestSettingsSetAndForget(master, CaptureRequest.CONTROL_CAPTURE_INTENT, + CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW); + + Camera2RequestSettingsSet slave = new Camera2RequestSettingsSet(); + master.union(slave); + assertEquals((Object) CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW, + master.get(CaptureRequest.CONTROL_CAPTURE_INTENT)); + + requestSettingsSetAndForget(slave, CaptureRequest.CONTROL_CAPTURE_INTENT, null); + master.union(slave); + assertEquals(null, master.get(CaptureRequest.CONTROL_CAPTURE_INTENT)); + + requestSettingsSetAndForget(slave, CaptureRequest.CONTROL_CAPTURE_INTENT, + CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE); + master.union(slave); + assertEquals((Object) CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE, + master.get(CaptureRequest.CONTROL_CAPTURE_INTENT)); + + slave.unset(CaptureRequest.CONTROL_CAPTURE_INTENT); + master.union(slave); + assertEquals((Object) CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE, + master.get(CaptureRequest.CONTROL_CAPTURE_INTENT)); + } + + @Test + public void requestSettingsSetNullChangesRecorded() throws Exception { + Camera2RequestSettingsSet s = new Camera2RequestSettingsSet(); + requestSettingsSetAndAssertChanged(s, CaptureRequest.CONTROL_CAPTURE_INTENT, null, true); + requestSettingsSetAndAssertChanged(s, CaptureRequest.CONTROL_CAPTURE_INTENT, + CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW, true); + requestSettingsSetAndAssertChanged(s, CaptureRequest.CONTROL_CAPTURE_INTENT, null, true); + requestSettingsSetAndAssertChanged(s, CaptureRequest.CONTROL_CAPTURE_INTENT, null, false); + } + + @Test + public void requestSettingsSetUnionChangesRecorded() { + Camera2RequestSettingsSet[] sets = { new Camera2RequestSettingsSet(), + new Camera2RequestSettingsSet() }; + sets[0].union(sets[1]); + assertEquals(1, sets[0].getRevision()); + assertEquals(0, sets[1].getRevision()); + } + + private <T> void requestSettingsSetsCheckPairOfProperties(Camera2RequestSettingsSet firstSet, + Camera2RequestSettingsSet secondSet, + Key<T> firstKey, + Key<T> secondKey, + T expectedFirstSetFirstValue, + T expectedFirstSetSecondValue, + T expectedSecondSetFirstValue, + T expectedSecondSetSecondValue) { + assertEquals(expectedFirstSetFirstValue, firstSet.get(firstKey)); + assertEquals(expectedFirstSetSecondValue, firstSet.get(secondKey)); + assertEquals(expectedSecondSetFirstValue, secondSet.get(firstKey)); + assertEquals(expectedSecondSetSecondValue, secondSet.get(secondKey)); + } + + @Test + public void requestSettingsSetUnionChangesReflected() { + Camera2RequestSettingsSet[] sets = { new Camera2RequestSettingsSet(), + new Camera2RequestSettingsSet() }; + + sets[0].set(CaptureRequest.CONTROL_AE_LOCK, true); + sets[1].set(CaptureRequest.CONTROL_AWB_LOCK, true); + sets[0].union(sets[1]); + sets[1].set(CaptureRequest.CONTROL_AE_LOCK, false); + requestSettingsSetsCheckPairOfProperties(sets[0], sets[1], + CaptureRequest.CONTROL_AE_LOCK, CaptureRequest.CONTROL_AWB_LOCK, + true, true, false, true); + + sets[0].union(sets[1]); + requestSettingsSetsCheckPairOfProperties(sets[0], sets[1], + CaptureRequest.CONTROL_AE_LOCK, CaptureRequest.CONTROL_AWB_LOCK, + false, true, false, true); + + sets[1].set(CaptureRequest.CONTROL_AE_LOCK, false); + sets[1].set(CaptureRequest.CONTROL_AWB_LOCK, false); + sets[0].union(sets[1]); + requestSettingsSetsCheckPairOfProperties(sets[0], sets[1], + CaptureRequest.CONTROL_AE_LOCK, CaptureRequest.CONTROL_AWB_LOCK, + false, false, false, false); + } +} diff --git a/camera2/utils/utils.mk b/camera2/utils/utils.mk new file mode 100644 index 0000000..a193582 --- /dev/null +++ b/camera2/utils/utils.mk @@ -0,0 +1,23 @@ +# Copyright 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE := android-ex-camera2-utils +LOCAL_MODULE_TAGS := optional +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +include $(BUILD_STATIC_JAVA_LIBRARY) |