OSDN Git Service

Camera2: Fire all callbacks
authorEino-Ville Talvala <etalvala@google.com>
Thu, 3 Oct 2013 18:15:21 +0000 (11:15 -0700)
committerEino-Ville Talvala <etalvala@google.com>
Mon, 7 Oct 2013 19:40:49 +0000 (12:40 -0700)
 - Allow configureOutputs to be called when device is
   actively working, and document what happens then.
 - At the managed level, trigger proper sequence of callbacks for
   device state changes, for all the callbacks that are not yet
   managed by the camera service.
 - Restructure CameraManager.openDevice to have the device itself fire the
   initial callbacks.
 - Make CameraDevice.configureOutputs(null) work.
 - Make CameraDeviec.configureOutputs(identical surfaces) work.
 - Ensure proper checking for camera closed state.

Bug: 10360518
Change-Id: I9db348ee9c5ce4d3fe02fd34e779acc85cba68dc

core/java/android/hardware/camera2/CameraDevice.java
core/java/android/hardware/camera2/CameraManager.java
core/java/android/hardware/camera2/impl/CameraDevice.java

index a9a72b0..7095e4d 100644 (file)
@@ -197,26 +197,33 @@ public interface CameraDevice extends AutoCloseable {
      * if the format is user-visible, it must be one of android.scaler.availableFormats;
      * and the size must be one of android.scaler.available[Processed|Jpeg]Sizes).</p>
      *
