diff options
| author | Igor Murashkin <iam@google.com> | 2014-05-30 09:45:05 -0700 |
|---|---|---|
| committer | Igor Murashkin <iam@google.com> | 2014-06-03 14:55:52 -0700 |
| commit | 0b27d3453d5e257594792e9177c5fedb1bc6f9e9 (patch) | |
| tree | 1ed0d8390d244ce144b553f792beb24e4a48cac5 /core/java/android | |
| parent | 5743868e15c88d2886915163652a1571a218bf39 (diff) | |
| download | frameworks_base-0b27d3453d5e257594792e9177c5fedb1bc6f9e9.zip frameworks_base-0b27d3453d5e257594792e9177c5fedb1bc6f9e9.tar.gz frameworks_base-0b27d3453d5e257594792e9177c5fedb1bc6f9e9.tar.bz2 | |
camera2: Implement most of CameraCaptureSession
Bug: 14964443
Change-Id: I8203842c77a94a3a6e5f89494fce658b00a4160d
Diffstat (limited to 'core/java/android')
16 files changed, 1612 insertions, 33 deletions
diff --git a/core/java/android/hardware/camera2/CameraCaptureSession.java b/core/java/android/hardware/camera2/CameraCaptureSession.java index 7738d2d..d62958f 100644 --- a/core/java/android/hardware/camera2/CameraCaptureSession.java +++ b/core/java/android/hardware/camera2/CameraCaptureSession.java @@ -54,7 +54,7 @@ import java.util.List; public abstract class CameraCaptureSession implements AutoCloseable { /** - * Get the camera device that this session is created for + * Get the camera device that this session is created for. */ public abstract CameraDevice getDevice(); @@ -90,8 +90,9 @@ public abstract class CameraCaptureSession implements AutoCloseable { * * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error - * @throws IllegalStateException if this session is no longer active, either because a new - * session has been created or the camera device has been closed. + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. * @throws IllegalArgumentException if the request targets Surfaces that are not configured as * outputs for this session. Or if the handler is null, the * listener is not null, and the calling thread has no looper. @@ -99,6 +100,7 @@ public abstract class CameraCaptureSession implements AutoCloseable { * @see #captureBurst * @see #setRepeatingRequest * @see #setRepeatingBurst + * @see #abortCaptures */ public abstract int capture(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -132,8 +134,9 @@ public abstract class CameraCaptureSession implements AutoCloseable { * * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error - * @throws IllegalStateException if this session is no longer active, either because a new - * session has been created or the camera device has been closed. + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. * @throws IllegalArgumentException If the requests target Surfaces not currently configured as * outputs. Or if the handler is null, the listener is not * null, and the calling thread has no looper. @@ -141,6 +144,7 @@ public abstract class CameraCaptureSession implements AutoCloseable { * @see #capture * @see #setRepeatingRequest * @see #setRepeatingBurst + * @see #abortCaptures */ public abstract int captureBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -188,11 +192,13 @@ public abstract class CameraCaptureSession implements AutoCloseable { * * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error - * @throws IllegalStateException if this session is no longer active, either because a new - * session has been created or the camera device has been closed. + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. * @throws IllegalArgumentException If the requests reference Surfaces that are not currently * configured as outputs. Or if the handler is null, the * listener is not null, and the calling thread has no looper. + * Or if no requests were passed in. * * @see #capture * @see #captureBurst @@ -246,11 +252,13 @@ public abstract class CameraCaptureSession implements AutoCloseable { * * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error - * @throws IllegalStateException if this session is no longer active, either because a new - * session has been created or the camera device has been closed. + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. * @throws IllegalArgumentException If the requests reference Surfaces not currently configured * as outputs. Or if the handler is null, the listener is not - * null, and the calling thread has no looper. + * null, and the calling thread has no looper. Or if no + * requests were passed in. * * @see #capture * @see #captureBurst @@ -274,8 +282,9 @@ public abstract class CameraCaptureSession implements AutoCloseable { * * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error - * @throws IllegalStateException if this session is no longer active, either because a new - * session has been created or the camera device has been closed. + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. * * @see #setRepeatingRequest * @see #setRepeatingBurst @@ -308,8 +317,9 @@ public abstract class CameraCaptureSession implements AutoCloseable { * * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error - * @throws IllegalStateException if this session is no longer active, either because a new - * session has been created or the camera device has been closed. + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. * * @see #setRepeatingRequest * @see #setRepeatingBurst @@ -320,8 +330,8 @@ public abstract class CameraCaptureSession implements AutoCloseable { /** * Close this capture session asynchronously. * - * <p>Closing a session frees up the target output Surfaces of the session for reuse with either a - * new session, or to other APIs that can draw to Surfaces.</p> + * <p>Closing a session frees up the target output Surfaces of the session for reuse with either + * a new session, or to other APIs that can draw to Surfaces.</p> * * <p>Note that creating a new capture session with {@link CameraDevice#createCaptureSession} * will close any existing capture session automatically, and call the older session listener's @@ -334,6 +344,8 @@ public abstract class CameraCaptureSession implements AutoCloseable { * However, any in-progress capture requests submitted to the session will be completed as * normal; once all captures have completed and the session has been torn down, * {@link StateListener#onClosed} will be called.</p> + * + * <p>Closing a session is idempotent; closing more than once has no effect.</p> */ @Override public abstract void close(); diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 6f5099b..f9f617a 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -244,6 +244,7 @@ public interface CameraDevice extends AutoCloseable { * @see StreamConfigurationMap#getOutputSizes(Class) * @deprecated Use {@link #createCaptureSession} instead */ + @Deprecated public void configureOutputs(List<Surface> outputs) throws CameraAccessException; /** @@ -432,6 +433,7 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingBurst * @deprecated Use {@link CameraCaptureSession} instead */ + @Deprecated public int capture(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -470,13 +472,15 @@ public interface CameraDevice extends AutoCloseable { * or the camera device has been closed. * @throws IllegalArgumentException If the requests target Surfaces not * currently configured as outputs. Or if the handler is null, the listener - * is not null, and the calling thread has no looper. + * is not null, and the calling thread has no looper. Or if no requests were + * passed in. * * @see #capture * @see #setRepeatingRequest * @see #setRepeatingBurst * @deprecated Use {@link CameraCaptureSession} instead */ + @Deprecated public int captureBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -536,6 +540,7 @@ public interface CameraDevice extends AutoCloseable { * @see #flush * @deprecated Use {@link CameraCaptureSession} instead */ + @Deprecated public int setRepeatingRequest(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -586,7 +591,8 @@ public interface CameraDevice extends AutoCloseable { * or the camera device has been closed. * @throws IllegalArgumentException If the requests reference Surfaces not * currently configured as outputs. Or if the handler is null, the listener - * is not null, and the calling thread has no looper. + * is not null, and the calling thread has no looper. Or if no requests were + * passed in. * * @see #capture * @see #captureBurst @@ -595,6 +601,7 @@ public interface CameraDevice extends AutoCloseable { * @see #flush * @deprecated Use {@link CameraCaptureSession} instead */ + @Deprecated public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -620,6 +627,7 @@ public interface CameraDevice extends AutoCloseable { * @see StateListener#onIdle * @deprecated Use {@link CameraCaptureSession} instead */ + @Deprecated public void stopRepeating() throws CameraAccessException; /** @@ -657,6 +665,7 @@ public interface CameraDevice extends AutoCloseable { * @see #configureOutputs * @deprecated Use {@link CameraCaptureSession} instead */ + @Deprecated public void flush() throws CameraAccessException; /** @@ -691,6 +700,7 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingBurst * @deprecated Use {@link CameraCaptureSession} instead */ + @Deprecated public static abstract class CaptureListener { /** @@ -1042,6 +1052,7 @@ public interface CameraDevice extends AutoCloseable { * @param camera the camera device has that become unconfigured * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ + @Deprecated public void onUnconfigured(CameraDevice camera) { // Default empty implementation } @@ -1072,6 +1083,7 @@ public interface CameraDevice extends AutoCloseable { * @see CameraDevice#setRepeatingRequest * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ + @Deprecated public void onActive(CameraDevice camera) { // Default empty implementation } @@ -1106,6 +1118,7 @@ public interface CameraDevice extends AutoCloseable { * @see CameraDevice#flush * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ + @Deprecated public void onBusy(CameraDevice camera) { // Default empty implementation } @@ -1154,6 +1167,7 @@ public interface CameraDevice extends AutoCloseable { * @see CameraDevice#flush * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ + @Deprecated public void onIdle(CameraDevice camera) { // Default empty implementation } diff --git a/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java b/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java new file mode 100644 index 0000000..fe575b2 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java @@ -0,0 +1,64 @@ +/* + * 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.dispatch; + + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import static com.android.internal.util.Preconditions.*; + +/** + * Broadcast a single dispatch into multiple other dispatchables. + * + * <p>Every time {@link #dispatch} is invoked, all the broadcast targets will + * see the same dispatch as well. The first target's return value is returned.</p> + * + * <p>This enables a single listener to be converted into a multi-listener.</p> + */ +public class BroadcastDispatcher<T> implements Dispatchable<T> { + + private final List<Dispatchable<T>> mDispatchTargets; + + /** + * Create a broadcast dispatcher from the supplied dispatch targets. + * + * @param dispatchTargets one or more targets to dispatch to + */ + @SafeVarargs + public BroadcastDispatcher(Dispatchable<T>... dispatchTargets) { + mDispatchTargets = Arrays.asList( + checkNotNull(dispatchTargets, "dispatchTargets must not be null")); + } + + @Override + public Object dispatch(Method method, Object[] args) throws Throwable { + Object result = null; + boolean gotResult = false; + + for (Dispatchable<T> dispatchTarget : mDispatchTargets) { + Object localResult = dispatchTarget.dispatch(method, args); + + if (!gotResult) { + gotResult = true; + result = localResult; + } + } + + return result; + } +} diff --git a/core/java/android/hardware/camera2/dispatch/Dispatchable.java b/core/java/android/hardware/camera2/dispatch/Dispatchable.java new file mode 100644 index 0000000..753103f --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/Dispatchable.java @@ -0,0 +1,35 @@ +/* + * 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.dispatch; + +import java.lang.reflect.Method; + +/** + * Dynamically dispatch a method and its argument to some object. + * + * <p>This can be used to intercept method calls and do work around them, redirect work, + * or block calls entirely.</p> + */ +public interface Dispatchable<T> { + /** + * Dispatch the method and arguments to this object. + * @param method a method defined in class {@code T} + * @param args arguments corresponding to said {@code method} + * @return the object returned when invoking {@code method} + * @throws Throwable any exception that might have been raised while invoking the method + */ + public Object dispatch(Method method, Object[] args) throws Throwable; +} diff --git a/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.java b/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.java new file mode 100644 index 0000000..75f97e4 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.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 android.hardware.camera2.dispatch; + + +import java.lang.reflect.Method; + +import static com.android.internal.util.Preconditions.*; + +/** + * Duck typing dispatcher; converts dispatch methods calls from one class to another by + * looking up equivalently methods at runtime by name. + * + * <p>For example, if two types have identical method names and arguments, but + * are not subclasses/subinterfaces of each other, this dispatcher will allow calls to be + * made from one type to the other.</p> + * + * @param <TFrom> source dispatch type, whose methods with {@link #dispatch} will be called + * @param <T> destination dispatch type, methods will be converted to the class of {@code T} + */ +public class DuckTypingDispatcher<TFrom, T> implements Dispatchable<TFrom> { + + private final MethodNameInvoker<T> mDuck; + + /** + * Create a new duck typing dispatcher. + * + * @param target destination dispatch type, methods will be redirected to this dispatcher + * @param targetClass destination dispatch class, methods will be converted to this class's + */ + public DuckTypingDispatcher(Dispatchable<T> target, Class<T> targetClass) { + checkNotNull(targetClass, "targetClass must not be null"); + checkNotNull(target, "target must not be null"); + + mDuck = new MethodNameInvoker<T>(target, targetClass); + } + + @Override + public Object dispatch(Method method, Object[] args) { + return mDuck.invoke(method.getName(), args); + } +} diff --git a/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java b/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java new file mode 100644 index 0000000..f8e9d49 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java @@ -0,0 +1,85 @@ +/* + * 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.dispatch; + +import android.hardware.camera2.utils.UncheckedThrow; +import android.os.Handler; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static com.android.internal.util.Preconditions.*; + +/** + * Forward all interface calls into a handler by posting it as a {@code Runnable}. + * + * <p>All calls will return immediately; functions with return values will return a default + * value of {@code null}, {@code 0}, or {@code false} where that value is legal.</p> + * + * <p>Any exceptions thrown on the handler while trying to invoke a method + * will be re-thrown. Throwing checked exceptions on a handler which doesn't expect any + * checked exceptions to be thrown will result in "undefined" behavior + * (although in practice it is usually thrown as normal).</p> + */ +public class HandlerDispatcher<T> implements Dispatchable<T> { + + private static final String TAG = "HandlerDispatcher"; + + private final Dispatchable<T> mDispatchTarget; + private final Handler mHandler; + + /** + * Create a dispatcher that forwards it's dispatch calls by posting + * them onto the {@code handler} as a {@code Runnable}. + * + * @param dispatchTarget the destination whose method calls will be redirected into the handler + * @param handler all calls into {@code dispatchTarget} will be posted onto this handler + * @param <T> the type of the element you want to wrap. + * @return a dispatcher that will forward it's dispatch calls to a handler + */ + public HandlerDispatcher(Dispatchable<T> dispatchTarget, Handler handler) { + mDispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null"); + mHandler = checkNotNull(handler, "handler must not be null"); + } + + @Override + public Object dispatch(final Method method, final Object[] args) throws Throwable { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + mDispatchTarget.dispatch(method, args); + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + // Potential UB. Hopefully 't' is a runtime exception. + UncheckedThrow.throwAnyException(t); + } catch (IllegalAccessException e) { + // Impossible + Log.wtf(TAG, "IllegalAccessException while invoking " + method, e); + } catch (IllegalArgumentException e) { + // Impossible + Log.wtf(TAG, "IllegalArgumentException while invoking " + method, e); + } catch (Throwable e) { + UncheckedThrow.throwAnyException(e); + } + } + }); + + // TODO handle primitive return values that would avoid NPE if unboxed + return null; + } +} diff --git a/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.java b/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.java new file mode 100644 index 0000000..ac5f526 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.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 android.hardware.camera2.dispatch; + +import android.hardware.camera2.utils.UncheckedThrow; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static com.android.internal.util.Preconditions.*; + + +public class InvokeDispatcher<T> implements Dispatchable<T> { + + private static final String TAG = "InvocationSink"; + private final T mTarget; + + public InvokeDispatcher(T target) { + mTarget = checkNotNull(target, "target must not be null"); + } + + @Override + public Object dispatch(Method method, Object[] args) { + try { + return method.invoke(mTarget, args); + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + // Potential UB. Hopefully 't' is a runtime exception. + UncheckedThrow.throwAnyException(t); + } catch (IllegalAccessException e) { + // Impossible + Log.wtf(TAG, "IllegalAccessException while invoking " + method, e); + } catch (IllegalArgumentException e) { + // Impossible + Log.wtf(TAG, "IllegalArgumentException while invoking " + method, e); + } + + // unreachable + return null; + } +} diff --git a/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.java b/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.java new file mode 100644 index 0000000..02c3d87 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.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 android.hardware.camera2.dispatch; + +import android.hardware.camera2.utils.UncheckedThrow; + +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; + +import static com.android.internal.util.Preconditions.*; + +/** + * Invoke a method on a dispatchable by its name (without knowing the {@code Method} ahead of time). + * + * @param <T> destination dispatch type, methods will be looked up in the class of {@code T} + */ +public class MethodNameInvoker<T> { + + private final Dispatchable<T> mTarget; + private final Class<T> mTargetClass; + private final ConcurrentHashMap<String, Method> mMethods = + new ConcurrentHashMap<>(); + + /** + * Create a new method name invoker. + * + * @param target destination dispatch type, invokes will be redirected to this dispatcher + * @param targetClass destination dispatch class, the invoked methods will be from this class + */ + public MethodNameInvoker(Dispatchable<T> target, Class<T> targetClass) { + mTargetClass = targetClass; + mTarget = target; + } + + /** + * Invoke a method by its name. + * + * <p>If more than one method exists in {@code targetClass}, the first method will be used.</p> + * + * @param methodName + * The name of the method, which will be matched 1:1 to the destination method + * @param params + * Variadic parameter list. + * @return + * The same kind of value that would normally be returned by calling {@code methodName} + * statically. + * + * @throws IllegalArgumentException if {@code methodName} does not exist on the target class + * @throws Throwable will rethrow anything that the target method would normally throw + */ + @SuppressWarnings("unchecked") + public <K> K invoke(String methodName, Object... params) { + checkNotNull(methodName, "methodName must not be null"); + + Method targetMethod = mMethods.get(methodName); + if (targetMethod == null) { + for (Method method : mTargetClass.getMethods()) { + // TODO future: match by # of params and types of params if possible + if (method.getName().equals(methodName)) { + targetMethod = method; + mMethods.put(methodName, targetMethod); + break; + } + } + + if (targetMethod == null) { + throw new IllegalArgumentException( + "Method " + methodName + " does not exist on class " + mTargetClass); + } + } + + try { + return (K) mTarget.dispatch(targetMethod, params); + } catch (Throwable e) { + UncheckedThrow.throwAnyException(e); + // unreachable + return null; + } + } +} diff --git a/core/java/android/hardware/camera2/dispatch/NullDispatcher.java b/core/java/android/hardware/camera2/dispatch/NullDispatcher.java new file mode 100644 index 0000000..fada075 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/NullDispatcher.java @@ -0,0 +1,38 @@ +/* + * 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.dispatch; + + +import java.lang.reflect.Method; + +/** + * Do nothing when dispatching; follows the null object pattern. + */ +public class NullDispatcher<T> implements Dispatchable<T> { + /** + * Create a dispatcher that does nothing when dispatched to. + */ + public NullDispatcher() { + } + + /** + * Do nothing; all parameters are ignored. + */ + @Override + public Object dispatch(Method method, Object[] args) { + return null; + } +} diff --git a/core/java/android/hardware/camera2/dispatch/package.html b/core/java/android/hardware/camera2/dispatch/package.html new file mode 100644 index 0000000..783d0a1 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/package.html @@ -0,0 +1,3 @@ +<body> +{@hide} +</body> diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java new file mode 100644 index 0000000..e129783 --- /dev/null +++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java @@ -0,0 +1,574 @@ +/* + * 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.impl; + +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.dispatch.BroadcastDispatcher; +import android.hardware.camera2.dispatch.Dispatchable; +import android.hardware.camera2.dispatch.DuckTypingDispatcher; +import android.hardware.camera2.dispatch.HandlerDispatcher; +import android.hardware.camera2.dispatch.InvokeDispatcher; +import android.hardware.camera2.dispatch.NullDispatcher; +import android.hardware.camera2.utils.TaskDrainer; +import android.hardware.camera2.utils.TaskSingleDrainer; +import android.os.Handler; +import android.util.Log; +import android.view.Surface; + +import java.util.Arrays; +import java.util.List; + +import static android.hardware.camera2.impl.CameraDevice.checkHandler; +import static com.android.internal.util.Preconditions.*; + +public class CameraCaptureSessionImpl extends CameraCaptureSession { + private static final String TAG = "CameraCaptureSession"; + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + /** User-specified set of surfaces used as the configuration outputs */ + private final List<Surface> mOutputs; + /** + * User-specified state listener, used for outgoing events; calls to this object will be + * automatically {@link Handler#post(Runnable) posted} to {@code mStateHandler}. + */ + private final CameraCaptureSession.StateListener mStateListener; + /** User-specified state handler used for outgoing state listener events */ + private final Handler mStateHandler; + + /** Internal camera device; used to translate calls into existing deprecated API */ + private final android.hardware.camera2.impl.CameraDevice mDeviceImpl; + /** Internal handler; used for all incoming events to preserve total order */ + private final Handler mDeviceHandler; + + /** Drain Sequence IDs which have been queued but not yet finished with aborted/completed */ + private final TaskDrainer<Integer> mSequenceDrainer; + /** Drain state transitions from ACTIVE -> IDLE */ + private final TaskSingleDrainer mIdleDrainer; + /** Drain state transitions from BUSY -> IDLE */ + private final TaskSingleDrainer mAbortDrainer; + /** Drain the UNCONFIGURED state transition */ + private final TaskSingleDrainer mUnconfigureDrainer; + + /** This session is closed; all further calls will throw ISE */ + private boolean mClosed = false; + /** Do not unconfigure if this is set; another session will overwrite configuration */ + private boolean mSkipUnconfigure = false; + + /** Is the session in the process of aborting? Pay attention to BUSY->IDLE transitions. */ + private boolean mAborting; + + /** + * Create a new CameraCaptureSession. + * + * <p>The camera device must already be in the {@code IDLE} state when this is invoked. + * There must be no pending actions + * (e.g. no pending captures, no repeating requests, no flush).</p> + */ + CameraCaptureSessionImpl(List<Surface> outputs, + CameraCaptureSession.StateListener listener, Handler stateHandler, + android.hardware.camera2.impl.CameraDevice deviceImpl, + Handler deviceStateHandler, boolean configureSuccess) { + if (outputs == null || outputs.isEmpty()) { + throw new IllegalArgumentException("outputs must be a non-null, non-empty list"); + } else if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + // TODO: extra verification of outputs + mOutputs = outputs; + mStateHandler = checkHandler(stateHandler); + mStateListener = createUserStateListenerProxy(mStateHandler, listener); + + mDeviceHandler = checkNotNull(deviceStateHandler, "deviceStateHandler must not be null"); + mDeviceImpl = checkNotNull(deviceImpl, "deviceImpl must not be null"); + + /* + * Use the same handler as the device's StateListener for all the internal coming events + * + * This ensures total ordering between CameraDevice.StateListener and + * CameraDevice.CaptureListener events. + */ + mSequenceDrainer = new TaskDrainer<>(mDeviceHandler, new SequenceDrainListener(), + /*name*/"seq"); + mIdleDrainer = new TaskSingleDrainer(mDeviceHandler, new IdleDrainListener(), + /*name*/"idle"); + mAbortDrainer = new TaskSingleDrainer(mDeviceHandler, new AbortDrainListener(), + /*name*/"abort"); + mUnconfigureDrainer = new TaskSingleDrainer(mDeviceHandler, new UnconfigureDrainListener(), + /*name*/"unconf"); + + // CameraDevice should call configureOutputs and have it finish before constructing us + + if (configureSuccess) { + mStateListener.onConfigured(this); + } else { + mStateListener.onConfigureFailed(this); + mClosed = true; // do not fire any other callbacks, do not allow any other work + } + } + + @Override + public CameraDevice getDevice() { + return mDeviceImpl; + } + + @Override + public synchronized int capture(CaptureRequest request, CaptureListener listener, + Handler handler) throws CameraAccessException { + checkNotClosed(); + checkLegalToCapture(); + + handler = checkHandler(handler); + + if (VERBOSE) { + Log.v(TAG, "capture - request " + request + ", listener " + listener + " handler" + + "" + handler); + } + + return addPendingSequence(mDeviceImpl.capture(request, + createCaptureListenerProxy(handler, listener), mDeviceHandler)); + } + + @Override + public synchronized int captureBurst(List<CaptureRequest> requests, CaptureListener listener, + Handler handler) throws CameraAccessException { + checkNotClosed(); + checkLegalToCapture(); + + handler = checkHandler(handler); + + if (VERBOSE) { + CaptureRequest[] requestArray = requests.toArray(new CaptureRequest[0]); + Log.v(TAG, "captureBurst - requests " + Arrays.toString(requestArray) + ", listener " + + listener + " handler" + "" + handler); + } + + return addPendingSequence(mDeviceImpl.captureBurst(requests, + createCaptureListenerProxy(handler, listener), mDeviceHandler)); + } + + @Override + public synchronized int setRepeatingRequest(CaptureRequest request, CaptureListener listener, + Handler handler) throws CameraAccessException { + checkNotClosed(); + checkLegalToCapture(); + + handler = checkHandler(handler); + + return addPendingSequence(mDeviceImpl.setRepeatingRequest(request, + createCaptureListenerProxy(handler, listener), mDeviceHandler)); + } + + @Override + public synchronized int setRepeatingBurst(List<CaptureRequest> requests, + CaptureListener listener, Handler handler) throws CameraAccessException { + checkNotClosed(); + checkLegalToCapture(); + + handler = checkHandler(handler); + + if (VERBOSE) { + CaptureRequest[] requestArray = requests.toArray(new CaptureRequest[0]); + Log.v(TAG, "setRepeatingBurst - requests " + Arrays.toString(requestArray) + + ", listener " + listener + " handler" + "" + handler); + } + + return addPendingSequence(mDeviceImpl.setRepeatingBurst(requests, + createCaptureListenerProxy(handler, listener), mDeviceHandler)); + } + + @Override + public synchronized void stopRepeating() throws CameraAccessException { + checkNotClosed(); + + if (VERBOSE) { + Log.v(TAG, "stopRepeating"); + } + + mDeviceImpl.stopRepeating(); + } + + @Override + public synchronized void abortCaptures() throws CameraAccessException { + checkNotClosed(); + + if (VERBOSE) { + Log.v(TAG, "abortCaptures"); + } + + if (mAborting) { + Log.w(TAG, "abortCaptures - Session is already aborting; doing nothing"); + return; + } + + mAborting = true; + mAbortDrainer.taskStarted(); + + mDeviceImpl.flush(); + // The next BUSY -> IDLE set of transitions will mark the end of the abort. + } + + /** + * Replace this session with another session. + * + * <p>This is an optimization to avoid unconfiguring and then immediately having to + * reconfigure again.</p> + * + * <p>The semantics are identical to {@link #close}, except that unconfiguring will be skipped. + * <p> + * + * @see CameraCaptureSession#close + */ + synchronized void replaceSessionClose(CameraCaptureSession other) { + /* + * In order for creating new sessions to be fast, the new session should be created + * before the old session is closed. + * + * Otherwise the old session will always unconfigure if there is no new session to + * replace it. + * + * Unconfiguring could add hundreds of milliseconds of delay. We could race and attempt + * to skip unconfigure if a new session is created before the captures are all drained, + * but this would introduce nondeterministic behavior. + */ + + // #close was already called explicitly, keep going the slow route + if (mClosed) { + return; + } + + mSkipUnconfigure = true; + close(); + } + + @Override + public synchronized void close() { + if (mClosed) { + return; + } + + mClosed = true; + + /* + * Flush out any repeating request. Since camera is closed, no new requests + * can be queued, and eventually the entire request queue will be drained. + * + * Once this is done, wait for camera to idle, then unconfigure the camera. + * Once that's done, fire #onClosed. + */ + try { + mDeviceImpl.stopRepeating(); + } catch (CameraAccessException e) { + // OK: close does not throw checked exceptions. + Log.e(TAG, "Exception while stopping repeating: ", e); + + // TODO: call onError instead of onClosed if this happens + } + + // If no sequences are pending, fire #onClosed immediately + mSequenceDrainer.beginDrain(); + } + + /** + * Post calls into a CameraCaptureSession.StateListener to the user-specified {@code handler}. + */ + private StateListener createUserStateListenerProxy(Handler handler, StateListener listener) { + InvokeDispatcher<StateListener> userListenerSink = new InvokeDispatcher<>(listener); + HandlerDispatcher<StateListener> handlerPassthrough = + new HandlerDispatcher<>(userListenerSink, handler); + + return new ListenerProxies.SessionStateListenerProxy(handlerPassthrough); + } + + /** + * Forward callbacks from + * CameraDevice.CaptureListener to the CameraCaptureSession.CaptureListener. + * + * <p>In particular, all calls are automatically split to go both to our own + * internal listener, and to the user-specified listener (by transparently posting + * to the user-specified handler).</p> + * + * <p>When a capture sequence finishes, update the pending checked sequences set.</p> + */ + @SuppressWarnings("deprecation") + private CameraDevice.CaptureListener createCaptureListenerProxy( + Handler handler, CaptureListener listener) { + CameraDevice.CaptureListener localListener = new CameraDevice.CaptureListener() { + @Override + public void onCaptureSequenceCompleted(CameraDevice camera, + int sequenceId, long frameNumber) { + finishPendingSequence(sequenceId); + } + + @Override + public void onCaptureSequenceAborted(CameraDevice camera, + int sequenceId) { + finishPendingSequence(sequenceId); + } + }; + + /* + * Split the calls from the device listener into local listener and the following chain: + * - duck type from device listener to session listener + * - then forward the call to a handler + * - then finally invoke the destination method on the session listener object + */ + Dispatchable<CaptureListener> userListenerSink; + if (listener == null) { // OK: API allows the user to not specify a listener + userListenerSink = new NullDispatcher<>(); + } else { + userListenerSink = new InvokeDispatcher<>(listener); + } + + InvokeDispatcher<CameraDevice.CaptureListener> localSink = + new InvokeDispatcher<>(localListener); + HandlerDispatcher<CaptureListener> handlerPassthrough = + new HandlerDispatcher<>(userListenerSink, handler); + DuckTypingDispatcher<CameraDevice.CaptureListener, CaptureListener> duckToSessionCaptureListener + = new DuckTypingDispatcher<>(handlerPassthrough, CaptureListener.class); + + BroadcastDispatcher<CameraDevice.CaptureListener> broadcaster = + new BroadcastDispatcher<CameraDevice.CaptureListener>( + duckToSessionCaptureListener, + localSink); + + return new ListenerProxies.DeviceCaptureListenerProxy(broadcaster); + } + + /** + * + * Create an internal state listener, to be invoked on the mDeviceHandler + * + * <p>It has a few behaviors: + * <ul> + * <li>Convert device state changes into session state changes. + * <li>Keep track of async tasks that the session began (idle, abort). + * </ul> + * </p> + * */ + CameraDevice.StateListener getDeviceStateListener() { + final CameraCaptureSession session = this; + + return new CameraDevice.StateListener() { + private boolean mBusy = false; + private boolean mActive = false; + + @Override + public void onOpened(CameraDevice camera) { + throw new AssertionError("Camera must already be open before creating a session"); + } + + @Override + public void onDisconnected(CameraDevice camera) { + close(); + } + + @Override + public void onError(CameraDevice camera, int error) { + // TODO: Handle errors somehow. + Log.wtf(TAG, "Got device error " + error); + } + + @Override + public void onActive(CameraDevice camera) { + mIdleDrainer.taskStarted(); + mActive = true; + + mStateListener.onActive(session); + } + + @Override + public void onIdle(CameraDevice camera) { + boolean isAborting; + synchronized (session) { + isAborting = mAborting; + } + + /* + * Check which states we transitioned through: + * + * (ACTIVE -> IDLE) + * (BUSY -> IDLE) + * + * Note that this is also legal: + * (ACTIVE -> BUSY -> IDLE) + * + * and mark those tasks as finished + */ + if (mBusy && isAborting) { + mAbortDrainer.taskFinished(); + + synchronized (session) { + mAborting = false; + } + } + + if (mActive) { + mIdleDrainer.taskFinished(); + } + + mBusy = false; + mActive = false; + + mStateListener.onReady(session); + } + + @Override + public void onBusy(CameraDevice camera) { + mBusy = true; + + // TODO: Queue captures during abort instead of failing them + // since the app won't be able to distinguish the two actives + Log.w(TAG, "Device is now busy; do not submit new captures (TODO: allow this)"); + mStateListener.onActive(session); + } + + @Override + public void onUnconfigured(CameraDevice camera) { + mUnconfigureDrainer.taskFinished(); + } + }; + + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + private void checkLegalToCapture() { + if (mAborting) { + throw new IllegalStateException( + "Session is aborting captures; new captures are not permitted"); + } + } + + private void checkNotClosed() { + if (mClosed) { + throw new IllegalStateException( + "Session has been closed; further changes are illegal."); + } + } + + /** + * Notify the session that a pending capture sequence has just been queued. + * + * <p>During a shutdown/close, the session waits until all pending sessions are finished + * before taking any further steps to shut down itself.</p> + * + * @see #finishPendingSequence + */ + private int addPendingSequence(int sequenceId) { + mSequenceDrainer.taskStarted(sequenceId); + return sequenceId; + } + + /** + * Notify the session that a pending capture sequence is now finished. + * + * <p>During a shutdown/close, once all pending sequences finish, it is safe to + * close the camera further by unconfiguring and then firing {@code onClosed}.</p> + */ + private void finishPendingSequence(int sequenceId) { + mSequenceDrainer.taskFinished(sequenceId); + } + + private class SequenceDrainListener implements TaskDrainer.DrainListener { + @Override + public void onDrained() { + /* + * No repeating request is set; and the capture queue has fully drained. + * + * If no captures were queued to begin with, and an abort was queued, + * it's still possible to get another BUSY before the last IDLE. + * + * If the camera is already "IDLE" and no aborts are pending, + * then the drain immediately finishes. + */ + mAbortDrainer.beginDrain(); + } + } + + private class AbortDrainListener implements TaskDrainer.DrainListener { + @Override + public void onDrained() { + synchronized (CameraCaptureSessionImpl.this) { + /* + * Any queued aborts have now completed. + * + * It's now safe to wait to receive the final "IDLE" event, as the camera device + * will no longer again transition to "ACTIVE" by itself. + * + * If the camera is already "IDLE", then the drain immediately finishes. + */ + mIdleDrainer.beginDrain(); + } + } + } + + private class IdleDrainListener implements TaskDrainer.DrainListener { + @Override + public void onDrained() { + synchronized (CameraCaptureSessionImpl.this) { + /* + * The device is now IDLE, and has settled. It will not transition to + * ACTIVE or BUSY again by itself. + * + * It's now safe to unconfigure the outputs and after it's done invoke #onClosed. + * + * This operation is idempotent; a session will not be closed twice. + */ + + // Fast path: A new capture session has replaced this one; don't unconfigure. + if (mSkipUnconfigure) { + mStateListener.onClosed(CameraCaptureSessionImpl.this); + return; + } + + // Slow path: #close was called explicitly on this session; unconfigure first + + try { + mUnconfigureDrainer.taskStarted(); + mDeviceImpl.configureOutputs(null); // begin transition to unconfigured state + } catch (CameraAccessException e) { + // OK: do not throw checked exceptions. + Log.e(TAG, "Exception while configuring outputs: ", e); + + // TODO: call onError instead of onClosed if this happens + } + + mUnconfigureDrainer.beginDrain(); + } + } + } + + private class UnconfigureDrainListener implements TaskDrainer.DrainListener { + @Override + public void onDrained() { + synchronized (CameraCaptureSessionImpl.this) { + // The device has finished unconfiguring. It's now fully closed. + mStateListener.onClosed(CameraCaptureSessionImpl.this); + } + } + } +} diff --git a/core/java/android/hardware/camera2/impl/CameraDevice.java b/core/java/android/hardware/camera2/impl/CameraDevice.java index 9a4c531..e9d4b0f 100644 --- a/core/java/android/hardware/camera2/impl/CameraDevice.java +++ b/core/java/android/hardware/camera2/impl/CameraDevice.java @@ -62,6 +62,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final CameraDeviceCallbacks mCallbacks = new CameraDeviceCallbacks(); private final StateListener mDeviceListener; + private volatile StateListener mSessionStateListener; private final Handler mDeviceHandler; private boolean mIdle = true; @@ -90,6 +91,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { */ private final FrameNumberTracker mFrameNumberTracker = new FrameNumberTracker(); + private CameraCaptureSessionImpl mCurrentSession; + // Runnables for all state transitions, except error, which needs the // error code argument @@ -98,6 +101,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onOpened(CameraDevice.this); + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onOpened(CameraDevice.this); + } } } }; @@ -107,6 +114,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onUnconfigured(CameraDevice.this); + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onUnconfigured(CameraDevice.this); + } } } }; @@ -116,6 +127,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onActive(CameraDevice.this); + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onActive(CameraDevice.this); + } } } }; @@ -125,6 +140,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onBusy(CameraDevice.this); + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onBusy(CameraDevice.this); + } } } }; @@ -133,6 +152,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public void run() { mDeviceListener.onClosed(CameraDevice.this); + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onClosed(CameraDevice.this); + } } }; @@ -141,6 +164,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onIdle(CameraDevice.this); + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onIdle(CameraDevice.this); + } } } }; @@ -150,6 +177,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onDisconnected(CameraDevice.this); + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onDisconnected(CameraDevice.this); + } } } }; @@ -170,7 +201,6 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { tag = tag.substring(0, MAX_TAG_LEN); } TAG = tag; - DEBUG = Log.isLoggable(TAG, Log.DEBUG); } @@ -263,7 +293,43 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void createCaptureSession(List<Surface> outputs, CameraCaptureSession.StateListener listener, Handler handler) throws CameraAccessException { - // TODO + synchronized (mLock) { + if (DEBUG) { + Log.d(TAG, "createCaptureSession"); + } + + checkIfCameraClosed(); + + // TODO: we must be in UNCONFIGURED mode to begin with, or using another session + + // TODO: dont block for this + boolean configureSuccess = true; + CameraAccessException pendingException = null; + try { + configureOutputs(outputs); // and then block until IDLE + } catch (CameraAccessException e) { + configureSuccess = false; + pendingException = e; + } + + // Fire onConfigured if configureOutputs succeeded, fire onConfigureFailed otherwise. + CameraCaptureSessionImpl newSession = + new CameraCaptureSessionImpl(outputs, listener, handler, this, mDeviceHandler, + configureSuccess); + + if (mCurrentSession != null) { + mCurrentSession.replaceSessionClose(newSession); + } + + // TODO: wait until current session closes, then create the new session + mCurrentSession = newSession; + + if (pendingException != null) { + throw pendingException; + } + + mSessionStateListener = mCurrentSession.getDeviceStateListener(); + } } @Override @@ -275,7 +341,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { CameraMetadataNative templatedRequest = new CameraMetadataNative(); try { - mRemoteDevice.createDefaultRequest(templateType, /* out */templatedRequest); + mRemoteDevice.createDefaultRequest(templateType, /*out*/templatedRequest); } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { @@ -304,10 +370,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public int captureBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException { - // TODO: remove this. Throw IAE if the request is null or empty. Need to update API doc. - if (requests.isEmpty()) { - Log.w(TAG, "Capture burst request list is empty, do nothing!"); - return -1; + if (requests == null || requests.isEmpty()) { + throw new IllegalArgumentException("At least one request must be given"); } return submitCaptureRequest(requests, listener, handler, /*streaming*/false); } @@ -454,10 +518,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException { - // TODO: remove this. Throw IAE if the request is null or empty. Need to update API doc. - if (requests.isEmpty()) { - Log.w(TAG, "Set Repeating burst request list is empty, do nothing!"); - return -1; + if (requests == null || requests.isEmpty()) { + throw new IllegalArgumentException("At least one request must be given"); } return submitCaptureRequest(requests, listener, handler, /*streaming*/true); } @@ -951,10 +1013,14 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } /** - * Default handler management. If handler is null, get the current thread's - * Looper to create a Handler with. If no looper exists, throw exception. + * Default handler management. + * + * <p> + * If handler is null, get the current thread's + * Looper to create a Handler with. If no looper exists, throw {@code IllegalArgumentException}. + * </p> */ - private Handler checkHandler(Handler handler) { + static Handler checkHandler(Handler handler) { if (handler == null) { Looper looper = Looper.myLooper(); if (looper == null) { diff --git a/core/java/android/hardware/camera2/impl/ListenerProxies.java b/core/java/android/hardware/camera2/impl/ListenerProxies.java new file mode 100644 index 0000000..04c43e3 --- /dev/null +++ b/core/java/android/hardware/camera2/impl/ListenerProxies.java @@ -0,0 +1,168 @@ +package android.hardware.camera2.impl; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.dispatch.Dispatchable; +import android.hardware.camera2.dispatch.MethodNameInvoker; + +import static com.android.internal.util.Preconditions.*; + +/** + * Proxy out invocations to the camera2 API listeners into a {@link Dispatchable}. + * + * <p>Since abstract classes do not support Java's dynamic {@code Proxy}, we have to + * to use our own proxy mechanism.</p> + */ +public class ListenerProxies { + + // TODO: replace with codegen + + public static class DeviceStateListenerProxy extends CameraDevice.StateListener { + private final MethodNameInvoker<CameraDevice.StateListener> mProxy; + + public DeviceStateListenerProxy( + Dispatchable<CameraDevice.StateListener> dispatchTarget) { + dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null"); + mProxy = new MethodNameInvoker<>(dispatchTarget, CameraDevice.StateListener.class); + } + + @Override + public void onOpened(CameraDevice camera) { + mProxy.invoke("onOpened", camera); + } + + @Override + public void onDisconnected(CameraDevice camera) { + mProxy.invoke("onDisconnected", camera); + } + + @Override + public void onError(CameraDevice camera, int error) { + mProxy.invoke("onError", camera, error); + } + + @Override + public void onUnconfigured(CameraDevice camera) { + mProxy.invoke("onUnconfigured", camera); + } + + @Override + public void onActive(CameraDevice camera) { + mProxy.invoke("onActive", camera); + } + + @Override + public void onBusy(CameraDevice camera) { + mProxy.invoke("onBusy", camera); + } + + @Override + public void onClosed(CameraDevice camera) { + mProxy.invoke("onClosed", camera); + } + + @Override + public void onIdle(CameraDevice camera) { + mProxy.invoke("onIdle", camera); + } + } + + @SuppressWarnings("deprecation") + public static class DeviceCaptureListenerProxy extends CameraDevice.CaptureListener { + private final MethodNameInvoker<CameraDevice.CaptureListener> mProxy; + + public DeviceCaptureListenerProxy( + Dispatchable<CameraDevice.CaptureListener> dispatchTarget) { + dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null"); + mProxy = new MethodNameInvoker<>(dispatchTarget, CameraDevice.CaptureListener.class); + } + + @Override + public void onCaptureStarted(CameraDevice camera, + CaptureRequest request, long timestamp) { + mProxy.invoke("onCaptureStarted", camera, request, timestamp); + } + + @Override + public void onCapturePartial(CameraDevice camera, + CaptureRequest request, CaptureResult result) { + mProxy.invoke("onCapturePartial", camera, request, result); + } + + @Override + public void onCaptureProgressed(CameraDevice camera, + CaptureRequest request, CaptureResult partialResult) { + mProxy.invoke("onCaptureProgressed", camera, request, partialResult); + } + + @Override + public void onCaptureCompleted(CameraDevice camera, + CaptureRequest request, TotalCaptureResult result) { + mProxy.invoke("onCaptureCompleted", camera, request, result); + } + + @Override + public void onCaptureFailed(CameraDevice camera, + CaptureRequest request, CaptureFailure failure) { + mProxy.invoke("onCaptureFailed", camera, request, failure); + } + + @Override + public void onCaptureSequenceCompleted(CameraDevice camera, + int sequenceId, long frameNumber) { + mProxy.invoke("onCaptureSequenceCompleted", camera, sequenceId, frameNumber); + } + + @Override + public void onCaptureSequenceAborted(CameraDevice camera, + int sequenceId) { + mProxy.invoke("onCaptureSequenceAborted", camera, sequenceId); + } + } + + public static class SessionStateListenerProxy + extends CameraCaptureSession.StateListener { + private final MethodNameInvoker<CameraCaptureSession.StateListener> mProxy; + + public SessionStateListenerProxy( + Dispatchable<CameraCaptureSession.StateListener> dispatchTarget) { + dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null"); + mProxy = new MethodNameInvoker<>(dispatchTarget, + CameraCaptureSession.StateListener.class); + } + + @Override + public void onConfigured(CameraCaptureSession session) { + mProxy.invoke("onConfigured", session); + } + + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + mProxy.invoke("onConfigureFailed", session); + } + + @Override + public void onReady(CameraCaptureSession session) { + mProxy.invoke("onReady", session); + } + + @Override + public void onActive(CameraCaptureSession session) { + mProxy.invoke("onActive", session); + } + + @Override + public void onClosed(CameraCaptureSession session) { + mProxy.invoke("onClosed", session); + } + } + + private ListenerProxies() { + throw new AssertionError(); + } +} diff --git a/core/java/android/hardware/camera2/utils/TaskDrainer.java b/core/java/android/hardware/camera2/utils/TaskDrainer.java new file mode 100644 index 0000000..3cba9a1 --- /dev/null +++ b/core/java/android/hardware/camera2/utils/TaskDrainer.java @@ -0,0 +1,201 @@ +/* + * 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.utils; + +import android.os.Handler; +import android.util.Log; + +import java.util.HashSet; +import java.util.Set; + +import static com.android.internal.util.Preconditions.*; + +/** + * Keep track of multiple concurrent tasks starting and finishing by their key; + * allow draining existing tasks and figuring out when all tasks have finished + * (and new ones won't begin). + * + * <p>The initial state is to allow all tasks to be started and finished. A task may only be started + * once, after which it must be finished before starting again. Likewise, finishing a task + * that hasn't been started is also not allowed.</p> + * + * <p>When draining begins, no more new tasks can be started. This guarantees that at some + * point when all the tasks are finished there will be no more collective new tasks, + * at which point the {@link DrainListener#onDrained} callback will be invoked.</p> + * + * + * @param <T> + * a type for the key that will represent tracked tasks; + * must implement {@code Object#equals} + */ +public class TaskDrainer<T> { + /** + * Fired asynchronously after draining has begun with {@link TaskDrainer#beginDrain} + * <em>and</em> all tasks that were started have finished. + */ + public interface DrainListener { + /** All tasks have fully finished draining; there will be no more pending tasks. */ + public void onDrained(); + } + + private static final String TAG = "TaskDrainer"; + private static final boolean VERBOSE = false; + + private final Handler mHandler; + private final DrainListener mListener; + private final String mName; + + /** Set of tasks which have been started but not yet finished with #taskFinished */ + private final Set<T> mTaskSet = new HashSet<T>(); + private final Object mLock = new Object(); + + private boolean mDraining = false; + private boolean mDrainFinished = false; + + /** + * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener + * via the {@code handler}. + * + * @param handler a non-{@code null} handler to use to post runnables to + * @param listener a non-{@code null} listener where {@code onDrained} will be called + */ + public TaskDrainer(Handler handler, DrainListener listener) { + mHandler = checkNotNull(handler, "handler must not be null"); + mListener = checkNotNull(listener, "listener must not be null"); + mName = null; + } + + /** + * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener + * via the {@code handler}. + * + * @param handler a non-{@code null} handler to use to post runnables to + * @param listener a non-{@code null} listener where {@code onDrained} will be called + * @param name an optional name used for debug logging + */ + public TaskDrainer(Handler handler, DrainListener listener, String name) { + // XX: Probably don't need a handler at all here + mHandler = checkNotNull(handler, "handler must not be null"); + mListener = checkNotNull(listener, "listener must not be null"); + mName = name; + } + + /** + * Mark an asynchronous task as having started. + * + * <p>A task cannot be started more than once without first having finished. Once + * draining begins with {@link #beginDrain}, no new tasks can be started.</p> + * + * @param task a key to identify a task + * + * @see #taskFinished + * @see #beginDrain + * + * @throws IllegalStateException + * If attempting to start a task which is already started (and not finished), + * or if attempting to start a task after draining has begun. + */ + public void taskStarted(T task) { + synchronized (mLock) { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "taskStarted " + task); + } + + if (mDraining) { + throw new IllegalStateException("Can't start more tasks after draining has begun"); + } + + if (!mTaskSet.add(task)) { + throw new IllegalStateException("Task " + task + " was already started"); + } + } + } + + + /** + * Mark an asynchronous task as having finished. + * + * <p>A task cannot be finished if it hasn't started. Once finished, a task + * cannot be finished again (unless it's started again).</p> + * + * @param task a key to identify a task + * + * @see #taskStarted + * @see #beginDrain + * + * @throws IllegalStateException + * If attempting to start a task which is already finished (and not re-started), + */ + public void taskFinished(T task) { + synchronized (mLock) { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "taskFinished " + task); + } + + if (!mTaskSet.remove(task)) { + throw new IllegalStateException("Task " + task + " was already finished"); + } + + // If this is the last finished task and draining has already begun, fire #onDrained + checkIfDrainFinished(); + } + } + + /** + * Do not allow any more tasks to be started; once all existing started tasks are finished, + * fire the {@link DrainListener#onDrained} callback asynchronously. + * + * <p>This operation is idempotent; calling it more than once has no effect.</p> + */ + public void beginDrain() { + synchronized (mLock) { + if (!mDraining) { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "beginDrain started"); + } + + mDraining = true; + + // If all tasks that had started had already finished by now, fire #onDrained + checkIfDrainFinished(); + } else { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "beginDrain ignored"); + } + } + } + } + + private void checkIfDrainFinished() { + if (mTaskSet.isEmpty() && mDraining && !mDrainFinished) { + mDrainFinished = true; + postDrained(); + } + } + + private void postDrained() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "onDrained"); + } + + mListener.onDrained(); + } + }); + } +} diff --git a/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java b/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java new file mode 100644 index 0000000..f6272c9 --- /dev/null +++ b/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java @@ -0,0 +1,104 @@ +/* + * 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.utils; + +import android.hardware.camera2.utils.TaskDrainer.DrainListener; +import android.os.Handler; + +/** + * Keep track of a single concurrent task starting and finishing; + * allow draining the existing task and figuring out when the task has finished + * (and won't restart). + * + * <p>The initial state is to allow all tasks to be started and finished. A task may only be started + * once, after which it must be finished before starting again. Likewise, finishing a task + * that hasn't been started is also not allowed.</p> + * + * <p>When draining begins, the task cannot be started again. This guarantees that at some + * point the task will be finished forever, at which point the {@link DrainListener#onDrained} + * callback will be invoked.</p> + */ +public class TaskSingleDrainer { + + private final TaskDrainer<Object> mTaskDrainer; + private final Object mSingleTask = new Object(); + + /** + * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener + * via the {@code handler}. + * + * @param handler a non-{@code null} handler to use to post runnables to + * @param listener a non-{@code null} listener where {@code onDrained} will be called + */ + public TaskSingleDrainer(Handler handler, DrainListener listener) { + mTaskDrainer = new TaskDrainer<>(handler, listener); + } + + /** + * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener + * via the {@code handler}. + * + * @param handler a non-{@code null} handler to use to post runnables to + * @param listener a non-{@code null} listener where {@code onDrained} will be called + * @param name an optional name used for debug logging + */ + public TaskSingleDrainer(Handler handler, DrainListener listener, String name) { + mTaskDrainer = new TaskDrainer<>(handler, listener, name); + } + + /** + * Mark this asynchronous task as having started. + * + * <p>The task cannot be started more than once without first having finished. Once + * draining begins with {@link #beginDrain}, no new tasks can be started.</p> + * + * @see #taskFinished + * @see #beginDrain + * + * @throws IllegalStateException + * If attempting to start a task which is already started (and not finished), + * or if attempting to start a task after draining has begun. + */ + public void taskStarted() { + mTaskDrainer.taskStarted(mSingleTask); + } + + /** + * Do not allow any more task re-starts; once the existing task is finished, + * fire the {@link DrainListener#onDrained} callback asynchronously. + * + * <p>This operation is idempotent; calling it more than once has no effect.</p> + */ + public void beginDrain() { + mTaskDrainer.beginDrain(); + } + + /** + * Mark this asynchronous task as having finished. + * + * <p>The task cannot be finished if it hasn't started. Once finished, a task + * cannot be finished again (unless it's started again).</p> + * + * @see #taskStarted + * @see #beginDrain + * + * @throws IllegalStateException + * If attempting to start a task which is already finished (and not re-started), + */ + public void taskFinished() { + mTaskDrainer.taskFinished(mSingleTask); + } +} diff --git a/core/java/android/hardware/camera2/utils/UncheckedThrow.java b/core/java/android/hardware/camera2/utils/UncheckedThrow.java index 8224fed..ffcb78b 100644 --- a/core/java/android/hardware/camera2/utils/UncheckedThrow.java +++ b/core/java/android/hardware/camera2/utils/UncheckedThrow.java @@ -33,8 +33,20 @@ public class UncheckedThrow { UncheckedThrow.<RuntimeException>throwAnyImpl(e); } + /** + * Throw any kind of throwable without needing it to be checked + * @param e any instance of a Throwable + */ + public static void throwAnyException(Throwable e) { + /** + * Abuse type erasure by making the compiler think we are throwing RuntimeException, + * which is unchecked, but then inserting any exception in there. + */ + UncheckedThrow.<RuntimeException>throwAnyImpl(e); + } + @SuppressWarnings("unchecked") - private static<T extends Exception> void throwAnyImpl(Exception e) throws T { + private static<T extends Throwable> void throwAnyImpl(Throwable e) throws T { throw (T) e; } } |
