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