| /* |
| * 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)"); |
| } |
| } |
| } |
| } |
| } |
| } |