/*
* 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 android.hardware.camera2.legacy;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.impl.CaptureResultExtras;
import android.hardware.camera2.ICameraDeviceCallbacks;
import android.hardware.camera2.utils.LongParcelable;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.utils.CameraRuntimeException;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.RemoteException;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static android.hardware.camera2.legacy.LegacyExceptionUtils.*;
import static android.hardware.camera2.utils.CameraBinderDecorator.*;
import static com.android.internal.util.Preconditions.*;
/**
* This class emulates the functionality of a Camera2 device using a the old Camera class.
*
*
* There are two main components that are used to implement this:
* - A state machine containing valid Camera2 device states ({@link CameraDeviceState}).
* - A message-queue based pipeline that manages an old Camera class, and executes capture and
* configuration requests.
*
*/
public class LegacyCameraDevice implements AutoCloseable {
public static final String DEBUG_PROP = "HAL1ShimLogging";
private final String TAG;
private static final boolean DEBUG = false;
private final int mCameraId;
private final ICameraDeviceCallbacks mDeviceCallbacks;
private final CameraDeviceState mDeviceState = new CameraDeviceState();
private List mConfiguredSurfaces;
private final ConditionVariable mIdle = new ConditionVariable(/*open*/true);
private final HandlerThread mResultThread = new HandlerThread("ResultThread");
private final HandlerThread mCallbackHandlerThread = new HandlerThread("CallbackThread");
private final Handler mCallbackHandler;
private final Handler mResultHandler;
private static final int ILLEGAL_VALUE = -1;
private CaptureResultExtras getExtrasFromRequest(RequestHolder holder) {
if (holder == null) {
return new CaptureResultExtras(ILLEGAL_VALUE, ILLEGAL_VALUE, ILLEGAL_VALUE,
ILLEGAL_VALUE, ILLEGAL_VALUE, ILLEGAL_VALUE);
}
return new CaptureResultExtras(holder.getRequestId(), holder.getSubsequeceId(),
/*afTriggerId*/0, /*precaptureTriggerId*/0, holder.getFrameNumber(),
/*partialResultCount*/1);
}
/**
* Listener for the camera device state machine. Calls the appropriate
* {@link ICameraDeviceCallbacks} for each state transition.
*/
private final CameraDeviceState.CameraDeviceStateListener mStateListener =
new CameraDeviceState.CameraDeviceStateListener() {
@Override
public void onError(final int errorCode, RequestHolder holder) {
mIdle.open();
final CaptureResultExtras extras = getExtrasFromRequest(holder);
mResultHandler.post(new Runnable() {
@Override
public void run() {
if (DEBUG) {
Log.d(TAG, "doing onError callback.");
}
try {
mDeviceCallbacks.onCameraError(errorCode, extras);
} catch (RemoteException e) {
throw new IllegalStateException(
"Received remote exception during onCameraError callback: ", e);
}
}
});
}
@Override
public void onConfiguring() {
// Do nothing
if (DEBUG) {
Log.d(TAG, "doing onConfiguring callback.");
}
}
@Override
public void onIdle() {
mIdle.open();
mResultHandler.post(new Runnable() {
@Override
public void run() {
if (DEBUG) {
Log.d(TAG, "doing onIdle callback.");
}
try {
mDeviceCallbacks.onCameraIdle();
} catch (RemoteException e) {
throw new IllegalStateException(
"Received remote exception during onCameraIdle callback: ", e);
}
}
});
}
@Override
public void onCaptureStarted(RequestHolder holder, final long timestamp) {
final CaptureResultExtras extras = getExtrasFromRequest(holder);
mResultHandler.post(new Runnable() {
@Override
public void run() {
if (DEBUG) {
Log.d(TAG, "doing onCaptureStarted callback.");
}
try {
mDeviceCallbacks.onCaptureStarted(extras, timestamp);
} catch (RemoteException e) {
throw new IllegalStateException(
"Received remote exception during onCameraError callback: ", e);
}
}
});
}
@Override
public void onCaptureResult(final CameraMetadataNative result, RequestHolder holder) {
final CaptureResultExtras extras = getExtrasFromRequest(holder);
mResultHandler.post(new Runnable() {
@Override
public void run() {
if (DEBUG) {
Log.d(TAG, "doing onCaptureResult callback.");
}
try {
mDeviceCallbacks.onResultReceived(result, extras);
} catch (RemoteException e) {
throw new IllegalStateException(
"Received remote exception during onCameraError callback: ", e);
}
}
});
}
};
private final RequestThreadManager mRequestThreadManager;
/**
* Check if a given surface uses {@link ImageFormat#YUV_420_888} or format that can be readily
* converted to this; YV12 and NV21 are the two currently supported formats.
*
* @param s the surface to check.
* @return {@code true} if the surfaces uses {@link ImageFormat#YUV_420_888} or a compatible
* format.
*/
static boolean needsConversion(Surface s) throws BufferQueueAbandonedException {
int nativeType = detectSurfaceType(s);
return nativeType == ImageFormat.YUV_420_888 || nativeType == ImageFormat.YV12 ||
nativeType == ImageFormat.NV21;
}
/**
* Create a new emulated camera device from a given Camera 1 API camera.
*
*
* The {@link Camera} provided to this constructor must already have been successfully opened,
* and ownership of the provided camera is passed to this object. No further calls to the
* camera methods should be made following this constructor.
*
*
* @param cameraId the id of the camera.
* @param camera an open {@link Camera} device.
* @param characteristics the static camera characteristics for this camera device
* @param callbacks {@link ICameraDeviceCallbacks} callbacks to call for Camera2 API operations.
*/
public LegacyCameraDevice(int cameraId, Camera camera, CameraCharacteristics characteristics,
ICameraDeviceCallbacks callbacks) {
mCameraId = cameraId;
mDeviceCallbacks = callbacks;
TAG = String.format("CameraDevice-%d-LE", mCameraId);
mResultThread.start();
mResultHandler = new Handler(mResultThread.getLooper());
mCallbackHandlerThread.start();
mCallbackHandler = new Handler(mCallbackHandlerThread.getLooper());
mDeviceState.setCameraDeviceCallbacks(mCallbackHandler, mStateListener);
mRequestThreadManager =
new RequestThreadManager(cameraId, camera, characteristics, mDeviceState);
mRequestThreadManager.start();
}
/**
* Configure the device with a set of output surfaces.
*
* Using empty or {@code null} {@code outputs} is the same as unconfiguring.
*
* Every surface in {@code outputs} must be non-{@code null}.
*
* @param outputs a list of surfaces to set.
* @return an error code for this binder operation, or {@link NO_ERROR}
* on success.
*/
public int configureOutputs(List outputs) {
if (outputs != null) {
for (Surface output : outputs) {
if (output == null) {
Log.e(TAG, "configureOutputs - null outputs are not allowed");
return BAD_VALUE;
}
}
}
int error = mDeviceState.setConfiguring();
if (error == NO_ERROR) {
mRequestThreadManager.configure(outputs);
error = mDeviceState.setIdle();
}
// TODO: May also want to check the surfaces more deeply (e.g. state, formats, sizes..)
if (error == NO_ERROR) {
mConfiguredSurfaces = outputs != null ? new ArrayList<>(outputs) : null;
}
return error;
}
/**
* Submit a burst of capture requests.
*
* @param requestList a list of capture requests to execute.
* @param repeating {@code true} if this burst is repeating.
* @param frameNumber an output argument that contains either the frame number of the last frame
* that will be returned for this request, or the frame number of the last
* frame that will be returned for the current repeating request if this
* burst is set to be repeating.
* @return the request id.
*/
public int submitRequestList(List requestList, boolean repeating,
/*out*/LongParcelable frameNumber) {
if (requestList == null || requestList.isEmpty()) {
Log.e(TAG, "submitRequestList - Empty/null requests are not allowed");
return BAD_VALUE;
}
List surfaceIds = (mConfiguredSurfaces == null) ? new ArrayList() :
getSurfaceIds(mConfiguredSurfaces);
// Make sure that there all requests have at least 1 surface; all surfaces are non-null
for (CaptureRequest request : requestList) {
if (request.getTargets().isEmpty()) {
Log.e(TAG, "submitRequestList - "
+ "Each request must have at least one Surface target");
return BAD_VALUE;
}
for (Surface surface : request.getTargets()) {
if (surface == null) {
Log.e(TAG, "submitRequestList - Null Surface targets are not allowed");
return BAD_VALUE;
} else if (mConfiguredSurfaces == null) {
Log.e(TAG, "submitRequestList - must configure " +
" device with valid surfaces before submitting requests");
return INVALID_OPERATION;
} else if (!containsSurfaceId(surface, surfaceIds)) {
Log.e(TAG, "submitRequestList - cannot use a surface that wasn't configured");
return BAD_VALUE;
}
}
}
// TODO: further validation of request here
mIdle.close();
return mRequestThreadManager.submitCaptureRequests(requestList, repeating,
frameNumber);
}
/**
* Submit a single capture request.
*
* @param request the capture request to execute.
* @param repeating {@code true} if this request is repeating.
* @param frameNumber an output argument that contains either the frame number of the last frame
* that will be returned for this request, or the frame number of the last
* frame that will be returned for the current repeating request if this
* request is set to be repeating.
* @return the request id.
*/
public int submitRequest(CaptureRequest request, boolean repeating,
/*out*/LongParcelable frameNumber) {
ArrayList requestList = new ArrayList();
requestList.add(request);
return submitRequestList(requestList, repeating, frameNumber);
}
/**
* Cancel the repeating request with the given request id.
*
* @param requestId the request id of the request to cancel.
* @return the last frame number to be returned from the HAL for the given repeating request, or
* {@code INVALID_FRAME} if none exists.
*/
public long cancelRequest(int requestId) {
return mRequestThreadManager.cancelRepeating(requestId);
}
/**
* Block until the {@link ICameraDeviceCallbacks#onCameraIdle()} callback is received.
*/
public void waitUntilIdle() {
mIdle.block();
}
@Override
public void close() {
mRequestThreadManager.quit();
mCallbackHandlerThread.quitSafely();
mResultThread.quitSafely();
try {
mCallbackHandlerThread.join();
} catch (InterruptedException e) {
Log.e(TAG, String.format("Thread %s (%d) interrupted while quitting.",
mCallbackHandlerThread.getName(), mCallbackHandlerThread.getId()));
}
try {
mResultThread.join();
} catch (InterruptedException e) {
Log.e(TAG, String.format("Thread %s (%d) interrupted while quitting.",
mResultThread.getName(), mResultThread.getId()));
}
// TODO: throw IllegalStateException in every method after close has been called
}
@Override
protected void finalize() throws Throwable {
try {
close();
} catch (CameraRuntimeException e) {
Log.e(TAG, "Got error while trying to finalize, ignoring: " + e.getMessage());
} finally {
super.finalize();
}
}
/**
* Query the surface for its currently configured default buffer size.
* @param surface a non-{@code null} {@code Surface}
* @return the width and height of the surface
*
* @throws NullPointerException if the {@code surface} was {@code null}
* @throws IllegalStateException if the {@code surface} was invalid
*/
static Size getSurfaceSize(Surface surface) throws BufferQueueAbandonedException {
checkNotNull(surface);
int[] dimens = new int[2];
LegacyExceptionUtils.throwOnError(nativeDetectSurfaceDimens(surface, /*out*/dimens));
return new Size(dimens[0], dimens[1]);
}
static int detectSurfaceType(Surface surface) throws BufferQueueAbandonedException {
checkNotNull(surface);
return LegacyExceptionUtils.throwOnError(nativeDetectSurfaceType(surface));
}
static void configureSurface(Surface surface, int width, int height,
int pixelFormat) throws BufferQueueAbandonedException {
checkNotNull(surface);
checkArgumentPositive(width, "width must be positive.");
checkArgumentPositive(height, "height must be positive.");
LegacyExceptionUtils.throwOnError(nativeConfigureSurface(surface, width, height,
pixelFormat));
}
static void produceFrame(Surface surface, byte[] pixelBuffer, int width,
int height, int pixelFormat)
throws BufferQueueAbandonedException {
checkNotNull(surface);
checkNotNull(pixelBuffer);
checkArgumentPositive(width, "width must be positive.");
checkArgumentPositive(height, "height must be positive.");
LegacyExceptionUtils.throwOnError(nativeProduceFrame(surface, pixelBuffer, width, height,
pixelFormat));
}
static void setSurfaceFormat(Surface surface, int pixelFormat)
throws BufferQueueAbandonedException {
checkNotNull(surface);
LegacyExceptionUtils.throwOnError(nativeSetSurfaceFormat(surface, pixelFormat));
}
static void setSurfaceDimens(Surface surface, int width, int height)
throws BufferQueueAbandonedException {
checkNotNull(surface);
checkArgumentPositive(width, "width must be positive.");
checkArgumentPositive(height, "height must be positive.");
LegacyExceptionUtils.throwOnError(nativeSetSurfaceDimens(surface, width, height));
}
static long getSurfaceId(Surface surface) {
checkNotNull(surface);
return nativeGetSurfaceId(surface);
}
static List getSurfaceIds(Collection surfaces) {
if (surfaces == null) {
throw new NullPointerException("Null argument surfaces");
}
List surfaceIds = new ArrayList<>();
for (Surface s : surfaces) {
long id = getSurfaceId(s);
if (id == 0) {
throw new IllegalStateException(
"Configured surface had null native GraphicBufferProducer pointer!");
}
surfaceIds.add(id);
}
return surfaceIds;
}
static boolean containsSurfaceId(Surface s, List ids) {
long id = getSurfaceId(s);
return ids.contains(id);
}
static void setSurfaceOrientation(Surface surface, int facing, int sensorOrientation)
throws BufferQueueAbandonedException {
checkNotNull(surface);
LegacyExceptionUtils.throwOnError(nativeSetSurfaceOrientation(surface, facing,
sensorOrientation));
}
static Size getTextureSize(SurfaceTexture surfaceTexture)
throws BufferQueueAbandonedException {
checkNotNull(surfaceTexture);
int[] dimens = new int[2];
LegacyExceptionUtils.throwOnError(nativeDetectTextureDimens(surfaceTexture,
/*out*/dimens));
return new Size(dimens[0], dimens[1]);
}
static void setNextTimestamp(Surface surface, long timestamp)
throws BufferQueueAbandonedException {
checkNotNull(surface);
LegacyExceptionUtils.throwOnError(nativeSetNextTimestamp(surface, timestamp));
}
private static native int nativeDetectSurfaceType(Surface surface);
private static native int nativeDetectSurfaceDimens(Surface surface,
/*out*/int[/*2*/] dimens);
private static native int nativeConfigureSurface(Surface surface, int width, int height,
int pixelFormat);
private static native int nativeProduceFrame(Surface surface, byte[] pixelBuffer, int width,
int height, int pixelFormat);
private static native int nativeSetSurfaceFormat(Surface surface, int pixelFormat);
private static native int nativeSetSurfaceDimens(Surface surface, int width, int height);
private static native long nativeGetSurfaceId(Surface surface);
private static native int nativeSetSurfaceOrientation(Surface surface, int facing,
int sensorOrientation);
private static native int nativeDetectTextureDimens(SurfaceTexture surfaceTexture,
/*out*/int[/*2*/] dimens);
private static native int nativeSetNextTimestamp(Surface surface, long timestamp);
}