blob: 21014d0395e1697b54cb5c34615d41442e2ae861 [file] [log] [blame]
/*
* Copyright 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.blocking;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.android.ex.camera2.exceptions.TimeoutRuntimeException;
import java.util.Objects;
/**
* Expose {@link CameraManager} functionality with blocking functions.
*
* <p>Safe to use at the same time as the regular CameraManager, so this does not
* duplicate any functionality that is already blocking.</p>
*
* <p>Be careful when using this from UI thread! This function will typically block
* for about 500ms when successful, and as long as {@value #OPEN_TIME_OUT_MS}ms when timing out.</p>
*/
public class BlockingCameraManager {
private static final String TAG = "BlockingCameraManager";
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
private static final int OPEN_TIME_OUT_MS = 2000; // ms time out for openCamera
/**
* Exception thrown by {@link #openCamera} if the open fails asynchronously.
*/
public static class BlockingOpenException extends Exception {
/**
* Suppress Eclipse warning
*/
private static final long serialVersionUID = 12397123891238912L;
public static final int ERROR_DISCONNECTED = 0; // Does not clash with ERROR_...
private final int mError;
public boolean wasDisconnected() {
return mError == ERROR_DISCONNECTED;
}
public boolean wasError() {
return mError != ERROR_DISCONNECTED;
}
/**
* Returns the error code {@link ERROR_DISCONNECTED} if disconnected, or one of
* {@code CameraDevice.StateCallback#ERROR_*} if there was another error.
*
* @return int Disconnect/error code
*/
public int getCode() {
return mError;
}
/**
* Thrown when camera device enters error state during open, or if
* it disconnects.
*
* @param errorCode
* @param message
*
* @see {@link CameraDevice.StateCallback#ERROR_CAMERA_DEVICE}
*/
public BlockingOpenException(int errorCode, String message) {
super(message);
mError = errorCode;
}
}
private final CameraManager mManager;
/**
* Create a new blocking camera manager.
*
* @param manager
* CameraManager returned by
* {@code Context.getSystemService(Context.CAMERA_SERVICE)}
*/
public BlockingCameraManager(CameraManager manager) {
if (manager == null) {
throw new IllegalArgumentException("manager must not be null");
}
mManager = manager;
}
/**
* Open the camera, blocking it until it succeeds or fails.
*
* <p>Note that the Handler provided must not be null. Furthermore, if there is a handler,
* its Looper must not be the current thread's Looper. Otherwise we'd never receive
* the callbacks from the CameraDevice since this function would prevent them from being
* processed.</p>
*
* <p>Throws {@link CameraAccessException} for the same reason {@link CameraManager#openCamera}
* does.</p>
*
* <p>Throws {@link BlockingOpenException} when the open fails asynchronously (due to
* {@link CameraDevice.StateCallback#onDisconnected(CameraDevice)} or
* ({@link CameraDevice.StateCallback#onError(CameraDevice)}.</p>
*
* <p>Throws {@link TimeoutRuntimeException} if opening times out. This is usually
* highly unrecoverable, and all future calls to opening that camera will fail since the
* service will think it's busy. This class will do its best to clean up eventually.</p>
*
* @param cameraId
* Id of the camera
* @param listener
* Listener to the camera. onOpened, onDisconnected, onError need not be implemented.
* @param handler
* Handler which to run the listener on. Must not be null.
*
* @return CameraDevice
*
* @throws IllegalArgumentException
* If the handler is null, or if the handler's looper is current.
* @throws CameraAccessException
* If open fails immediately.
* @throws BlockingOpenException
* If open fails after blocking for some amount of time.
* @throws TimeoutRuntimeException
* If opening times out. Typically unrecoverable.
*/
public CameraDevice openCamera(String cameraId, CameraDevice.StateCallback listener,
Handler handler) throws CameraAccessException, BlockingOpenException {
if (handler == null) {
throw new IllegalArgumentException("handler must not be null");
} else if (handler.getLooper() == Looper.myLooper()) {
throw new IllegalArgumentException("handler's looper must not be the current looper");
}
return (new OpenListener(mManager, cameraId, listener, handler)).blockUntilOpen();
}
private static void assertEquals(Object a, Object b) {
if (!Objects.equals(a, b)) {
throw new AssertionError("Expected " + a + ", but got " + b);
}
}
/**
* Block until CameraManager#openCamera finishes with onOpened/onError/onDisconnected
*
* <p>Pass-through all StateCallback changes to the proxy.</p>
*
* <p>Time out after {@link #OPEN_TIME_OUT_MS} and unblock. Clean up camera if it arrives
* later.</p>
*/
private class OpenListener extends CameraDevice.StateCallback {
private static final int ERROR_UNINITIALIZED = -1;
private final String mCameraId;
private final CameraDevice.StateCallback mProxy;
private final Object mLock = new Object();
private final ConditionVariable mDeviceReady = new ConditionVariable();
private CameraDevice mDevice = null;
private boolean mSuccess = false;
private int mError = ERROR_UNINITIALIZED;
private boolean mDisconnected = false;
private boolean mNoReply = true; // Start with no reply until proven otherwise
private boolean mTimedOut = false;
OpenListener(CameraManager manager, String cameraId,
CameraDevice.StateCallback listener, Handler handler)
throws CameraAccessException {
mCameraId = cameraId;
mProxy = listener;
manager.openCamera(cameraId, this, handler);
}
// Freebie check to make sure we aren't calling functions multiple times.
// We should still test the state interactions in a separate more thorough test.
private void assertInitialState() {
assertEquals(null, mDevice);
assertEquals(false, mDisconnected);
assertEquals(ERROR_UNINITIALIZED, mError);
assertEquals(false, mSuccess);
}
@Override
public void onOpened(CameraDevice camera) {
if (VERBOSE) {
Log.v(TAG, "onOpened: camera " + ((camera != null) ? camera.getId() : "null"));
}
synchronized (mLock) {
assertInitialState();
mNoReply = false;
mSuccess = true;
mDevice = camera;
mDeviceReady.open();
if (mTimedOut && camera != null) {
camera.close();
return;
}
}
if (mProxy != null) mProxy.onOpened(camera);
}
@Override
public void onDisconnected(CameraDevice camera) {
if (VERBOSE) {
Log.v(TAG, "onDisconnected: camera "
+ ((camera != null) ? camera.getId() : "null"));
}
synchronized (mLock) {
// Don't assert all initial states. onDisconnected can be called after camera
// is successfully opened.
assertEquals(false, mDisconnected);
mNoReply = false;
mDisconnected = true;
mDevice = camera;
mDeviceReady.open();
if (mTimedOut && camera != null) {
camera.close();
return;
}
}
if (mProxy != null) mProxy.onDisconnected(camera);
}
@Override
public void onError(CameraDevice camera, int error) {
if (VERBOSE) {
Log.v(TAG, "onError: camera " + ((camera != null) ? camera.getId() : "null"));
}
if (error <= 0) {
throw new AssertionError("Expected error to be a positive number");
}
synchronized (mLock) {
// Don't assert initial state. Error can happen later.
mNoReply = false;
mError = error;
mDevice = camera;
mDeviceReady.open();
if (mTimedOut && camera != null) {
camera.close();
return;
}
}
if (mProxy != null) mProxy.onError(camera, error);
}
@Override
public void onClosed(CameraDevice camera) {
if (mProxy != null) mProxy.onClosed(camera);
}
CameraDevice blockUntilOpen() throws BlockingOpenException {
/**
* Block until onOpened, onError, or onDisconnected
*/
if (!mDeviceReady.block(OPEN_TIME_OUT_MS)) {
synchronized (mLock) {
if (mNoReply) { // Give the async camera a fighting chance (required)
mTimedOut = true; // Clean up camera if it ever arrives later
throw new TimeoutRuntimeException(String.format(
"Timed out after %d ms while trying to open camera device %s",
OPEN_TIME_OUT_MS, mCameraId));
}
}
}
synchronized (mLock) {
/**
* Determine which state we ended up in:
*
* - Throw exceptions for onError/onDisconnected
* - Return device for onOpened
*/
if (!mSuccess && mDevice != null) {
mDevice.close();
}
if (mSuccess) {
return mDevice;
} else {
if (mDisconnected) {
throw new BlockingOpenException(
BlockingOpenException.ERROR_DISCONNECTED,
"Failed to open camera device: it is disconnected");
} else if (mError != ERROR_UNINITIALIZED) {
throw new BlockingOpenException(
mError,
"Failed to open camera device: error code " + mError);
} else {
throw new AssertionError("Failed to open camera device (impl bug)");
}
}
}
}
}
}