Home | History | Annotate | Download | only in blocking
      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 
     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_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 
    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_MS)) {
    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_MS, 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