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     protected 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     protected 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(String cameraId, CameraDevice.StateCallback listener)189         protected OpenListener(String cameraId, CameraDevice.StateCallback listener) {
190             mCameraId = cameraId;
191             mProxy = listener;
192         }
193 
OpenListener(CameraManager manager, String cameraId, CameraDevice.StateCallback listener, Handler handler)194         OpenListener(CameraManager manager, String cameraId, CameraDevice.StateCallback listener,
195                 Handler handler) throws CameraAccessException {
196             mCameraId = cameraId;
197             mProxy = listener;
198             manager.openCamera(cameraId, this, handler);
199         }
200 
201         // Freebie check to make sure we aren't calling functions multiple times.
202         // We should still test the state interactions in a separate more thorough test.
assertInitialState()203         private void assertInitialState() {
204             assertEquals(null, mDevice);
205             assertEquals(false, mDisconnected);
206             assertEquals(ERROR_UNINITIALIZED, mError);
207             assertEquals(false, mSuccess);
208         }
209 
210         @Override
onOpened(CameraDevice camera)211         public void onOpened(CameraDevice camera) {
212             if (VERBOSE) {
213                 Log.v(TAG, "onOpened: camera " + ((camera != null) ? camera.getId() : "null"));
214             }
215 
216             synchronized (mLock) {
217                 assertInitialState();
218                 mNoReply = false;
219                 mSuccess = true;
220                 mDevice = camera;
221                 mDeviceReady.open();
222 
223                 if (mTimedOut && camera != null) {
224                     camera.close();
225                     return;
226                 }
227             }
228 
229             if (mProxy != null) mProxy.onOpened(camera);
230         }
231 
232         @Override
onDisconnected(CameraDevice camera)233         public void onDisconnected(CameraDevice camera) {
234             if (VERBOSE) {
235                 Log.v(TAG, "onDisconnected: camera "
236                         + ((camera != null) ? camera.getId() : "null"));
237             }
238 
239             synchronized (mLock) {
240                 // Don't assert all initial states. onDisconnected can be called after camera
241                 // is successfully opened.
242                 assertEquals(false, mDisconnected);
243                 mNoReply = false;
244                 mDisconnected = true;
245                 mDevice = camera;
246                 mDeviceReady.open();
247 
248                 if (mTimedOut && camera != null) {
249                     camera.close();
250                     return;
251                 }
252             }
253 
254             if (mProxy != null) mProxy.onDisconnected(camera);
255         }
256 
257         @Override
onError(CameraDevice camera, int error)258         public void onError(CameraDevice camera, int error) {
259             if (VERBOSE) {
260                 Log.v(TAG, "onError: camera " + ((camera != null) ? camera.getId() : "null"));
261             }
262 
263             if (error <= 0) {
264                 throw new AssertionError("Expected error to be a positive number");
265             }
266 
267             synchronized (mLock) {
268                 // Don't assert initial state. Error can happen later.
269                 mNoReply = false;
270                 mError = error;
271                 mDevice = camera;
272                 mDeviceReady.open();
273 
274                 if (mTimedOut && camera != null) {
275                     camera.close();
276                     return;
277                 }
278             }
279 
280             if (mProxy != null) mProxy.onError(camera, error);
281         }
282 
283         @Override
onClosed(CameraDevice camera)284         public void onClosed(CameraDevice camera) {
285             if (mProxy != null) mProxy.onClosed(camera);
286         }
287 
blockUntilOpen()288         public CameraDevice blockUntilOpen() throws BlockingOpenException {
289             /**
290              * Block until onOpened, onError, or onDisconnected
291              */
292             if (!mDeviceReady.block(OPEN_TIME_OUT_MS)) {
293 
294                 synchronized (mLock) {
295                     if (mNoReply) { // Give the async camera a fighting chance (required)
296                         mTimedOut = true; // Clean up camera if it ever arrives later
297                         throw new TimeoutRuntimeException(String.format(
298                                 "Timed out after %d ms while trying to open camera device %s",
299                                 OPEN_TIME_OUT_MS, mCameraId));
300                     }
301                 }
302             }
303 
304             synchronized (mLock) {
305                 /**
306                  * Determine which state we ended up in:
307                  *
308                  * - Throw exceptions for onError/onDisconnected
309                  * - Return device for onOpened
310                  */
311                 if (!mSuccess && mDevice != null) {
312                     mDevice.close();
313                 }
314 
315                 if (mSuccess) {
316                     return mDevice;
317                 } else {
318                     if (mDisconnected) {
319                         throw new BlockingOpenException(
320                                 BlockingOpenException.ERROR_DISCONNECTED,
321                                 "Failed to open camera device: it is disconnected");
322                     } else if (mError != ERROR_UNINITIALIZED) {
323                         throw new BlockingOpenException(
324                                 mError,
325                                 "Failed to open camera device: error code " + mError);
326                     } else {
327                         throw new AssertionError("Failed to open camera device (impl bug)");
328                     }
329                 }
330             }
331         }
332     }
333 }
334