-     * <p>To change the output, the camera device must be idle. The device is considered
-     * to be idle once all in-flight and pending capture requests have been processed,
-     * and all output image buffers from the captures have been sent to their destination
-     * Surfaces.</p>
-     *
-     * <p>To reach an idle state without cancelling any submitted captures, first
-     * stop any repeating request/burst with {@link #stopRepeating}, and then
-     * wait for the {@link StateListener#onIdle} callback to be
-     * called. To idle as fast as possible, use {@link #flush} and wait for the
-     * idle callback.</p>
+     * <p>When this method is called with valid Surfaces, the device will transition to the {@link
+     * StateListener#onBusy busy state}. Once configuration is complete, the device will transition
+     * into the {@link StateListener#onIdle idle state}. Capture requests using the newly-configured
+     * Surfaces may then be submitted with {@link #capture}, {@link #captureBurst}, {@link
+     * #setRepeatingRequest}, or {@link #setRepeatingBurst}.</p>
+     *
+     * <p>If this method is called while the camera device is still actively processing previously
+     * submitted captures, then the following sequence of events occurs: The device transitions to
+     * the busy state and calls the {@link StateListener#onBusy} callback. Second, if a repeating
+     * request is set it is cleared.  Third, the device finishes up all in-flight and pending
+     * requests. Finally, once the device is idle, it then reconfigures its outputs, and calls the
+     * {@link StateListener#onIdle} method once it is again ready to accept capture
+     * requests. Therefore, no submitted work is discarded. To idle as fast as possible, use {@link
+     * #flush} and wait for the idle callback before calling configureOutputs. This will discard
+     * work, but reaches the new configuration sooner.</p>
      *
      * <p>Using larger resolution outputs, or more outputs, can result in slower
      * output rate from the device.</p>
      *
-     * <p>Configuring the outputs with an empty or null list will transition
-     * the camera into an {@link StateListener#onUnconfigured unconfigured state}.
-     * </p>
+     * <p>Configuring the outputs with an empty or null list will transition the camera into an
+     * {@link StateListener#onUnconfigured unconfigured state} instead of the {@link
+     * StateListener#onIdle idle state}.  </p>
      *
      * <p>Calling configureOutputs with the same arguments as the last call to
-     * configureOutputs has no effect.</p>
+     * configureOutputs has no effect, and the {@link StateListener#onBusy busy}
+     * and {@link StateListener#onIdle idle} state transitions will happen
+     * immediately.</p>
      *
      * @param outputs The new set of Surfaces that should be made available as
      * targets for captured image data.
@@ -228,7 +235,10 @@ public interface CameraDevice extends AutoCloseable {
      * @throws IllegalStateException if the camera device is not idle, or
      *                               if the camera device has been closed
      *
+     * @see StateListener#onBusy
      * @see StateListener#onIdle
+     * @see StateListener#onActive
+     * @see StateListener#onUnconfigured
      * @see #stopRepeating
      * @see #flush
      */
@@ -516,31 +526,6 @@ public interface CameraDevice extends AutoCloseable {
     public void waitUntilIdle() throws CameraAccessException;
 
     /**
-     * Set the listener object to call when an asynchronous device event occurs,
-     * such as errors or idle notifications.
-     *
-     * <p>The events reported here are device-wide; notifications about
-     * individual capture requests or capture results are reported through
-     * {@link CaptureListener}.</p>
-     *
-     * <p>If the camera device is idle when the listener is set, then the
-     * {@link StateListener#onIdle} method will be immediately called,
-     * even if the device has never been active before.
-     * </p>
-     *
-     * @param listener the CameraDeviceListener to send device-level event
-     * notifications to. Setting this to null will stop notifications.
-     * @param handler the handler on which the listener should be invoked, or
-     * {@code null} to use the current thread's {@link android.os.Looper looper}.
-     *
-     * @throws IllegalArgumentException if handler is null, the listener is
-     * not null, and the calling thread has no looper
-     *
-     * @hide
-     */
-    public void setDeviceListener(StateListener listener, Handler handler);
-
-    /**
      * Flush all captures currently pending and in-progress as fast as
      * possible.
      *
@@ -577,13 +562,24 @@ public interface CameraDevice extends AutoCloseable {
     public void flush() throws CameraAccessException;
 
     /**
-     * Close the connection to this camera device. After this call, all calls to
+     * Close the connection to this camera device.
+     *
+     * <p>After this call, all calls to
      * the camera device interface will throw a {@link IllegalStateException},
-     * except for calls to close().
+     * except for calls to close(). Once the device has fully shut down, the
+     * {@link StateListener#onClosed} callback will be called, and the camera is
+     * free to be re-opened.</p>
+     *
+     * <p>After this call, besides the final {@link StateListener#onClosed} call, no calls to the
+     * device's {@link StateListener} will occur, and any remaining submitted capture requests will
+     * not fire their {@link CaptureListener} callbacks.</p>
+     *
+     * <p>To shut down as fast as possible, call the {@link #flush} method and then {@link #close}
+     * once the flush completes. This will discard some capture requests, but results in faster
+     * shutdown.</p>
      */
     @Override
     public void close();
-    // TODO: We should decide on the behavior of in-flight requests should be on close.
 
     /**
      * <p>A listener for tracking the progress of a {@link CaptureRequest}
@@ -713,6 +709,9 @@ public interface CameraDevice extends AutoCloseable {
      * A listener for notifications about the state of a camera
      * device.
      *
+     * <p>A listener must be provided to the {@link CameraManager#openCamera}
+     * method to open a camera device.</p>
+     *
      * <p>These events include notifications about the device becoming idle (
      * allowing for {@link #configureOutputs} to be called), about device
      * disconnection, and about unexpected device errors.</p>
@@ -722,7 +721,7 @@ public interface CameraDevice extends AutoCloseable {
      * the {@link #capture}, {@link #captureBurst}, {@link
      * #setRepeatingRequest}, or {@link #setRepeatingBurst} methods.
      *
-     * @see #setDeviceListener
+     * @see CameraManager#openCamera
      */
     public static abstract class StateListener {
        /**
index f5ee367..65b6c7a 100644 (file)
@@ -197,6 +197,8 @@ public final class CameraManager {
      * {@link #openCamera}.
      *
      * @param cameraId The unique identifier of the camera device to open
+     * @param listener The listener for the camera. Must not be null.
+     * @param handler  The handler to call the listener on. Must not be null.
      *
      * @throws CameraAccessException if the camera is disabled by device policy,
      * or too many camera devices are already open, or the cameraId does not match
@@ -204,11 +206,14 @@ public final class CameraManager {
      *
      * @throws SecurityException if the application does not have permission to
      * access the camera
+     * @throws IllegalArgumentException if listener or handler is null.
      *
      * @see #getCameraIdList
      * @see android.app.admin.DevicePolicyManager#setCameraDisabled
      */
-    private CameraDevice openCamera(String cameraId) throws CameraAccessException {
+    private void openCameraDeviceUserAsync(String cameraId,
+            CameraDevice.StateListener listener, Handler handler)
+            throws CameraAccessException {
         try {
 
             synchronized (mLock) {
@@ -216,7 +221,10 @@ public final class CameraManager {
                 ICameraDeviceUser cameraUser;
 
                 android.hardware.camera2.impl.CameraDevice device =
-                        new android.hardware.camera2.impl.CameraDevice(cameraId);
+                        new android.hardware.camera2.impl.CameraDevice(
+                                cameraId,
+                                listener,
+                                handler);
 
                 BinderHolder holder = new BinderHolder();
                 mCameraService.connectDevice(device.getCallbacks(),
@@ -225,10 +233,9 @@ public final class CameraManager {
                 cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder());
 
                 // TODO: factor out listener to be non-nested, then move setter to constructor
+                // For now, calling setRemoteDevice will fire initial
+                // onOpened/onUnconfigured callbacks.
                 device.setRemoteDevice(cameraUser);
-
-                return device;
-
             }
 
         } catch (NumberFormatException e) {
@@ -238,7 +245,6 @@ public final class CameraManager {
             throw e.asChecked();
         } catch (RemoteException e) {
             // impossible
-            return null;
         }
     }
 
@@ -303,16 +309,7 @@ public final class CameraManager {
             }
         }
 
-        final CameraDevice camera = openCamera(cameraId);
-        camera.setDeviceListener(listener, handler);
-
-        // TODO: make truly async in the camera service
-        handler.post(new Runnable() {
-            @Override
-            public void run() {
-                listener.onOpened(camera);
-            }
-        });
+        openCameraDeviceUserAsync(cameraId, listener, handler);
     }
 
     /**
index 463063c..c5d0999 100644 (file)
@@ -55,8 +55,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
     private final Object mLock = new Object();
     private final CameraDeviceCallbacks mCallbacks = new CameraDeviceCallbacks();
 
-    private StateListener mDeviceListener;
-    private Handler mDeviceHandler;
+    private final StateListener mDeviceListener;
+    private final Handler mDeviceHandler;
+
+    private boolean mIdle = true;
 
     private final SparseArray<CaptureListenerHolder> mCaptureListenerMap =
             new SparseArray<CaptureListenerHolder>();
@@ -67,8 +69,72 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
 
     private final String mCameraId;
 
-    public CameraDevice(String cameraId) {
+    // Runnables for all state transitions, except error, which needs the
+    // error code argument
+
+    private final Runnable mCallOnOpened = new Runnable() {
+        public void run() {
+            if (!CameraDevice.this.isClosed()) {
+                mDeviceListener.onOpened(CameraDevice.this);
+            }
+        }
+    };
+
+    private final Runnable mCallOnUnconfigured = new Runnable() {
+        public void run() {
+            if (!CameraDevice.this.isClosed()) {
+                mDeviceListener.onUnconfigured(CameraDevice.this);
+            }
+        }
+    };
+
+    private final Runnable mCallOnActive = new Runnable() {
+        public void run() {
+            if (!CameraDevice.this.isClosed()) {
+                mDeviceListener.onActive(CameraDevice.this);
+            }
+        }
+    };
+
+    private final Runnable mCallOnBusy = new Runnable() {
+        public void run() {
+            if (!CameraDevice.this.isClosed()) {
+                mDeviceListener.onBusy(CameraDevice.this);
+            }
+        }
+    };
+
+    private final Runnable mCallOnClosed = new Runnable() {
+        public void run() {
+            if (!CameraDevice.this.isClosed()) {
+                mDeviceListener.onClosed(CameraDevice.this);
+            }
+        }
+    };
+
+    private final Runnable mCallOnIdle = new Runnable() {
+        public void run() {
+            if (!CameraDevice.this.isClosed()) {
+                mDeviceListener.onIdle(CameraDevice.this);
+            }
+        }
+    };
+
+    private final Runnable mCallOnDisconnected = new Runnable() {
+        public void run() {
+            if (!CameraDevice.this.isClosed()) {
+                mDeviceListener.onDisconnected(CameraDevice.this);
+            }
+        }
+    };
+
+    public CameraDevice(String cameraId, StateListener listener, Handler handler) {
+        if (cameraId == null || listener == null || handler == null) {
+            throw new IllegalArgumentException("Null argument given");
+        }
         mCameraId = cameraId;
+        mDeviceListener = listener;
+        mDeviceHandler = handler;
         TAG = String.format("CameraDevice-%s-JV", mCameraId);
         DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     }
@@ -79,7 +145,12 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
 
     public void setRemoteDevice(ICameraDeviceUser remoteDevice) {
         // TODO: Move from decorator to direct binder-mediated exceptions
-        mRemoteDevice = CameraBinderDecorator.newInstance(remoteDevice);
+        synchronized(mLock) {
+            mRemoteDevice = CameraBinderDecorator.newInstance(remoteDevice);
+
+            mDeviceHandler.post(mCallOnOpened);
+            mDeviceHandler.post(mCallOnUnconfigured);
+        }
     }
 
     @Override
@@ -89,7 +160,13 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
 
     @Override
     public void configureOutputs(List<Surface> outputs) throws CameraAccessException {
+        // Treat a null input the same an empty list
+        if (outputs == null) {
+            outputs = new ArrayList<Surface>();
+        }
         synchronized (mLock) {
+            checkIfCameraClosed();
+
             HashSet<Surface> addSet = new HashSet<Surface>(outputs);    // Streams to create
             List<Integer> deleteList = new ArrayList<Integer>();        // Streams to delete
 
@@ -105,9 +182,13 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
                 }
             }
 
+            mDeviceHandler.post(mCallOnBusy);
+            stopRepeating();
+
             try {
-                // TODO: mRemoteDevice.beginConfigure
+                mRemoteDevice.waitUntilIdle();
 
+                // TODO: mRemoteDevice.beginConfigure
                 // Delete all streams first (to free up HW resources)
                 for (Integer streamId : deleteList) {
                     mRemoteDevice.deleteStream(streamId);
@@ -126,7 +207,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
             } catch (CameraRuntimeException e) {
                 if (e.getReason() == CAMERA_IN_USE) {
                     throw new IllegalStateException("The camera is currently busy." +
-                            " You must call waitUntilIdle before trying to reconfigure.");
+                            " You must wait until the previous operation completes.");
                 }
 
                 throw e.asChecked();
@@ -134,6 +215,12 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
                 // impossible
                 return;
             }
+
+            if (outputs.size() > 0) {
+                mDeviceHandler.post(mCallOnIdle);
+            } else {
+                mDeviceHandler.post(mCallOnUnconfigured);
+            }
         }
     }
 
@@ -141,6 +228,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
     public CaptureRequest.Builder createCaptureRequest(int templateType)
             throws CameraAccessException {
         synchronized (mLock) {
+            checkIfCameraClosed();
 
             CameraMetadataNative templatedRequest = new CameraMetadataNative();
 
@@ -188,7 +276,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
         }
 
         synchronized (mLock) {
-
+            checkIfCameraClosed();
             int requestId;
 
             try {
@@ -208,6 +296,11 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
                 mRepeatingRequestIdStack.add(requestId);
             }
 
+            if (mIdle) {
+                mDeviceHandler.post(mCallOnActive);
+            }
+            mIdle = false;
+
             return requestId;
         }
     }
@@ -233,7 +326,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
     public void stopRepeating() throws CameraAccessException {
 
         synchronized (mLock) {
-
+            checkIfCameraClosed();
             while (!mRepeatingRequestIdStack.isEmpty()) {
                 int requestId = mRepeatingRequestIdStack.pop();
 
@@ -270,20 +363,11 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
     }
 
     @Override
-    public void setDeviceListener(StateListener listener, Handler handler) {
-        synchronized (mLock) {
-            if (listener != null) {
-                handler = checkHandler(handler);
-            }
-
-            mDeviceListener = listener;
-            mDeviceHandler = handler;
-        }
-    }
-
-    @Override
     public void flush() throws CameraAccessException {
         synchronized (mLock) {
+            checkIfCameraClosed();
+
+            mDeviceHandler.post(mCallOnBusy);
             try {
                 mRemoteDevice.flush();
             } catch (CameraRuntimeException e) {
@@ -297,9 +381,6 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
 
     @Override
     public void close() {
-
-        // TODO: every method should throw IllegalStateException after close has been called
-
         synchronized (mLock) {
 
             try {
@@ -312,8 +393,11 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
                 // impossible
             }
 
-            mRemoteDevice = null;
+            if (mRemoteDevice != null) {
+                mDeviceHandler.post(mCallOnClosed);
+            }
 
+            mRemoteDevice = null;
         }
     }
 
@@ -399,49 +483,44 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
 
         @Override
         public void onCameraError(final int errorCode) {
-            synchronized (mLock) {
-                if (CameraDevice.this.mDeviceListener == null) return;
-                final StateListener listener = CameraDevice.this.mDeviceListener;
-                Runnable r = null;
+            Runnable r = null;
+            if (isClosed()) return;
+
+            synchronized(mLock) {
                 switch (errorCode) {
                     case ERROR_CAMERA_DISCONNECTED:
-                        r = new Runnable() {
-                            public void run() {
-                                listener.onDisconnected(CameraDevice.this);
-                            }
-                        };
+                        r = mCallOnDisconnected;
                         break;
+                    default:
+                        Log.e(TAG, "Unknown error from camera device: " + errorCode);
+                        // no break
                     case ERROR_CAMERA_DEVICE:
                     case ERROR_CAMERA_SERVICE:
                         r = new Runnable() {
                             public void run() {
-                                listener.onError(CameraDevice.this, errorCode);
+                                if (!CameraDevice.this.isClosed()) {
+                                    mDeviceListener.onError(CameraDevice.this, errorCode);
+                                }
                             }
                         };
                         break;
-                    default:
-                        Log.e(TAG, "Unknown error from camera device: " + errorCode);
-                }
-                if (r != null) {
-                    CameraDevice.this.mDeviceHandler.post(r);
                 }
+                CameraDevice.this.mDeviceHandler.post(r);
             }
         }
 
         @Override
         public void onCameraIdle() {
+            if (isClosed()) return;
+
             if (DEBUG) {
                 Log.d(TAG, "Camera now idle");
             }
             synchronized (mLock) {
-                if (CameraDevice.this.mDeviceListener == null) return;
-                final StateListener listener = CameraDevice.this.mDeviceListener;
-                Runnable r = new Runnable() {
-                    public void run() {
-                        listener.onIdle(CameraDevice.this);
-                    }
-                };
-                CameraDevice.this.mDeviceHandler.post(r);
+                if (!CameraDevice.this.mIdle) {
+                    CameraDevice.this.mDeviceHandler.post(mCallOnIdle);
+                }
+                CameraDevice.this.mIdle = true;
             }
         }
 
@@ -461,14 +540,18 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
                 return;
             }
 
+            if (isClosed()) return;
+
             // Dispatch capture start notice
             holder.getHandler().post(
                 new Runnable() {
                     public void run() {
-                        holder.getListener().onCaptureStarted(
-                            CameraDevice.this,
-                            holder.getRequest(),
-                            timestamp);
+                        if (!CameraDevice.this.isClosed()) {
+                            holder.getListener().onCaptureStarted(
+                                CameraDevice.this,
+                                holder.getRequest(),
+                                timestamp);
+                        }
                     }
                 });
         }
@@ -503,6 +586,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
                 return;
             }
 
+            if (isClosed()) return;
+
             final CaptureRequest request = holder.getRequest();
             final CaptureResult resultAsCapture = new CaptureResult(result, request, requestId);
 
@@ -510,10 +595,12 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
                 new Runnable() {
                     @Override
                     public void run() {
-                        holder.getListener().onCaptureCompleted(
-                            CameraDevice.this,
-                            request,
-                            resultAsCapture);
+                        if (!CameraDevice.this.isClosed()){
+                            holder.getListener().onCaptureCompleted(
+                                CameraDevice.this,
+                                request,
+                                resultAsCapture);
+                        }
                     }
                 });
         }
@@ -541,4 +628,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice {
             throw new IllegalStateException("CameraDevice was already closed");
         }
     }
+
+    private boolean isClosed() {
+        synchronized(mLock) {
+            return (mRemoteDevice == null);
+        }
+    }
 }