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