CameraManager.java revision bd9b106806f9792be210cc2d9848d8b1f4b9664d
1/*
2 * Copyright (C) 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
17package android.hardware.camera2;
18
19import android.content.Context;
20import android.hardware.ICameraService;
21import android.hardware.ICameraServiceListener;
22import android.hardware.CameraInfo;
23import android.hardware.camera2.impl.CameraMetadataNative;
24import android.hardware.camera2.legacy.CameraDeviceUserShim;
25import android.hardware.camera2.legacy.LegacyMetadataMapper;
26import android.hardware.camera2.utils.CameraServiceBinderDecorator;
27import android.hardware.camera2.utils.CameraRuntimeException;
28import android.hardware.camera2.utils.BinderHolder;
29import android.os.IBinder;
30import android.os.Handler;
31import android.os.Looper;
32import android.os.RemoteException;
33import android.os.ServiceManager;
34import android.util.Log;
35import android.util.ArrayMap;
36
37import java.util.ArrayList;
38
39/**
40 * <p>A system service manager for detecting, characterizing, and connecting to
41 * {@link CameraDevice CameraDevices}.</p>
42 *
43 * <p>You can get an instance of this class by calling
44 * {@link android.content.Context#getSystemService(String) Context.getSystemService()}.</p>
45 *
46 * <pre>CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);</pre>
47 *
48 * <p>For more details about communicating with camera devices, read the Camera
49 * developer guide or the {@link android.hardware.camera2 camera2}
50 * package documentation.</p>
51 */
52public final class CameraManager {
53
54    private static final String TAG = "CameraManager";
55    private final boolean DEBUG;
56
57    /**
58     * This should match the ICameraService definition
59     */
60    private static final String CAMERA_SERVICE_BINDER_NAME = "media.camera";
61    private static final int USE_CALLING_UID = -1;
62
63    @SuppressWarnings("unused")
64    private static final int API_VERSION_1 = 1;
65    private static final int API_VERSION_2 = 2;
66
67    // Access only through getCameraServiceLocked to deal with binder death
68    private ICameraService mCameraService;
69
70    private ArrayList<String> mDeviceIdList;
71
72    private final ArrayMap<AvailabilityListener, Handler> mListenerMap =
73            new ArrayMap<AvailabilityListener, Handler>();
74
75    private final Context mContext;
76    private final Object mLock = new Object();
77
78    private final CameraServiceListener mServiceListener = new CameraServiceListener();
79
80    /**
81     * @hide
82     */
83    public CameraManager(Context context) {
84        DEBUG = Log.isLoggable(TAG, Log.DEBUG);
85        synchronized(mLock) {
86            mContext = context;
87
88            connectCameraServiceLocked();
89        }
90    }
91
92    /**
93     * Return the list of currently connected camera devices by
94     * identifier.
95     *
96     * <p>Non-removable cameras use integers starting at 0 for their
97     * identifiers, while removable cameras have a unique identifier for each
98     * individual device, even if they are the same model.</p>
99     *
100     * @return The list of currently connected camera devices.
101     */
102    public String[] getCameraIdList() throws CameraAccessException {
103        synchronized (mLock) {
104            // ID list creation handles various known failures in device enumeration, so only
105            // exceptions it'll throw are unexpected, and should be propagated upward.
106            return getOrCreateDeviceIdListLocked().toArray(new String[0]);
107        }
108    }
109
110    /**
111     * Register a listener to be notified about camera device availability.
112     *
113     * <p>Registering the same listener again will replace the handler with the
114     * new one provided.</p>
115     *
116     * <p>The first time a listener is registered, it is immediately called
117     * with the availability status of all currently known camera devices.</p>
118     *
119     * @param listener The new listener to send camera availability notices to
120     * @param handler The handler on which the listener should be invoked, or
121     * {@code null} to use the current thread's {@link android.os.Looper looper}.
122     */
123    public void addAvailabilityListener(AvailabilityListener listener, Handler handler) {
124        if (handler == null) {
125            Looper looper = Looper.myLooper();
126            if (looper == null) {
127                throw new IllegalArgumentException(
128                        "No handler given, and current thread has no looper!");
129            }
130            handler = new Handler(looper);
131        }
132
133        synchronized (mLock) {
134            Handler oldHandler = mListenerMap.put(listener, handler);
135            // For new listeners, provide initial availability information
136            if (oldHandler == null) {
137                mServiceListener.updateListenerLocked(listener, handler);
138            }
139        }
140    }
141
142    /**
143     * Remove a previously-added listener; the listener will no longer receive
144     * connection and disconnection callbacks.
145     *
146     * <p>Removing a listener that isn't registered has no effect.</p>
147     *
148     * @param listener The listener to remove from the notification list
149     */
150    public void removeAvailabilityListener(AvailabilityListener listener) {
151        synchronized (mLock) {
152            mListenerMap.remove(listener);
153        }
154    }
155
156    /**
157     * <p>Query the capabilities of a camera device. These capabilities are
158     * immutable for a given camera.</p>
159     *
160     * @param cameraId The id of the camera device to query
161     * @return The properties of the given camera
162     *
163     * @throws IllegalArgumentException if the cameraId does not match any
164     *         known camera device.
165     * @throws CameraAccessException if the camera is disabled by device policy, or
166     *         the camera device has been disconnected.
167     * @throws SecurityException if the application does not have permission to
168     *         access the camera
169     *
170     * @see #getCameraIdList
171     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
172     */
173    public CameraCharacteristics getCameraCharacteristics(String cameraId)
174            throws CameraAccessException {
175        CameraCharacteristics characteristics = null;
176
177        synchronized (mLock) {
178            if (!getOrCreateDeviceIdListLocked().contains(cameraId)) {
179                throw new IllegalArgumentException(String.format("Camera id %s does not match any" +
180                        " currently connected camera device", cameraId));
181            }
182
183            int id = Integer.valueOf(cameraId);
184
185            /*
186             * Get the camera characteristics from the camera service directly if it supports it,
187             * otherwise get them from the legacy shim instead.
188             */
189
190            ICameraService cameraService = getCameraServiceLocked();
191            if (cameraService == null) {
192                throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
193                        "Camera service is currently unavailable");
194            }
195            try {
196                if (!supportsCamera2ApiLocked(cameraId)) {
197                    // Legacy backwards compatibility path; build static info from the camera
198                    // parameters
199                    String[] outParameters = new String[1];
200
201                    cameraService.getLegacyParameters(id, /*out*/outParameters);
202                    String parameters = outParameters[0];
203
204                    CameraInfo info = new CameraInfo();
205                    cameraService.getCameraInfo(id, /*out*/info);
206
207                    characteristics = LegacyMetadataMapper.createCharacteristics(parameters, info);
208                } else {
209                    // Normal path: Get the camera characteristics directly from the camera service
210                    CameraMetadataNative info = new CameraMetadataNative();
211
212                    cameraService.getCameraCharacteristics(id, info);
213
214                    characteristics = new CameraCharacteristics(info);
215                }
216            } catch (CameraRuntimeException e) {
217                throw e.asChecked();
218            } catch (RemoteException e) {
219                // Camera service died - act as if the camera was disconnected
220                throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
221                        "Camera service is currently unavailable", e);
222            }
223        }
224        return characteristics;
225    }
226
227    /**
228     * Helper for openning a connection to a camera with the given ID.
229     *
230     * @param cameraId The unique identifier of the camera device to open
231     * @param listener The listener for the camera. Must not be null.
232     * @param handler  The handler to call the listener on. Must not be null.
233     *
234     * @throws CameraAccessException if the camera is disabled by device policy,
235     * or too many camera devices are already open, or the cameraId does not match
236     * any currently available camera device.
237     *
238     * @throws SecurityException if the application does not have permission to
239     * access the camera
240     * @throws IllegalArgumentException if listener or handler is null.
241     * @return A handle to the newly-created camera device.
242     *
243     * @see #getCameraIdList
244     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
245     */
246    private CameraDevice openCameraDeviceUserAsync(String cameraId,
247            CameraDevice.StateListener listener, Handler handler)
248            throws CameraAccessException {
249        CameraCharacteristics characteristics = getCameraCharacteristics(cameraId);
250        CameraDevice device = null;
251        try {
252
253            synchronized (mLock) {
254
255                ICameraDeviceUser cameraUser = null;
256
257                android.hardware.camera2.impl.CameraDeviceImpl deviceImpl =
258                        new android.hardware.camera2.impl.CameraDeviceImpl(
259                                cameraId,
260                                listener,
261                                handler,
262                                characteristics);
263
264                BinderHolder holder = new BinderHolder();
265
266                ICameraDeviceCallbacks callbacks = deviceImpl.getCallbacks();
267                int id = Integer.parseInt(cameraId);
268                try {
269                    if (supportsCamera2ApiLocked(cameraId)) {
270                        // Use cameraservice's cameradeviceclient implementation for HAL3.2+ devices
271                        ICameraService cameraService = getCameraServiceLocked();
272                        if (cameraService == null) {
273                            throw new CameraRuntimeException(
274                                CameraAccessException.CAMERA_DISCONNECTED,
275                                "Camera service is currently unavailable");
276                        }
277                        cameraService.connectDevice(callbacks, id,
278                                mContext.getPackageName(), USE_CALLING_UID, holder);
279                        cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder());
280                    } else {
281                        // Use legacy camera implementation for HAL1 devices
282                        Log.i(TAG, "Using legacy camera HAL.");
283                        cameraUser = CameraDeviceUserShim.connectBinderShim(callbacks, id);
284                    }
285                } catch (CameraRuntimeException e) {
286                    if (e.getReason() == CameraAccessException.CAMERA_DEPRECATED_HAL) {
287                        throw new AssertionError("Should've gone down the shim path");
288                    } else if (e.getReason() == CameraAccessException.CAMERA_IN_USE ||
289                            e.getReason() == CameraAccessException.MAX_CAMERAS_IN_USE ||
290                            e.getReason() == CameraAccessException.CAMERA_DISABLED ||
291                            e.getReason() == CameraAccessException.CAMERA_DISCONNECTED ||
292                            e.getReason() == CameraAccessException.CAMERA_ERROR) {
293                        // Received one of the known connection errors
294                        // The remote camera device cannot be connected to, so
295                        // set the local camera to the startup error state
296                        deviceImpl.setRemoteFailure(e);
297
298                        if (e.getReason() == CameraAccessException.CAMERA_DISABLED ||
299                                e.getReason() == CameraAccessException.CAMERA_DISCONNECTED) {
300                            // Per API docs, these failures call onError and throw
301                            throw e.asChecked();
302                        }
303                    } else {
304                        // Unexpected failure - rethrow
305                        throw e;
306                    }
307                } catch (RemoteException e) {
308                    // Camera service died - act as if it's a CAMERA_DISCONNECTED case
309                    CameraRuntimeException ce = new CameraRuntimeException(
310                        CameraAccessException.CAMERA_DISCONNECTED,
311                        "Camera service is currently unavailable", e);
312                    deviceImpl.setRemoteFailure(ce);
313                    throw ce.asChecked();
314                }
315
316                // TODO: factor out listener to be non-nested, then move setter to constructor
317                // For now, calling setRemoteDevice will fire initial
318                // onOpened/onUnconfigured callbacks.
319                deviceImpl.setRemoteDevice(cameraUser);
320                device = deviceImpl;
321            }
322
323        } catch (NumberFormatException e) {
324            throw new IllegalArgumentException("Expected cameraId to be numeric, but it was: "
325                    + cameraId);
326        } catch (CameraRuntimeException e) {
327            throw e.asChecked();
328        }
329        return device;
330    }
331
332    /**
333     * Open a connection to a camera with the given ID.
334     *
335     * <p>Use {@link #getCameraIdList} to get the list of available camera
336     * devices. Note that even if an id is listed, open may fail if the device
337     * is disconnected between the calls to {@link #getCameraIdList} and
338     * {@link #openCamera}.</p>
339     *
340     * <p>Once the camera is successfully opened, {@link CameraDevice.StateListener#onOpened} will
341     * be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up
342     * for operation by calling {@link CameraDevice#createCaptureSession} and
343     * {@link CameraDevice#createCaptureRequest}</p>
344     *
345     * <!--
346     * <p>Since the camera device will be opened asynchronously, any asynchronous operations done
347     * on the returned CameraDevice instance will be queued up until the device startup has
348     * completed and the listener's {@link CameraDevice.StateListener#onOpened onOpened} method is
349     * called. The pending operations are then processed in order.</p>
350     * -->
351     * <p>If the camera becomes disconnected during initialization
352     * after this function call returns,
353     * {@link CameraDevice.StateListener#onDisconnected} with a
354     * {@link CameraDevice} in the disconnected state (and
355     * {@link CameraDevice.StateListener#onOpened} will be skipped).</p>
356     *
357     * <p>If opening the camera device fails, then the device listener's
358     * {@link CameraDevice.StateListener#onError onError} method will be called, and subsequent
359     * calls on the camera device will throw a {@link CameraAccessException}.</p>
360     *
361     * @param cameraId
362     *             The unique identifier of the camera device to open
363     * @param listener
364     *             The listener which is invoked once the camera is opened
365     * @param handler
366     *             The handler on which the listener should be invoked, or
367     *             {@code null} to use the current thread's {@link android.os.Looper looper}.
368     *
369     * @throws CameraAccessException if the camera is disabled by device policy,
370     * or the camera has become or was disconnected.
371     *
372     * @throws IllegalArgumentException if cameraId or the listener was null,
373     * or the cameraId does not match any currently or previously available
374     * camera device.
375     *
376     * @throws SecurityException if the application does not have permission to
377     * access the camera
378     *
379     * @see #getCameraIdList
380     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
381     */
382    public void openCamera(String cameraId, final CameraDevice.StateListener listener,
383            Handler handler)
384            throws CameraAccessException {
385
386        if (cameraId == null) {
387            throw new IllegalArgumentException("cameraId was null");
388        } else if (listener == null) {
389            throw new IllegalArgumentException("listener was null");
390        } else if (handler == null) {
391            if (Looper.myLooper() != null) {
392                handler = new Handler();
393            } else {
394                throw new IllegalArgumentException(
395                        "Looper doesn't exist in the calling thread");
396            }
397        }
398
399        openCameraDeviceUserAsync(cameraId, listener, handler);
400    }
401
402    /**
403     * A listener for camera devices becoming available or
404     * unavailable to open.
405     *
406     * <p>Cameras become available when they are no longer in use, or when a new
407     * removable camera is connected. They become unavailable when some
408     * application or service starts using a camera, or when a removable camera
409     * is disconnected.</p>
410     *
411     * <p>Extend this listener and pass an instance of the subclass to
412     * {@link CameraManager#addAvailabilityListener} to be notified of such availability
413     * changes.</p>
414     *
415     * @see addAvailabilityListener
416     */
417    public static abstract class AvailabilityListener {
418
419        /**
420         * A new camera has become available to use.
421         *
422         * <p>The default implementation of this method does nothing.</p>
423         *
424         * @param cameraId The unique identifier of the new camera.
425         */
426        public void onCameraAvailable(String cameraId) {
427            // default empty implementation
428        }
429
430        /**
431         * A previously-available camera has become unavailable for use.
432         *
433         * <p>If an application had an active CameraDevice instance for the
434         * now-disconnected camera, that application will receive a
435         * {@link CameraDevice.StateListener#onDisconnected disconnection error}.</p>
436         *
437         * <p>The default implementation of this method does nothing.</p>
438         *
439         * @param cameraId The unique identifier of the disconnected camera.
440         */
441        public void onCameraUnavailable(String cameraId) {
442            // default empty implementation
443        }
444    }
445
446    /**
447     * Return or create the list of currently connected camera devices.
448     *
449     * <p>In case of errors connecting to the camera service, will return an empty list.</p>
450     */
451    private ArrayList<String> getOrCreateDeviceIdListLocked() throws CameraAccessException {
452        if (mDeviceIdList == null) {
453            int numCameras = 0;
454            ICameraService cameraService = getCameraServiceLocked();
455            ArrayList<String> deviceIdList = new ArrayList<>();
456
457            // If no camera service, then no devices
458            if (cameraService == null) {
459                return deviceIdList;
460            }
461
462            try {
463                numCameras = cameraService.getNumberOfCameras();
464            } catch(CameraRuntimeException e) {
465                throw e.asChecked();
466            } catch (RemoteException e) {
467                // camera service just died - if no camera service, then no devices
468                return deviceIdList;
469            }
470
471            CameraMetadataNative info = new CameraMetadataNative();
472            for (int i = 0; i < numCameras; ++i) {
473                // Non-removable cameras use integers starting at 0 for their
474                // identifiers
475                boolean isDeviceSupported = false;
476                try {
477                    cameraService.getCameraCharacteristics(i, info);
478                    if (!info.isEmpty()) {
479                        isDeviceSupported = true;
480                    } else {
481                        throw new AssertionError("Expected to get non-empty characteristics");
482                    }
483                } catch(IllegalArgumentException  e) {
484                    // Got a BAD_VALUE from service, meaning that this
485                    // device is not supported.
486                } catch(CameraRuntimeException e) {
487                    // DISCONNECTED means that the HAL reported an low-level error getting the
488                    // device info; skip listing the device.  Other errors,
489                    // propagate exception onward
490                    if (e.getReason() != CameraAccessException.CAMERA_DISCONNECTED) {
491                        throw e.asChecked();
492                    }
493                } catch(RemoteException e) {
494                    // Camera service died - no devices to list
495                    deviceIdList.clear();
496                    return deviceIdList;
497                }
498
499                if (isDeviceSupported) {
500                    deviceIdList.add(String.valueOf(i));
501                } else {
502                    Log.w(TAG, "Error querying camera device " + i + " for listing.");
503                }
504
505            }
506            mDeviceIdList = deviceIdList;
507        }
508        return mDeviceIdList;
509    }
510
511    private void handleRecoverableSetupErrors(CameraRuntimeException e, String msg) {
512        int problem = e.getReason();
513        switch (problem) {
514            case CameraAccessException.CAMERA_DISCONNECTED:
515                String errorMsg = CameraAccessException.getDefaultMessage(problem);
516                Log.w(TAG, msg + ": " + errorMsg);
517                break;
518            default:
519                throw new IllegalStateException(msg, e.asChecked());
520        }
521    }
522
523    /**
524     * Queries the camera service if it supports the camera2 api directly, or needs a shim.
525     *
526     * @param cameraId a non-{@code null} camera identifier
527     * @return {@code false} if the legacy shim needs to be used, {@code true} otherwise.
528     */
529    private boolean supportsCamera2ApiLocked(String cameraId) {
530        return supportsCameraApiLocked(cameraId, API_VERSION_2);
531    }
532
533    /**
534     * Queries the camera service if it supports a camera api directly, or needs a shim.
535     *
536     * @param cameraId a non-{@code null} camera identifier
537     * @param apiVersion the version, i.e. {@code API_VERSION_1} or {@code API_VERSION_2}
538     * @return {@code true} if connecting will work for that device version.
539     */
540    private boolean supportsCameraApiLocked(String cameraId, int apiVersion) {
541        int id = Integer.parseInt(cameraId);
542
543        /*
544         * Possible return values:
545         * - NO_ERROR => CameraX API is supported
546         * - CAMERA_DEPRECATED_HAL => CameraX API is *not* supported (thrown as an exception)
547         * - Remote exception => If the camera service died
548         *
549         * Anything else is an unexpected error we don't want to recover from.
550         */
551        try {
552            ICameraService cameraService = getCameraServiceLocked();
553            // If no camera service, no support
554            if (cameraService == null) return false;
555
556            int res = cameraService.supportsCameraApi(id, apiVersion);
557
558            if (res != CameraServiceBinderDecorator.NO_ERROR) {
559                throw new AssertionError("Unexpected value " + res);
560            }
561            return true;
562        } catch (CameraRuntimeException e) {
563            if (e.getReason() != CameraAccessException.CAMERA_DEPRECATED_HAL) {
564                throw e;
565            }
566            // API level is not supported
567        } catch (RemoteException e) {
568            // Camera service is now down, no support for any API level
569        }
570        return false;
571    }
572
573    /**
574     * Connect to the camera service if it's available, and set up listeners.
575     *
576     * <p>Sets mCameraService to a valid pointer or null if the connection does not succeed.</p>
577     */
578    private void connectCameraServiceLocked() {
579        mCameraService = null;
580        IBinder cameraServiceBinder = ServiceManager.getService(CAMERA_SERVICE_BINDER_NAME);
581        if (cameraServiceBinder == null) {
582            // Camera service is now down, leave mCameraService as null
583            return;
584        }
585        try {
586            cameraServiceBinder.linkToDeath(new CameraServiceDeathListener(), /*flags*/ 0);
587        } catch (RemoteException e) {
588            // Camera service is now down, leave mCameraService as null
589            return;
590        }
591
592        ICameraService cameraServiceRaw = ICameraService.Stub.asInterface(cameraServiceBinder);
593
594        /**
595         * Wrap the camera service in a decorator which automatically translates return codes
596         * into exceptions.
597         */
598        ICameraService cameraService = CameraServiceBinderDecorator.newInstance(cameraServiceRaw);
599
600        try {
601            CameraServiceBinderDecorator.throwOnError(
602                    CameraMetadataNative.nativeSetupGlobalVendorTagDescriptor());
603        } catch (CameraRuntimeException e) {
604            handleRecoverableSetupErrors(e, "Failed to set up vendor tags");
605        }
606
607        try {
608            cameraService.addListener(mServiceListener);
609            mCameraService = cameraService;
610        } catch(CameraRuntimeException e) {
611            // Unexpected failure
612            throw new IllegalStateException("Failed to register a camera service listener",
613                    e.asChecked());
614        } catch (RemoteException e) {
615            // Camera service is now down, leave mCameraService as null
616        }
617    }
618
619    /**
620     * Return a best-effort ICameraService.
621     *
622     * <p>This will be null if the camera service
623     * is not currently available. If the camera service has died since the last
624     * use of the camera service, will try to reconnect to the service.</p>
625     */
626    private ICameraService getCameraServiceLocked() {
627        if (mCameraService == null) {
628            Log.i(TAG, "getCameraServiceLocked: Reconnecting to camera service");
629            connectCameraServiceLocked();
630            if (mCameraService == null) {
631                Log.e(TAG, "Camera service is unavailable");
632            }
633        }
634        return mCameraService;
635    }
636
637    /**
638     * Listener for camera service death.
639     *
640     * <p>The camera service isn't supposed to die under any normal circumstances, but can be turned
641     * off during debug, or crash due to bugs.  So detect that and null out the interface object, so
642     * that the next calls to the manager can try to reconnect.</p>
643     */
644    private class CameraServiceDeathListener implements IBinder.DeathRecipient {
645        public void binderDied() {
646            synchronized(mLock) {
647                mCameraService = null;
648                // Tell listeners that the cameras are _available_, because any existing clients
649                // will have gotten disconnected. This is optimistic under the assumption that the
650                // service will be back shortly.
651                //
652                // Without this, a camera service crash while a camera is open will never signal to
653                // listeners that previously in-use cameras are now available.
654                for (String cameraId : mDeviceIdList) {
655                    mServiceListener.onStatusChangedLocked(CameraServiceListener.STATUS_PRESENT,
656                            cameraId);
657                }
658            }
659        }
660    }
661
662    // TODO: this class needs unit tests
663    // TODO: extract class into top level
664    private class CameraServiceListener extends ICameraServiceListener.Stub {
665
666        // Keep up-to-date with ICameraServiceListener.h
667
668        // Device physically unplugged
669        public static final int STATUS_NOT_PRESENT = 0;
670        // Device physically has been plugged in
671        // and the camera can be used exclusively
672        public static final int STATUS_PRESENT = 1;
673        // Device physically has been plugged in
674        // but it will not be connect-able until enumeration is complete
675        public static final int STATUS_ENUMERATING = 2;
676        // Camera is in use by another app and cannot be used exclusively
677        public static final int STATUS_NOT_AVAILABLE = 0x80000000;
678
679        // Camera ID -> Status map
680        private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();
681
682        private static final String TAG = "CameraServiceListener";
683
684        @Override
685        public IBinder asBinder() {
686            return this;
687        }
688
689        private boolean isAvailable(int status) {
690            switch (status) {
691                case STATUS_PRESENT:
692                    return true;
693                default:
694                    return false;
695            }
696        }
697
698        private boolean validStatus(int status) {
699            switch (status) {
700                case STATUS_NOT_PRESENT:
701                case STATUS_PRESENT:
702                case STATUS_ENUMERATING:
703                case STATUS_NOT_AVAILABLE:
704                    return true;
705                default:
706                    return false;
707            }
708        }
709
710        private void postSingleUpdate(final AvailabilityListener listener, final Handler handler,
711                final String id, final int status) {
712            if (isAvailable(status)) {
713                handler.post(
714                    new Runnable() {
715                        @Override
716                        public void run() {
717                            listener.onCameraAvailable(id);
718                        }
719                    });
720            } else {
721                handler.post(
722                    new Runnable() {
723                        @Override
724                        public void run() {
725                            listener.onCameraUnavailable(id);
726                        }
727                    });
728            }
729        }
730
731        /**
732         * Send the state of all known cameras to the provided listener, to initialize
733         * the listener's knowledge of camera state.
734         */
735        public void updateListenerLocked(AvailabilityListener listener, Handler handler) {
736            for (int i = 0; i < mDeviceStatus.size(); i++) {
737                String id = mDeviceStatus.keyAt(i);
738                Integer status = mDeviceStatus.valueAt(i);
739                postSingleUpdate(listener, handler, id, status);
740            }
741        }
742
743        @Override
744        public void onStatusChanged(int status, int cameraId) throws RemoteException {
745            synchronized(CameraManager.this.mLock) {
746                onStatusChangedLocked(status, String.valueOf(cameraId));
747            }
748        }
749
750        public void onStatusChangedLocked(int status, String id) {
751            if (DEBUG) {
752                Log.v(TAG,
753                        String.format("Camera id %s has status changed to 0x%x", id, status));
754            }
755
756            if (!validStatus(status)) {
757                Log.e(TAG, String.format("Ignoring invalid device %s status 0x%x", id,
758                                status));
759                return;
760            }
761
762            Integer oldStatus = mDeviceStatus.put(id, status);
763
764            if (oldStatus != null && oldStatus == status) {
765                if (DEBUG) {
766                    Log.v(TAG, String.format(
767                        "Device status changed to 0x%x, which is what it already was",
768                        status));
769                }
770                return;
771            }
772
773            // TODO: consider abstracting out this state minimization + transition
774            // into a separate
775            // more easily testable class
776            // i.e. (new State()).addState(STATE_AVAILABLE)
777            //                   .addState(STATE_NOT_AVAILABLE)
778            //                   .addTransition(STATUS_PRESENT, STATE_AVAILABLE),
779            //                   .addTransition(STATUS_NOT_PRESENT, STATE_NOT_AVAILABLE)
780            //                   .addTransition(STATUS_ENUMERATING, STATE_NOT_AVAILABLE);
781            //                   .addTransition(STATUS_NOT_AVAILABLE, STATE_NOT_AVAILABLE);
782
783            // Translate all the statuses to either 'available' or 'not available'
784            //  available -> available         => no new update
785            //  not available -> not available => no new update
786            if (oldStatus != null && isAvailable(status) == isAvailable(oldStatus)) {
787                if (DEBUG) {
788                    Log.v(TAG,
789                            String.format(
790                                "Device status was previously available (%d), " +
791                                " and is now again available (%d)" +
792                                "so no new client visible update will be sent",
793                                isAvailable(status), isAvailable(status)));
794                }
795                return;
796            }
797
798            final int listenerCount = mListenerMap.size();
799            for (int i = 0; i < listenerCount; i++) {
800                Handler handler = mListenerMap.valueAt(i);
801                final AvailabilityListener listener = mListenerMap.keyAt(i);
802
803                postSingleUpdate(listener, handler, id, status);
804            }
805        } // onStatusChangedLocked
806
807    } // CameraServiceListener
808} // CameraManager
809