/* * 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. * *

Provides a waiter that can be used to block until the next unobserved state of the * requested type arrives.

* *

Pass-through all StateCallback changes to the proxy.

* * @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 > 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. * *

Only one thread should wait at a time.

*/ 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. * *

Does not consume any of the states from the state waiter.

* * @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 preparedSurfaces = mPreparedSurfaces.get(session); if (preparedSurfaces == null) { preparedSurfaces = new ArrayList(); } preparedSurfaces.add(surface); mPreparedSurfaces.put(session, preparedSurfaces); mPreparedSurfaces.notifyAll(); } } /** * 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 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 { 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; } } }