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