content: /*
   * Copyright 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 com.android.ex.camera2.blocking;
  
  import android.hardware.camera2.CameraCaptureSession;
  import android.os.ConditionVariable;
  import android.os.SystemClock;
  import android.util.Log;
  import android.view.Surface;
  
  import com.android.ex.camera2.exceptions.TimeoutRuntimeException;
  import com.android.ex.camera2.utils.StateChangeListener;
  import com.android.ex.camera2.utils.StateWaiter;
  
  import java.util.List;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.concurrent.Future;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.TimeoutException;
  
  
  /**
   * A camera session listener that implements blocking operations on session state changes.
   *
   * <p>Provides a waiter that can be used to block until the next unobserved state of the
   * requested type arrives.</p>
   *
   * <p>Pass-through all StateCallback changes to the proxy.</p>
   *
   * @see #getStateWaiter
   */
  public class BlockingSessionCallback extends CameraCaptureSession.StateCallback {
      /**
       * Session is configured, ready for captures
       */
      public static final int SESSION_CONFIGURED = 0;
  
      /**
       * Session has failed to configure, can't do any captures
       */
      public static final int SESSION_CONFIGURE_FAILED = 1;
  
      /**
       * Session is ready
       */
      public static final int SESSION_READY = 2;
  
      /**
       * Session is active (transitory)
       */
      public static final int SESSION_ACTIVE = 3;
  
      /**
       * Session is closed
       */
      public static final int SESSION_CLOSED = 4;
  
      private static final int NUM_STATES = 5;
  
      /*
       * Private fields
       */
      private static final String TAG = "BlockingSessionCallback";
      private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
  
      private final CameraCaptureSession.StateCallback mProxy;
      private final SessionFuture mSessionFuture = new SessionFuture();
  
      private final StateWaiter mStateWaiter = new StateWaiter(sStateNames);
      private final StateChangeListener mStateChangeListener = mStateWaiter.getListener();
      private final HashMap<CameraCaptureSession, List<Surface> > mPreparedSurfaces = new HashMap<>();
  
      private static final String[] sStateNames = {
          "SESSION_CONFIGURED",
          "SESSION_CONFIGURE_FAILED",
          "SESSION_READY",
          "SESSION_ACTIVE",
          "SESSION_CLOSED"
      };
  
      /**
       * Create a blocking session listener without forwarding the session listener invocations
       * to another session listener.
       */
      public BlockingSessionCallback() {
          mProxy = null;
      }
  
      /**
       * Create a blocking session listener; forward original listener invocations
       * into {@code listener}.
       *
       * @param listener a non-{@code null} listener to forward invocations into
       *
       * @throws NullPointerException if {@code listener} was {@code null}
       */
      public BlockingSessionCallback(CameraCaptureSession.StateCallback listener) {
          if (listener == null) {
              throw new NullPointerException("listener must not be null");
          }
          mProxy = listener;
      }
  
      /**
       * Acquire the state waiter; can be used to block until a set of state transitions have
       * been reached.
       *
       * <p>Only one thread should wait at a time.</p>
       */
      public StateWaiter getStateWaiter() {
          return mStateWaiter;
      }
  
      /**
       * Return session if already have it; otherwise wait until any of the session listener
       * invocations fire and the session is available.
       *
       * <p>Does not consume any of the states from the state waiter.</p>
       *
       * @param timeoutMs how many milliseconds to wait for
       * @return a non-{@code null} {@link CameraCaptureSession} instance
       *
       * @throws TimeoutRuntimeException if waiting for more than {@long timeoutMs}
       */
      public CameraCaptureSession waitAndGetSession(long timeoutMs) {
          try {
              return mSessionFuture.get(timeoutMs, TimeUnit.MILLISECONDS);
          } catch (TimeoutException e) {
              throw new TimeoutRuntimeException(
                      String.format("Failed to get session after %s milliseconds", timeoutMs), e);
          }
      }
  
      /*
       * CameraCaptureSession.StateCallback implementation
       */
  
      @Override
      public void onActive(CameraCaptureSession session) {
          mSessionFuture.setSession(session);
          if (mProxy != null) mProxy.onActive(session);
          mStateChangeListener.onStateChanged(SESSION_ACTIVE);
      }
  
      @Override
      public void onClosed(CameraCaptureSession session) {
          mSessionFuture.setSession(session);
          if (mProxy != null) mProxy.onClosed(session);
          mStateChangeListener.onStateChanged(SESSION_CLOSED);
          synchronized (mPreparedSurfaces) {
              mPreparedSurfaces.remove(session);
          }
      }
  
      @Override
      public void onConfigured(CameraCaptureSession session) {
          mSessionFuture.setSession(session);
          if (mProxy != null) {
              mProxy.onConfigured(session);
          }
          mStateChangeListener.onStateChanged(SESSION_CONFIGURED);
      }
  
      @Override
      public void onConfigureFailed(CameraCaptureSession session) {
          mSessionFuture.setSession(session);
          if (mProxy != null) {
              mProxy.onConfigureFailed(session);
          }
          mStateChangeListener.onStateChanged(SESSION_CONFIGURE_FAILED);
      }
  
      @Override
      public void onReady(CameraCaptureSession session) {
          mSessionFuture.setSession(session);
          if (mProxy != null) {
              mProxy.onReady(session);
          }
          mStateChangeListener.onStateChanged(SESSION_READY);
      }
  
      @Override
      public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
          mSessionFuture.setSession(session);
          if (mProxy != null) {
              mProxy.onSurfacePrepared(session, surface);
          }
          // Surface prepared doesn't cause a session state change, so don't trigger the
          // state change listener
          synchronized (mPreparedSurfaces) {
              List<Surface> preparedSurfaces = mPreparedSurfaces.get(session);
              if (preparedSurfaces == null) {
                  preparedSurfaces = new ArrayList<Surface>();
              }
              preparedSurfaces.add(surface);
              mPreparedSurfaces.put(session, preparedSurfaces);
              mPreparedSurfaces.notifyAll();
          }
      }
  
      @Override
      public void onCaptureQueueEmpty(CameraCaptureSession session) {
          mSessionFuture.setSession(session);
          if (mProxy != null) {
              mProxy.onCaptureQueueEmpty(session);
          }
      }
  
      /**
       * Wait until the designated surface is prepared by the camera capture session.
       *
       * @param session the input {@link CameraCaptureSession} to wait for
       * @param surface the input {@link Surface} to wait for
       * @param timeoutMs how many milliseconds to wait for
       *
       * @throws TimeoutRuntimeException if waiting for more than {@long timeoutMs}
       */
      public void waitForSurfacePrepared(
              CameraCaptureSession session, Surface surface, long timeoutMs) {
          synchronized (mPreparedSurfaces) {
              List<Surface> preparedSurfaces = mPreparedSurfaces.get(session);
              if (preparedSurfaces != null && preparedSurfaces.contains(surface)) {
                  return;
              }
              try {
                  long waitTimeRemaining = timeoutMs;
                  while (waitTimeRemaining > 0) {
                      long waitStartTime = SystemClock.elapsedRealtime();
                      mPreparedSurfaces.wait(timeoutMs);
                      long waitTime = SystemClock.elapsedRealtime() - waitStartTime;
                      waitTimeRemaining -= waitTime;
                      preparedSurfaces = mPreparedSurfaces.get(session);
                      if (waitTimeRemaining >= 0 && preparedSurfaces != null &&
                              preparedSurfaces.contains(surface)) {
                          return;
                      }
                  }
                  throw new TimeoutRuntimeException(
                          "Unable to get Surface prepared in " + timeoutMs + "ms");
              } catch (InterruptedException ie) {
                  throw new AssertionError();
              }
          }
      }
  
      private static class SessionFuture implements Future<CameraCaptureSession> {
          private volatile CameraCaptureSession mSession;
          ConditionVariable mCondVar = new ConditionVariable(/*opened*/false);
  
          public void setSession(CameraCaptureSession session) {
              mSession = session;
              mCondVar.open();
          }
  
          @Override
          public boolean cancel(boolean mayInterruptIfRunning) {
              return false; // don't allow canceling this task
          }
  
          @Override
          public boolean isCancelled() {
              return false; // can never cancel this task
          }
  
          @Override
          public boolean isDone() {
              return mSession != null;
          }
  
          @Override
          public CameraCaptureSession get() {
              mCondVar.block();
              return mSession;
          }
  
          @Override
          public CameraCaptureSession get(long timeout, TimeUnit unit) throws TimeoutException {
              long timeoutMs = unit.convert(timeout, TimeUnit.MILLISECONDS);
              if (!mCondVar.block(timeoutMs)) {
                  throw new TimeoutException(
                          "Failed to receive session after " + timeout + " " + unit);
              }
  
              if (mSession == null) {
                  throw new AssertionError();
              }
              return mSession;
          }
  
      }
  }
  