CameraManager.java revision 68f40066c914aefc1f88819dd46dd1135fb9f5bc
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        // TODO: implement and call a service function to get the capabilities on C++ side
180
181        // TODO: get properties from service
182        return new CameraCharacteristics(new CameraMetadataNative());
183    }
184
185    /**
186     * Open a connection to a camera with the given ID. Use
187     * {@link #getCameraIdList} to get the list of available camera
188     * devices. Note that even if an id is listed, open may fail if the device
189     * is disconnected between the calls to {@link #getCameraIdList} and
190     * {@link #openCamera}.
191     *
192     * @param cameraId The unique identifier of the camera device to open
193     *
194     * @throws CameraAccessException if the camera is disabled by device policy,
195     * or too many camera devices are already open, or the cameraId does not match
196     * any currently available camera device.
197     *
198     * @throws SecurityException if the application does not have permission to
199     * access the camera
200     *
201     * @see #getCameraIdList
202     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
203     */
204    private CameraDevice openCamera(String cameraId) throws CameraAccessException {
205        try {
206
207            synchronized (mLock) {
208
209                ICameraDeviceUser cameraUser;
210
211                android.hardware.camera2.impl.CameraDevice device =
212                        new android.hardware.camera2.impl.CameraDevice(cameraId);
213
214                BinderHolder holder = new BinderHolder();
215                mCameraService.connectDevice(device.getCallbacks(),
216                        Integer.parseInt(cameraId),
217                        mContext.getPackageName(), USE_CALLING_UID, holder);
218                cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder());
219
220                // TODO: factor out listener to be non-nested, then move setter to constructor
221                device.setRemoteDevice(cameraUser);
222
223                return device;
224
225            }
226
227        } catch (NumberFormatException e) {
228            throw new IllegalArgumentException("Expected cameraId to be numeric, but it was: "
229                    + cameraId);
230        } catch (CameraRuntimeException e) {
231            throw e.asChecked();
232        } catch (RemoteException e) {
233            // impossible
234            return null;
235        }
236    }
237
238    /**
239     * Open a connection to a camera with the given ID.
240     *
241     * <p>Use {@link #getCameraIdList} to get the list of available camera
242     * devices. Note that even if an id is listed, open may fail if the device
243     * is disconnected between the calls to {@link #getCameraIdList} and
244     * {@link #openCamera}.</p>
245     *
246     * <p>If the camera successfully opens after this function call returns,
247     * {@link CameraDevice.StateListener#onOpened} will be invoked with the
248     * newly opened {@link CameraDevice} in the unconfigured state.</p>
249     *
250     * <p>If the camera becomes disconnected during initialization
251     * after this function call returns,
252     * {@link CameraDevice.StateListener#onDisconnected} with a
253     * {@link CameraDevice} in the disconnected state (and
254     * {@link CameraDevice.StateListener#onOpened} will be skipped).</p>
255     *
256     * <p>If the camera fails to initialize after this function call returns,
257     * {@link CameraDevice.StateListener#onError} will be invoked with a
258     * {@link CameraDevice} in the error state (and
259     * {@link CameraDevice.StateListener#onOpened} will be skipped).</p>
260     *
261     * @param cameraId
262     *             The unique identifier of the camera device to open
263     * @param listener
264     *             The listener which is invoked once the camera is opened
265     * @param handler
266     *             The handler on which the listener should be invoked, or
267     *             {@code null} to use the current thread's {@link android.os.Looper looper}.
268     *
269     * @throws CameraAccessException if the camera is disabled by device policy,
270     * or the camera has become or was disconnected.
271     *
272     * @throws IllegalArgumentException if cameraId or the listener was null,
273     * or the cameraId does not match any currently or previously available
274     * camera device.
275     *
276     * @throws SecurityException if the application does not have permission to
277     * access the camera
278     *
279     * @see #getCameraIdList
280     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
281     */
282    public void openCamera(String cameraId, final CameraDevice.StateListener listener,
283            Handler handler)
284            throws CameraAccessException {
285
286        if (cameraId == null) {
287            throw new IllegalArgumentException("cameraId was null");
288        } else if (listener == null) {
289            throw new IllegalArgumentException("listener was null");
290        } else if (handler == null) {
291            if (Looper.myLooper() != null) {
292                handler = new Handler();
293            } else {
294                throw new IllegalArgumentException(
295                        "Looper doesn't exist in the calling thread");
296            }
297        }
298
299        final CameraDevice camera = openCamera(cameraId);
300        camera.setDeviceListener(listener, handler);
301
302        // TODO: make truly async in the camera service
303        handler.post(new Runnable() {
304            @Override
305            public void run() {
306                listener.onOpened(camera);
307            }
308        });
309    }
310
311    /**
312     * Interface for listening to camera devices becoming available or
313     * unavailable.
314     *
315     * <p>Cameras become available when they are no longer in use, or when a new
316     * removable camera is connected. They become unavailable when some
317     * application or service starts using a camera, or when a removable camera
318     * is disconnected.</p>
319     *
320     * @see addAvailabilityListener
321     */
322    public static abstract class AvailabilityListener {
323
324        /**
325         * A new camera has become available to use.
326         *
327         * <p>The default implementation of this method does nothing.</p>
328         *
329         * @param cameraId The unique identifier of the new camera.
330         */
331        public void onCameraAvailable(String cameraId) {
332            // default empty implementation
333        }
334
335        /**
336         * A previously-available camera has become unavailable for use.
337         *
338         * <p>If an application had an active CameraDevice instance for the
339         * now-disconnected camera, that application will receive a
340         * {@link CameraDevice.StateListener#onDisconnected disconnection error}.</p>
341         *
342         * <p>The default implementation of this method does nothing.</p>
343         *
344         * @param cameraId The unique identifier of the disconnected camera.
345         */
346        public void onCameraUnavailable(String cameraId) {
347            // default empty implementation
348        }
349    }
350
351    private ArrayList<String> getOrCreateDeviceIdListLocked() throws CameraAccessException {
352        if (mDeviceIdList == null) {
353            int numCameras = 0;
354
355            try {
356                numCameras = mCameraService.getNumberOfCameras();
357            } catch(CameraRuntimeException e) {
358                throw e.asChecked();
359            } catch (RemoteException e) {
360                // impossible
361                return null;
362            }
363
364            mDeviceIdList = new ArrayList<String>();
365            for (int i = 0; i < numCameras; ++i) {
366                // Non-removable cameras use integers starting at 0 for their
367                // identifiers
368                mDeviceIdList.add(String.valueOf(i));
369            }
370
371        }
372        return mDeviceIdList;
373    }
374
375    // TODO: this class needs unit tests
376    // TODO: extract class into top level
377    private class CameraServiceListener extends ICameraServiceListener.Stub {
378
379        // Keep up-to-date with ICameraServiceListener.h
380
381        // Device physically unplugged
382        public static final int STATUS_NOT_PRESENT = 0;
383        // Device physically has been plugged in
384        // and the camera can be used exclusively
385        public static final int STATUS_PRESENT = 1;
386        // Device physically has been plugged in
387        // but it will not be connect-able until enumeration is complete
388        public static final int STATUS_ENUMERATING = 2;
389        // Camera is in use by another app and cannot be used exclusively
390        public static final int STATUS_NOT_AVAILABLE = 0x80000000;
391
392        // Camera ID -> Status map
393        private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();
394
395        private static final String TAG = "CameraServiceListener";
396
397        @Override
398        public IBinder asBinder() {
399            return this;
400        }
401
402        private boolean isAvailable(int status) {
403            switch (status) {
404                case STATUS_PRESENT:
405                    return true;
406                default:
407                    return false;
408            }
409        }
410
411        private boolean validStatus(int status) {
412            switch (status) {
413                case STATUS_NOT_PRESENT:
414                case STATUS_PRESENT:
415                case STATUS_ENUMERATING:
416                case STATUS_NOT_AVAILABLE:
417                    return true;
418                default:
419                    return false;
420            }
421        }
422
423        @Override
424        public void onStatusChanged(int status, int cameraId) throws RemoteException {
425            synchronized(CameraManager.this.mLock) {
426
427                Log.v(TAG,
428                        String.format("Camera id %d has status changed to 0x%x", cameraId, status));
429
430                final String id = String.valueOf(cameraId);
431
432                if (!validStatus(status)) {
433                    Log.e(TAG, String.format("Ignoring invalid device %d status 0x%x", cameraId,
434                            status));
435                    return;
436                }
437
438                Integer oldStatus = mDeviceStatus.put(id, status);
439
440                if (oldStatus != null && oldStatus == status) {
441                    Log.v(TAG, String.format(
442                            "Device status changed to 0x%x, which is what it already was",
443                            status));
444                    return;
445                }
446
447                // TODO: consider abstracting out this state minimization + transition
448                // into a separate
449                // more easily testable class
450                // i.e. (new State()).addState(STATE_AVAILABLE)
451                //                   .addState(STATE_NOT_AVAILABLE)
452                //                   .addTransition(STATUS_PRESENT, STATE_AVAILABLE),
453                //                   .addTransition(STATUS_NOT_PRESENT, STATE_NOT_AVAILABLE)
454                //                   .addTransition(STATUS_ENUMERATING, STATE_NOT_AVAILABLE);
455                //                   .addTransition(STATUS_NOT_AVAILABLE, STATE_NOT_AVAILABLE);
456
457                // Translate all the statuses to either 'available' or 'not available'
458                //  available -> available         => no new update
459                //  not available -> not available => no new update
460                if (oldStatus != null && isAvailable(status) == isAvailable(oldStatus)) {
461
462                    Log.v(TAG,
463                            String.format(
464                                    "Device status was previously available (%d), " +
465                                            " and is now again available (%d)" +
466                                            "so no new client visible update will be sent",
467                                    isAvailable(status), isAvailable(status)));
468                    return;
469                }
470
471                final int listenerCount = mListenerMap.size();
472                for (int i = 0; i < listenerCount; i++) {
473                    Handler handler = mListenerMap.valueAt(i);
474                    final AvailabilityListener listener = mListenerMap.keyAt(i);
475                    if (isAvailable(status)) {
476                        handler.post(
477                            new Runnable() {
478                                @Override
479                                public void run() {
480                                    listener.onCameraAvailable(id);
481                                }
482                            });
483                    } else {
484                        handler.post(
485                            new Runnable() {
486                                @Override
487                                public void run() {
488                                    listener.onCameraUnavailable(id);
489                                }
490                            });
491                    }
492                } // for
493            } // synchronized
494        } // onStatusChanged
495    } // CameraServiceListener
496} // CameraManager
497