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