1/*
2 * Copyright 2013 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.CameraAccessException;
19import android.hardware.camera2.CameraDevice;
20import android.hardware.camera2.CameraManager;
21import android.os.ConditionVariable;
22import android.os.Handler;
23import android.os.Looper;
24import android.util.Log;
25
26import com.android.ex.camera2.exceptions.TimeoutRuntimeException;
27
28import java.util.Objects;
29
30/**
31 * Expose {@link CameraManager} functionality with blocking functions.
32 *
33 * <p>Safe to use at the same time as the regular CameraManager, so this does not
34 * duplicate any functionality that is already blocking.</p>
35 *
36 * <p>Be careful when using this from UI thread! This function will typically block
37 * for about 500ms when successful, and as long as {@value #OPEN_TIME_OUT}ms when timing out.</p>
38 */
39public class BlockingCameraManager {
40
41    private static final String TAG = "CameraBlockingOpener";
42    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
43
44    private static final int OPEN_TIME_OUT = 2000; // ms time out for openCamera
45
46    /**
47     * Exception thrown by {@link #openCamera} if the open fails asynchronously.
48     */
49    public static class BlockingOpenException extends Exception {
50        /**
51         * Suppress eclipse warning
52         */
53        private static final long serialVersionUID = 12397123891238912L;
54
55        public static final int ERROR_DISCONNECTED = 0; // Does not clash with ERROR_...
56
57        private final int mError;
58
59        public boolean wasDisconnected() {
60            return mError == ERROR_DISCONNECTED;
61        }
62
63        public boolean wasError() {
64            return mError != ERROR_DISCONNECTED;
65        }
66
67        /**
68         * Returns the error code {@link ERROR_DISCONNECTED} if disconnected, or one of
69         * {@code CameraDevice.StateCallback#ERROR_*} if there was another error.
70         *
71         * @return int Disconnect/error code
72         */
73        public int getCode() {
74            return mError;
75        }
76
77        /**
78         * Thrown when camera device enters error state during open, or if
79         * it disconnects.
80         *
81         * @param errorCode
82         * @param message
83         *
84         * @see {@link CameraDevice.StateCallback#ERROR_CAMERA_DEVICE}
85         */
86        public BlockingOpenException(int errorCode, String message) {
87            super(message);
88            mError = errorCode;
89        }
90    }
91
92    private final CameraManager mManager;
93
94    /**
95     * Create a new blocking camera manager.
96     *
97     * @param manager
98     *            CameraManager returned by
99     *            {@code Context.getSystemService(Context.CAMERA_SERVICE)}
100     */
101    public BlockingCameraManager(CameraManager manager) {
102        if (manager == null) {
103            throw new IllegalArgumentException("manager must not be null");
104        }
105        mManager = manager;
106    }
107
108    /**
109     * Open the camera, blocking it until it succeeds or fails.
110     *
111     * <p>Note that the Handler provided must not be null. Furthermore, if there is a handler,
112     * its Looper must not be the current thread's Looper. Otherwise we'd never receive
113     * the callbacks from the CameraDevice since this function would prevent them from being
114     * processed.</p>
115     *
116     * <p>Throws {@link CameraAccessException} for the same reason {@link CameraManager#openCamera}
117     * does.</p>
118     *
119     * <p>Throws {@link BlockingOpenException} when the open fails asynchronously (due to
120     * {@link CameraDevice.StateCallback#onDisconnected(CameraDevice)} or
121     * ({@link CameraDevice.StateCallback#onError(CameraDevice)}.</p>
122     *
123     * <p>Throws {@link TimeoutRuntimeException} if opening times out. This is usually
124     * highly unrecoverable, and all future calls to opening that camera will fail since the
125     * service will think it's busy. This class will do its best to clean up eventually.</p>
126     *
127     * @param cameraId
128     *            Id of the camera
129     * @param listener
130     *            Listener to the camera. onOpened, onDisconnected, onError need not be implemented.
131     * @param handler
132     *            Handler which to run the listener on. Must not be null.
133     *
134     * @return CameraDevice
135     *
136     * @throws IllegalArgumentException
137     *            If the handler is null, or if the handler's looper is current.
138     * @throws CameraAccessException
139     *            If open fails immediately.
140     * @throws BlockingOpenException
141     *            If open fails after blocking for some amount of time.
142     * @throws TimeoutRuntimeException
143     *            If opening times out. Typically unrecoverable.
144     */
145    public CameraDevice openCamera(String cameraId, CameraDevice.StateCallback listener,
146            Handler handler) throws CameraAccessException, BlockingOpenException {
147
148        if (handler == null) {
149            throw new IllegalArgumentException("handler must not be null");
150        } else if (handler.getLooper() == Looper.myLooper()) {
151            throw new IllegalArgumentException("handler's looper must not be the current looper");
152        }
153
154        return (new OpenListener(mManager, cameraId, listener, handler)).blockUntilOpen();
155    }
156
157    private static void assertEquals(Object a, Object b) {
158        if (!Objects.equals(a, b)) {
159            throw new AssertionError("Expected " + a + ", but got " + b);
160        }
161    }
162
163    /**
164     * Block until CameraManager#openCamera finishes with onOpened/onError/onDisconnected
165     *
166     * <p>Pass-through all StateCallback changes to the proxy.</p>
167     *
168     * <p>Time out after {@link #OPEN_TIME_OUT} and unblock. Clean up camera if it arrives
169     * later.</p>
170     */
171    private class OpenListener extends CameraDevice.StateCallback {
172        private static final int ERROR_UNINITIALIZED = -1;
173
174        private final String mCameraId;
175
176        private final CameraDevice.StateCallback mProxy;
177
178        private final Object mLock = new Object();
179        private final ConditionVariable mDeviceReady = new ConditionVariable();
180
181        private CameraDevice mDevice = null;
182        private boolean mSuccess = false;
183        private int mError = ERROR_UNINITIALIZED;
184        private boolean mDisconnected = false;
185
186        private boolean mNoReply = true; // Start with no reply until proven otherwise
187        private boolean mTimedOut = false;
188
189        OpenListener(CameraManager manager, String cameraId,
190                CameraDevice.StateCallback listener, Handler handler)
191                throws CameraAccessException {
192            mCameraId = cameraId;
193            mProxy = listener;
194            manager.openCamera(cameraId, this, handler);
195        }
196
197        // Freebie check to make sure we aren't calling functions multiple times.
198        // We should still test the state interactions in a separate more thorough test.
199        private void assertInitialState() {
200            assertEquals(null, mDevice);
201            assertEquals(false, mDisconnected);
202            assertEquals(ERROR_UNINITIALIZED, mError);
203            assertEquals(false, mSuccess);
204        }
205
206        @Override
207        public void onOpened(CameraDevice camera) {
208            if (VERBOSE) {
209                Log.v(TAG, "onOpened: camera " + ((camera != null) ? camera.getId() : "null"));
210            }
211
212            synchronized (mLock) {
213                assertInitialState();
214                mNoReply = false;
215                mSuccess = true;
216                mDevice = camera;
217                mDeviceReady.open();
218
219                if (mTimedOut && camera != null) {
220                    camera.close();
221                    return;
222                }
223            }
224
225            if (mProxy != null) mProxy.onOpened(camera);
226        }
227
228        @Override
229        public void onDisconnected(CameraDevice camera) {
230            if (VERBOSE) {
231                Log.v(TAG, "onDisconnected: camera "
232                        + ((camera != null) ? camera.getId() : "null"));
233            }
234
235            synchronized (mLock) {
236                assertInitialState();
237                mNoReply = false;
238                mDisconnected = true;
239                mDevice = camera;
240                mDeviceReady.open();
241
242                if (mTimedOut && camera != null) {
243                    camera.close();
244                    return;
245                }
246            }
247
248            if (mProxy != null) mProxy.onDisconnected(camera);
249        }
250
251        @Override
252        public void onError(CameraDevice camera, int error) {
253            if (VERBOSE) {
254                Log.v(TAG, "onError: camera " + ((camera != null) ? camera.getId() : "null"));
255            }
256
257            if (error <= 0) {
258                throw new AssertionError("Expected error to be a positive number");
259            }
260
261            synchronized (mLock) {
262                // Don't assert initial state. Error can happen later.
263                mNoReply = false;
264                mError = error;
265                mDevice = camera;
266                mDeviceReady.open();
267
268                if (mTimedOut && camera != null) {
269                    camera.close();
270                    return;
271                }
272            }
273
274            if (mProxy != null) mProxy.onError(camera, error);
275        }
276
277        @Override
278        public void onClosed(CameraDevice camera) {
279            if (mProxy != null) mProxy.onClosed(camera);
280        }
281
282        CameraDevice blockUntilOpen() throws BlockingOpenException {
283            /**
284             * Block until onOpened, onError, or onDisconnected
285             */
286            if (!mDeviceReady.block(OPEN_TIME_OUT)) {
287
288                synchronized (mLock) {
289                    if (mNoReply) { // Give the async camera a fighting chance (required)
290                        mTimedOut = true; // Clean up camera if it ever arrives later
291                        throw new TimeoutRuntimeException(String.format(
292                                "Timed out after %d ms while trying to open camera device %s",
293                                OPEN_TIME_OUT, mCameraId));
294                    }
295                }
296            }
297
298            synchronized (mLock) {
299                /**
300                 * Determine which state we ended up in:
301                 *
302                 * - Throw exceptions for onError/onDisconnected
303                 * - Return device for onOpened
304                 */
305                if (!mSuccess && mDevice != null) {
306                    mDevice.close();
307                }
308
309                if (mSuccess) {
310                    return mDevice;
311                } else {
312                    if (mDisconnected) {
313                        throw new BlockingOpenException(
314                                BlockingOpenException.ERROR_DISCONNECTED,
315                                "Failed to open camera device: it is disconnected");
316                    } else if (mError != ERROR_UNINITIALIZED) {
317                        throw new BlockingOpenException(
318                                mError,
319                                "Failed to open camera device: error code " + mError);
320                    } else {
321                        throw new AssertionError("Failed to open camera device (impl bug)");
322                    }
323                }
324            }
325        }
326    }
327}
328