diff options
-rw-r--r-- | camera2/Android.mk | 15 | ||||
-rw-r--r-- | camera2/public/Android.mk | 26 | ||||
-rw-r--r-- | camera2/public/src/com/android/ex/camera2/blocking/BlockingCameraManager.java | 347 | ||||
-rw-r--r-- | camera2/public/src/com/android/ex/camera2/exceptions/TimeoutRuntimeException.java | 33 |
4 files changed, 421 insertions, 0 deletions
diff --git a/camera2/Android.mk b/camera2/Android.mk new file mode 100644 index 0000000..9ac4a8a --- /dev/null +++ b/camera2/Android.mk @@ -0,0 +1,15 @@ +# 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. + +include $(call all-subdir-makefiles) diff --git a/camera2/public/Android.mk b/camera2/public/Android.mk new file mode 100644 index 0000000..a0d1489 --- /dev/null +++ b/camera2/public/Android.mk @@ -0,0 +1,26 @@ +# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE := android-ex-camera2 +LOCAL_SDK_VERSION := current + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/camera2/public/src/com/android/ex/camera2/blocking/BlockingCameraManager.java b/camera2/public/src/com/android/ex/camera2/blocking/BlockingCameraManager.java new file mode 100644 index 0000000..de036e1 --- /dev/null +++ b/camera2/public/src/com/android/ex/camera2/blocking/BlockingCameraManager.java @@ -0,0 +1,347 @@ +/* + * 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 when timing out.</p> + */ +public class BlockingCameraManager { + + private static final String TAG = "CameraBlockingOpener"; + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + private static final int OPEN_TIME_OUT = 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.StateListener#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.StateListener#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.StateListener#onDisconnected(CameraDevice)} or + * ({@link CameraDevice.StateListener#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.StateListener 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 StateListener changes to the proxy.</p> + * + * <p>Time out after {@link #OPEN_TIME_OUT} and unblock. Clean up camera if it arrives + * later.</p> + */ + private class OpenListener extends CameraDevice.StateListener { + private static final int ERROR_UNINITIALIZED = -1; + + private final String mCameraId; + + private final CameraDevice.StateListener 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.StateListener 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) { + assertInitialState(); + 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 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); + } + + CameraDevice blockUntilOpen() throws BlockingOpenException { + /** + * Block until onOpened, onError, or onDisconnected + */ + if (!mDeviceReady.block(OPEN_TIME_OUT)) { + + 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, 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)"); + } + } + } + } + } +} diff --git a/camera2/public/src/com/android/ex/camera2/exceptions/TimeoutRuntimeException.java b/camera2/public/src/com/android/ex/camera2/exceptions/TimeoutRuntimeException.java new file mode 100644 index 0000000..c86b5fd --- /dev/null +++ b/camera2/public/src/com/android/ex/camera2/exceptions/TimeoutRuntimeException.java @@ -0,0 +1,33 @@ +/* + * 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.exceptions; + +/** + * Used instead of TimeoutException when something times out in a non-recoverable manner. + * + * <p>This typically happens due to a deadlock or bug in the camera service, + * so please file a bug for this. This should never ever happen in normal operation, which is + * why this exception is unchecked.</p> + */ +public class TimeoutRuntimeException extends RuntimeException { + public TimeoutRuntimeException(String message) { + super(message); + } + + public TimeoutRuntimeException(String message, Throwable cause) { + super(message, cause); + } +} |