1/*
2 * Copyright 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.ex.camera2.blocking;
17
18import android.hardware.camera2.CameraCaptureSession;
19import android.os.ConditionVariable;
20import android.os.SystemClock;
21import android.util.Log;
22import android.view.Surface;
23
24import com.android.ex.camera2.exceptions.TimeoutRuntimeException;
25import com.android.ex.camera2.utils.StateChangeListener;
26import com.android.ex.camera2.utils.StateWaiter;
27
28import java.util.List;
29import java.util.ArrayList;
30import java.util.HashMap;
31import java.util.concurrent.Future;
32import java.util.concurrent.TimeUnit;
33import java.util.concurrent.TimeoutException;
34
35
36/**
37 * A camera session listener that implements blocking operations on session state changes.
38 *
39 * <p>Provides a waiter that can be used to block until the next unobserved state of the
40 * requested type arrives.</p>
41 *
42 * <p>Pass-through all StateCallback changes to the proxy.</p>
43 *
44 * @see #getStateWaiter
45 */
46public class BlockingSessionCallback extends CameraCaptureSession.StateCallback {
47    /**
48     * Session is configured, ready for captures
49     */
50    public static final int SESSION_CONFIGURED = 0;
51
52    /**
53     * Session has failed to configure, can't do any captures
54     */
55    public static final int SESSION_CONFIGURE_FAILED = 1;
56
57    /**
58     * Session is ready
59     */
60    public static final int SESSION_READY = 2;
61
62    /**
63     * Session is active (transitory)
64     */
65    public static final int SESSION_ACTIVE = 3;
66
67    /**
68     * Session is closed
69     */
70    public static final int SESSION_CLOSED = 4;
71
72    private static final int NUM_STATES = 5;
73
74    /*
75     * Private fields
76     */
77    private static final String TAG = "BlockingSessionCallback";
78    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
79
80    private final CameraCaptureSession.StateCallback mProxy;
81    private final SessionFuture mSessionFuture = new SessionFuture();
82
83    private final StateWaiter mStateWaiter = new StateWaiter(sStateNames);
84    private final StateChangeListener mStateChangeListener = mStateWaiter.getListener();
85    private final HashMap<CameraCaptureSession, List<Surface> > mPreparedSurfaces = new HashMap<>();
86
87    private static final String[] sStateNames = {
88        "SESSION_CONFIGURED",
89        "SESSION_CONFIGURE_FAILED",
90        "SESSION_READY",
91        "SESSION_ACTIVE",
92        "SESSION_CLOSED"
93    };
94
95    /**
96     * Create a blocking session listener without forwarding the session listener invocations
97     * to another session listener.
98     */
99    public BlockingSessionCallback() {
100        mProxy = null;
101    }
102
103    /**
104     * Create a blocking session listener; forward original listener invocations
105     * into {@code listener}.
106     *
107     * @param listener a non-{@code null} listener to forward invocations into
108     *
109     * @throws NullPointerException if {@code listener} was {@code null}
110     */
111    public BlockingSessionCallback(CameraCaptureSession.StateCallback listener) {
112        if (listener == null) {
113            throw new NullPointerException("listener must not be null");
114        }
115        mProxy = listener;
116    }
117
118    /**
119     * Acquire the state waiter; can be used to block until a set of state transitions have
120     * been reached.
121     *
122     * <p>Only one thread should wait at a time.</p>
123     */
124    public StateWaiter getStateWaiter() {
125        return mStateWaiter;
126    }
127
128    /**
129     * Return session if already have it; otherwise wait until any of the session listener
130     * invocations fire and the session is available.
131     *
132     * <p>Does not consume any of the states from the state waiter.</p>
133     *
134     * @param timeoutMs how many milliseconds to wait for
135     * @return a non-{@code null} {@link CameraCaptureSession} instance
136     *
137     * @throws TimeoutRuntimeException if waiting for more than {@long timeoutMs}
138     */
139    public CameraCaptureSession waitAndGetSession(long timeoutMs) {
140        try {
141            return mSessionFuture.get(timeoutMs, TimeUnit.MILLISECONDS);
142        } catch (TimeoutException e) {
143            throw new TimeoutRuntimeException(
144                    String.format("Failed to get session after %s milliseconds", timeoutMs), e);
145        }
146    }
147
148    /*
149     * CameraCaptureSession.StateCallback implementation
150     */
151
152    @Override
153    public void onActive(CameraCaptureSession session) {
154        mSessionFuture.setSession(session);
155        if (mProxy != null) mProxy.onActive(session);
156        mStateChangeListener.onStateChanged(SESSION_ACTIVE);
157    }
158
159    @Override
160    public void onClosed(CameraCaptureSession session) {
161        mSessionFuture.setSession(session);
162        if (mProxy != null) mProxy.onClosed(session);
163        mStateChangeListener.onStateChanged(SESSION_CLOSED);
164        synchronized (mPreparedSurfaces) {
165            mPreparedSurfaces.remove(session);
166        }
167    }
168
169    @Override
170    public void onConfigured(CameraCaptureSession session) {
171        mSessionFuture.setSession(session);
172        if (mProxy != null) {
173            mProxy.onConfigured(session);
174        }
175        mStateChangeListener.onStateChanged(SESSION_CONFIGURED);
176    }
177
178    @Override
179    public void onConfigureFailed(CameraCaptureSession session) {
180        mSessionFuture.setSession(session);
181        if (mProxy != null) {
182            mProxy.onConfigureFailed(session);
183        }
184        mStateChangeListener.onStateChanged(SESSION_CONFIGURE_FAILED);
185    }
186
187    @Override
188    public void onReady(CameraCaptureSession session) {
189        mSessionFuture.setSession(session);
190        if (mProxy != null) {
191            mProxy.onReady(session);
192        }
193        mStateChangeListener.onStateChanged(SESSION_READY);
194    }
195
196    @Override
197    public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
198        mSessionFuture.setSession(session);
199        if (mProxy != null) {
200            mProxy.onSurfacePrepared(session, surface);
201        }
202        // Surface prepared doesn't cause a session state change, so don't trigger the
203        // state change listener
204        synchronized (mPreparedSurfaces) {
205            List<Surface> preparedSurfaces = mPreparedSurfaces.get(session);
206            if (preparedSurfaces == null) {
207                preparedSurfaces = new ArrayList<Surface>();
208            }
209            preparedSurfaces.add(surface);
210            mPreparedSurfaces.put(session, preparedSurfaces);
211            mPreparedSurfaces.notifyAll();
212        }
213    }
214
215    @Override
216    public void onCaptureQueueEmpty(CameraCaptureSession session) {
217        mSessionFuture.setSession(session);
218        if (mProxy != null) {
219            mProxy.onCaptureQueueEmpty(session);
220        }
221    }
222
223    /**
224     * Wait until the designated surface is prepared by the camera capture session.
225     *
226     * @param session the input {@link CameraCaptureSession} to wait for
227     * @param surface the input {@link Surface} to wait for
228     * @param timeoutMs how many milliseconds to wait for
229     *
230     * @throws TimeoutRuntimeException if waiting for more than {@long timeoutMs}
231     */
232    public void waitForSurfacePrepared(
233            CameraCaptureSession session, Surface surface, long timeoutMs) {
234        synchronized (mPreparedSurfaces) {
235            List<Surface> preparedSurfaces = mPreparedSurfaces.get(session);
236            if (preparedSurfaces != null && preparedSurfaces.contains(surface)) {
237                return;
238            }
239            try {
240                long waitTimeRemaining = timeoutMs;
241                while (waitTimeRemaining > 0) {
242                    long waitStartTime = SystemClock.elapsedRealtime();
243                    mPreparedSurfaces.wait(timeoutMs);
244                    long waitTime = SystemClock.elapsedRealtime() - waitStartTime;
245                    waitTimeRemaining -= waitTime;
246                    preparedSurfaces = mPreparedSurfaces.get(session);
247                    if (waitTimeRemaining >= 0 && preparedSurfaces != null &&
248                            preparedSurfaces.contains(surface)) {
249                        return;
250                    }
251                }
252                throw new TimeoutRuntimeException(
253                        "Unable to get Surface prepared in " + timeoutMs + "ms");
254            } catch (InterruptedException ie) {
255                throw new AssertionError();
256            }
257        }
258    }
259
260    private static class SessionFuture implements Future<CameraCaptureSession> {
261        private volatile CameraCaptureSession mSession;
262        ConditionVariable mCondVar = new ConditionVariable(/*opened*/false);
263
264        public void setSession(CameraCaptureSession session) {
265            mSession = session;
266            mCondVar.open();
267        }
268
269        @Override
270        public boolean cancel(boolean mayInterruptIfRunning) {
271            return false; // don't allow canceling this task
272        }
273
274        @Override
275        public boolean isCancelled() {
276            return false; // can never cancel this task
277        }
278
279        @Override
280        public boolean isDone() {
281            return mSession != null;
282        }
283
284        @Override
285        public CameraCaptureSession get() {
286            mCondVar.block();
287            return mSession;
288        }
289
290        @Override
291        public CameraCaptureSession get(long timeout, TimeUnit unit) throws TimeoutException {
292            long timeoutMs = unit.convert(timeout, TimeUnit.MILLISECONDS);
293            if (!mCondVar.block(timeoutMs)) {
294                throw new TimeoutException(
295                        "Failed to receive session after " + timeout + " " + unit);
296            }
297
298            if (mSession == null) {
299                throw new AssertionError();
300            }
301            return mSession;
302        }
303
304    }
305}
306