CameraManager.java revision 2001188be30649198972a3199a4322d6f7f5f93d
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.IProCameraUser;
23import android.hardware.camera2.impl.CameraMetadataNative;
24import android.hardware.camera2.utils.CameraBinderDecorator;
25import android.hardware.camera2.utils.CameraRuntimeException;
26import android.hardware.camera2.utils.BinderHolder;
27import android.os.IBinder;
28import android.os.Handler;
29import android.os.Looper;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.util.Log;
33import android.util.ArrayMap;
34
35import java.util.ArrayList;
36
37/**
38 * <p>An interface for iterating, listing, and connecting to
39 * {@link CameraDevice CameraDevices}.</p>
40 *
41 * <p>You can get an instance of this class by calling
42 * {@link android.content.Context#getSystemService(String) Context.getSystemService()}.</p>
43 *
44 * <pre>CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);</pre>
45 *
46 * <p>For more details about communicating with camera devices, read the Camera
47 * developer guide or the {@link android.hardware.camera2 camera2}
48 * package documentation.</p>
49 */
50public final class CameraManager {
51
52    /**
53     * This should match the ICameraService definition
54     */
55    private static final String CAMERA_SERVICE_BINDER_NAME = "media.camera";
56    private static final int USE_CALLING_UID = -1;
57
58    private final ICameraService mCameraService;
59    private ArrayList<String> mDeviceIdList;
60
61    private final ArrayMap<AvailabilityListener, Handler> mListenerMap =
62            new ArrayMap<AvailabilityListener, Handler>();
63
64    private final Context mContext;
65    private final Object mLock = new Object();
66
67    /**
68     * @hide
69     */
70    public CameraManager(Context context) {
71        mContext = context;
72
73        IBinder cameraServiceBinder = ServiceManager.getService(CAMERA_SERVICE_BINDER_NAME);
74        ICameraService cameraServiceRaw = ICameraService.Stub.asInterface(cameraServiceBinder);
75
76        /**
77         * Wrap the camera service in a decorator which automatically translates return codes
78         * into exceptions, and RemoteExceptions into other exceptions.
79         */
80        mCameraService = CameraBinderDecorator.newInstance(cameraServiceRaw);
81
82        try {
83            mCameraService.addListener(new CameraServiceListener());
84        } catch(CameraRuntimeException e) {
85            throw new IllegalStateException("Failed to register a camera service listener",
86                    e.asChecked());
87        } catch (RemoteException e) {
88            // impossible
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            try {
105                return getOrCreateDeviceIdListLocked().toArray(new String[0]);
106            } catch(CameraAccessException e) {
107                // this should almost never happen, except if mediaserver crashes
108                throw new IllegalStateException(
109                        "Failed to query camera service for device ID list", e);
110            }
111        }
112    }
113
114    /**
115     * Register a listener to be notified about camera device availability.
116     *
117     * <p>Registering the same listener again will replace the handler with the
118     * new one provided.</p>
119     *
120     * @param listener The new listener to send camera availability notices to
121     * @param handler The handler on which the listener should be invoked, or
122     * {@code null} to use the current thread's {@link android.os.Looper looper}.
123     */
124    public void addAvailabilityListener(AvailabilityListener listener, Handler handler) {
125        if (handler == null) {
126            Looper looper = Looper.myLooper();
127            if (looper == null) {
128                throw new IllegalArgumentException(
129                        "No handler given, and current thread has no looper!");
130            }
131            handler = new Handler(looper);
132        }
133
134        synchronized (mLock) {
135            mListenerMap.put(listener, handler);
136        }
137    }
138
139    /**
140     * Remove a previously-added listener; the listener will no longer receive
141     * connection and disconnection callbacks.
142     *
143     * <p>Removing a listener that isn't registered has no effect.</p>
144     *
145     * @param listener The listener to remove from the notification list
146     */
147    public void removeAvailabilityListener(AvailabilityListener listener) {
148        synchronized (mLock) {
149            mListenerMap.remove(listener);
150        }
151    }
152
153    /**
154     * <p>Query the capabilities of a camera device. These capabilities are
155     * immutable for a given camera.</p>
156     *
157     * @param cameraId The id of the camera device to query
158     * @return The properties of the given camera
159     *
160     * @throws IllegalArgumentException if the cameraId does not match any
161     * currently connected camera device.
162     * @throws CameraAccessException if the camera is disabled by device policy.
163     * @throws SecurityException if the application does not have permission to
164     * access the camera
165     *
166     * @see #getCameraIdList
167     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
168     */
169    public CameraCharacteristics getCameraCharacteristics(String cameraId)
170            throws CameraAccessException {
171
172        synchronized (mLock) {
173            if (!getOrCreateDeviceIdListLocked().contains(cameraId)) {
174                throw new IllegalArgumentException(String.format("Camera id %s does not match any" +
175                        " currently connected camera device", cameraId));
176            }
177        }
178
179        CameraMetadataNative info = new CameraMetadataNative();
180        try {
181            mCameraService.getCameraCharacteristics(Integer.valueOf(cameraId), info);
182        } catch(CameraRuntimeException e) {
183            throw e.asChecked();
184        } catch(RemoteException e) {
185            // impossible
186            return null;
187        }
188
189        return new CameraCharacteristics(info);
190    }
191
192    /**
193     * Open a connection to a camera with the given ID. Use
194     * {@link #getCameraIdList} to get the list of available camera
195     * devices. Note that even if an id is listed, open may fail if the device
196     * is disconnected between the calls to {@link #getCameraIdList} and
197     * {@link #openCamera}.
198     *
199     * @param cameraId The unique identifier of the camera device to open
200     *
201     * @throws CameraAccessException if the camera is disabled by device policy,
202     * or too many camera devices are already open, or the cameraId does not match
203     * any currently available camera device.
204     *
205     * @throws SecurityException if the application does not have permission to
206     * access the camera
207     *
208     * @see #getCameraIdList
209     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
210     */
211    private CameraDevice openCamera(String cameraId) throws CameraAccessException {
212        try {
213
214            synchronized (mLock) {
215
216                ICameraDeviceUser cameraUser;
217
218                android.hardware.camera2.impl.CameraDevice device =
219                        new android.hardware.camera2.impl.CameraDevice(cameraId);
220
221                BinderHolder holder = new BinderHolder();
222                mCameraService.connectDevice(device.getCallbacks(),
223                        Integer.parseInt(cameraId),
224                        mContext.getPackageName(), USE_CALLING_UID, holder);
225                cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder());
226
227                // TODO: factor out listener to be non-nested, then move setter to constructor
228                device.setRemoteDevice(cameraUser);
229
230                return device;
231
232            }
233
234        } catch (NumberFormatException e) {
235            throw new IllegalArgumentException("Expected cameraId to be numeric, but it was: "
236                    + cameraId);
237        } catch (CameraRuntimeException e) {
238            throw e.asChecked();
239        } catch (RemoteException e) {
240            // impossible
241            return null;
242        }
243    }
244
245    /**
246     * Open a connection to a camera with the given ID.
247     *
248     * <p>Use {@link #getCameraIdList} to get the list of available camera
249     * devices. Note that even if an id is listed, open may fail if the device
250     * is disconnected between the calls to {@link #getCameraIdList} and
251     * {@link #openCamera}.</p>
252     *
253     * <p>If the camera successfully opens after this function call returns,
254     * {@link CameraDevice.StateListener#onOpened} will be invoked with the
255     * newly opened {@link CameraDevice} in the unconfigured state.</p>
256     *
257     * <p>If the camera becomes disconnected during initialization
258     * after this function call returns,
259     * {@link CameraDevice.StateListener#onDisconnected} with a
260     * {@link CameraDevice} in the disconnected state (and
261     * {@link CameraDevice.StateListener#onOpened} will be skipped).</p>
262     *
263     * <p>If the camera fails to initialize after this function call returns,
264     * {@link CameraDevice.StateListener#onError} will be invoked with a
265     * {@link CameraDevice} in the error state (and
266     * {@link CameraDevice.StateListener#onOpened} will be skipped).</p>
267     *
268     * @param cameraId
269     *             The unique identifier of the camera device to open
270     * @param listener
271     *             The listener which is invoked once the camera is opened
272     * @param handler
273     *             The handler on which the listener should be invoked, or
274     *             {@code null} to use the current thread's {@link android.os.Looper looper}.
275     *
276     * @throws CameraAccessException if the camera is disabled by device policy,
277     * or the camera has become or was disconnected.
278     *
279     * @throws IllegalArgumentException if cameraId or the listener was null,
280     * or the cameraId does not match any currently or previously available
281     * camera device.
282     *
283     * @throws SecurityException if the application does not have permission to
284     * access the camera
285     *
286     * @see #getCameraIdList
287     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
288     */
289    public void openCamera(String cameraId, final CameraDevice.StateListener listener,
290            Handler handler)
291            throws CameraAccessException {
292
293        if (cameraId == null) {
294            throw new IllegalArgumentException("cameraId was null");
295        } else if (listener == null) {
296            throw new IllegalArgumentException("listener was null");
297        } else if (handler == null) {
298            if (Looper.myLooper() != null) {
299                handler = new Handler();
300            } else {
301                throw new IllegalArgumentException(
302                        "Looper doesn't exist in the calling thread");
303            }
304        }
305
306        final CameraDevice camera = openCamera(cameraId);
307        camera.setDeviceListener(listener, handler);
308
309        // TODO: make truly async in the camera service
310        handler.post(new Runnable() {
311            @Override
312            public void run() {
313                listener.onOpened(camera);
314            }
315        });
316    }
317
318    /**
319     * Interface for listening to camera devices becoming available or
320     * unavailable.
321     *
322     * <p>Cameras become available when they are no longer in use, or when a new
323     * removable camera is connected. They become unavailable when some
324     * application or service starts using a camera, or when a removable camera
325     * is disconnected.</p>
326     *
327     * @see addAvailabilityListener
328     */
329    public static abstract class AvailabilityListener {
330
331        /**
332         * A new camera has become available to use.
333         *
334         * <p>The default implementation of this method does nothing.</p>
335         *
336         * @param cameraId The unique identifier of the new camera.
337         */
338        public void onCameraAvailable(String cameraId) {
339            // default empty implementation
340        }
341
342        /**
343         * A previously-available camera has become unavailable for use.
344         *
345         * <p>If an application had an active CameraDevice instance for the
346         * now-disconnected camera, that application will receive a
347         * {@link CameraDevice.StateListener#onDisconnected disconnection error}.</p>
348         *
349         * <p>The default implementation of this method does nothing.</p>
350         *
351         * @param cameraId The unique identifier of the disconnected camera.
352         */
353        public void onCameraUnavailable(String cameraId) {
354            // default empty implementation
355        }
356    }
357
358    private ArrayList<String> getOrCreateDeviceIdListLocked() throws CameraAccessException {
359        if (mDeviceIdList == null) {
360            int numCameras = 0;
361
362            try {
363                numCameras = mCameraService.getNumberOfCameras();
364            } catch(CameraRuntimeException e) {
365                throw e.asChecked();
366            } catch (RemoteException e) {
367                // impossible
368                return null;
369            }
370
371            mDeviceIdList = new ArrayList<String>();
372            for (int i = 0; i < numCameras; ++i) {
373                // Non-removable cameras use integers starting at 0 for their
374                // identifiers
375                mDeviceIdList.add(String.valueOf(i));
376            }
377
378        }
379        return mDeviceIdList;
380    }
381
382    // TODO: this class needs unit tests
383    // TODO: extract class into top level
384    private class CameraServiceListener extends ICameraServiceListener.Stub {
385
386        // Keep up-to-date with ICameraServiceListener.h
387
388        // Device physically unplugged
389        public static final int STATUS_NOT_PRESENT = 0;
390        // Device physically has been plugged in
391        // and the camera can be used exclusively
392        public static final int STATUS_PRESENT = 1;
393        // Device physically has been plugged in
394        // but it will not be connect-able until enumeration is complete
395        public static final int STATUS_ENUMERATING = 2;
396        // Camera is in use by another app and cannot be used exclusively
397        public static final int STATUS_NOT_AVAILABLE = 0x80000000;
398
399        // Camera ID -> Status map
400        private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();
401
402        private static final String TAG = "CameraServiceListener";
403
404        @Override
405        public IBinder asBinder() {
406            return this;
407        }
408
409        private boolean isAvailable(int status) {
410            switch (status) {
411                case STATUS_PRESENT:
412                    return true;
413                default:
414                    return false;
415            }
416        }
417
418        private boolean validStatus(int status) {
419            switch (status) {
420                case STATUS_NOT_PRESENT:
421                case STATUS_PRESENT:
422                case STATUS_ENUMERATING:
423                case STATUS_NOT_AVAILABLE:
424                    return true;
425                default:
426                    return false;
427            }
428        }
429
430        @Override
431        public void onStatusChanged(int status, int cameraId) throws RemoteException {
432            synchronized(CameraManager.this.mLock) {
433
434                Log.v(TAG,
435                        String.format("Camera id %d has status changed to 0x%x", cameraId, status));
436
437                final String id = String.valueOf(cameraId);
438
439                if (!validStatus(status)) {
440                    Log.e(TAG, String.format("Ignoring invalid device %d status 0x%x", cameraId,
441                            status));
442                    return;
443                }
444
445                Integer oldStatus = mDeviceStatus.put(id, status);
446
447                if (oldStatus != null && oldStatus == status) {
448                    Log.v(TAG, String.format(
449                            "Device status changed to 0x%x, which is what it already was",
450                            status));
451                    return;
452                }
453
454                // TODO: consider abstracting out this state minimization + transition
455                // into a separate
456                // more easily testable class
457                // i.e. (new State()).addState(STATE_AVAILABLE)
458                //                   .addState(STATE_NOT_AVAILABLE)
459                //                   .addTransition(STATUS_PRESENT, STATE_AVAILABLE),
460                //                   .addTransition(STATUS_NOT_PRESENT, STATE_NOT_AVAILABLE)
461                //                   .addTransition(STATUS_ENUMERATING, STATE_NOT_AVAILABLE);
462                //                   .addTransition(STATUS_NOT_AVAILABLE, STATE_NOT_AVAILABLE);
463
464                // Translate all the statuses to either 'available' or 'not available'
465                //  available -> available         => no new update
466                //  not available -> not available => no new update
467                if (oldStatus != null && isAvailable(status) == isAvailable(oldStatus)) {
468
469                    Log.v(TAG,
470                            String.format(
471                                    "Device status was previously available (%d), " +
472                                            " and is now again available (%d)" +
473                                            "so no new client visible update will be sent",
474                                    isAvailable(status), isAvailable(status)));
475                    return;
476                }
477
478                final int listenerCount = mListenerMap.size();
479                for (int i = 0; i < listenerCount; i++) {
480                    Handler handler = mListenerMap.valueAt(i);
481                    final AvailabilityListener listener = mListenerMap.keyAt(i);
482                    if (isAvailable(status)) {
483                        handler.post(
484                            new Runnable() {
485                                @Override
486                                public void run() {
487                                    listener.onCameraAvailable(id);
488                                }
489                            });
490                    } else {
491                        handler.post(
492                            new Runnable() {
493                                @Override
494                                public void run() {
495                                    listener.onCameraUnavailable(id);
496                                }
497                            });
498                    }
499                } // for
500            } // synchronized
501        } // onStatusChanged
502    } // CameraServiceListener
503} // CameraManager
504