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  */
16 package com.android.ex.camera2.blocking;
17 
18 import android.hardware.camera2.CameraAccessException;
19 import android.hardware.camera2.CameraDevice;
20 import android.hardware.camera2.CameraManager;
21 import android.os.ConditionVariable;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.util.Log;
25 
26 import com.android.ex.camera2.exceptions.TimeoutRuntimeException;
27 
28 import 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}ms when timing out.</p>
38  */
39 public class BlockingCameraManager {
40 
41     private static final String TAG = "BlockingCameraManager";
42     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
43 
44     private static final int OPEN_TIME_OUT_MS = 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 
wasDisconnected()59         public boolean wasDisconnected() {
60             return mError == ERROR_DISCONNECTED;
61         }
62 
wasError()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          */
getCode()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          */
BlockingOpenException(int errorCode, String message)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      */
BlockingCameraManager(CameraManager manager)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      */
openCamera(String cameraId, CameraDevice.StateCallback listener, Handler handler)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 
assertEquals(Object a, Object b)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_MS} 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 
OpenListener(CameraManager manager, String cameraId, CameraDevice.StateCallback listener, Handler handler)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.
assertInitialState()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
onOpened(CameraDevice camera)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
onDisconnected(CameraDevice camera)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                 // Don't assert all initial states. onDisconnected can be called after camera
237                 // is successfully opened.
238                 assertEquals(false, mDisconnected);
239                 mNoReply = false;
240                 mDisconnected = true;
241                 mDevice = camera;
242                 mDeviceReady.open();
243 
244                 if (mTimedOut && camera != null) {
245                     camera.close();
246                     return;
247                 }
248             }
249 
250             if (mProxy != null) mProxy.onDisconnected(camera);
251         }
252 
253         @Override
onError(CameraDevice camera, int error)254         public void onError(CameraDevice camera, int error) {
255             if (VERBOSE) {
256                 Log.v(TAG, "onError: camera " + ((camera != null) ? camera.getId() : "null"));
257             }
258 
259             if (error <= 0) {
260                 throw new AssertionError("Expected error to be a positive number");
261             }
262 
263             synchronized (mLock) {
264                 // Don't assert initial state. Error can happen later.
265                 mNoReply = false;
266                 mError = error;
267                 mDevice = camera;
268                 mDeviceReady.open();
269 
270                 if (mTimedOut && camera != null) {
271                     camera.close();
272                     return;
273                 }
274             }
275 
276             if (mProxy != null) mProxy.onError(camera, error);
277         }
278 
279         @Override
onClosed(CameraDevice camera)280         public void onClosed(CameraDevice camera) {
281             if (mProxy != null) mProxy.onClosed(camera);
282         }
283 
blockUntilOpen()284         CameraDevice blockUntilOpen() throws BlockingOpenException {
285             /**
286              * Block until onOpened, onError, or onDisconnected
287              */
288             if (!mDeviceReady.block(OPEN_TIME_OUT_MS)) {
289 
290                 synchronized (mLock) {
291                     if (mNoReply) { // Give the async camera a fighting chance (required)
292                         mTimedOut = true; // Clean up camera if it ever arrives later
293                         throw new TimeoutRuntimeException(String.format(
294                                 "Timed out after %d ms while trying to open camera device %s",
295                                 OPEN_TIME_OUT_MS, mCameraId));
296                     }
297                 }
298             }
299 
300             synchronized (mLock) {
301                 /**
302                  * Determine which state we ended up in:
303                  *
304                  * - Throw exceptions for onError/onDisconnected
305                  * - Return device for onOpened
306                  */
307                 if (!mSuccess && mDevice != null) {
308                     mDevice.close();
309                 }
310 
311                 if (mSuccess) {
312                     return mDevice;
313                 } else {
314                     if (mDisconnected) {
315                         throw new BlockingOpenException(
316                                 BlockingOpenException.ERROR_DISCONNECTED,
317                                 "Failed to open camera device: it is disconnected");
318                     } else if (mError != ERROR_UNINITIALIZED) {
319                         throw new BlockingOpenException(
320                                 mError,
321                                 "Failed to open camera device: error code " + mError);
322                     } else {
323                         throw new AssertionError("Failed to open camera device (impl bug)");
324                     }
325                 }
326             }
327         }
328     }
329 }
330