CameraManager.java revision ecb323e3ce94f62411b6799f9a0aa42b052de30d
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.utils.CameraBinderDecorator;
24import android.hardware.camera2.utils.CameraRuntimeException;
25import android.os.IBinder;
26import android.os.RemoteException;
27import android.os.ServiceManager;
28import android.util.Log;
29
30import java.util.ArrayList;
31import java.util.HashMap;
32import java.util.HashSet;
33
34/**
35 * <p>An interface for iterating, listing, and connecting to
36 * {@link CameraDevice CameraDevices}.</p>
37 *
38 * <p>You can get an instance of this class by calling
39 * {@link android.content.Context#getSystemService(String) Context.getSystemService()}.</p>
40 *
41 * <pre>CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);</pre>
42 *
43 * <p>For more details about communicating with camera devices, read the Camera
44 * developer guide or the {@link android.hardware.camera2 camera2}
45 * package documentation.</p>
46 */
47public final class CameraManager {
48
49    /**
50     * This should match the ICameraService definition
51     */
52    private static final String CAMERA_SERVICE_BINDER_NAME = "media.camera";
53    private static final int USE_CALLING_UID = -1;
54
55    private final ICameraService mCameraService;
56    private ArrayList<String> mDeviceIdList;
57    private HashSet<CameraListener> mListenerSet;
58    private final Context mContext;
59    private final Object mLock = new Object();
60
61    /**
62     * @hide
63     */
64    public CameraManager(Context context) {
65        mContext = context;
66
67        IBinder cameraServiceBinder = ServiceManager.getService(CAMERA_SERVICE_BINDER_NAME);
68        ICameraService cameraServiceRaw = ICameraService.Stub.asInterface(cameraServiceBinder);
69
70        /**
71         * Wrap the camera service in a decorator which automatically translates return codes
72         * into exceptions, and RemoteExceptions into other exceptions.
73         */
74        mCameraService = CameraBinderDecorator.newInstance(cameraServiceRaw);
75
76        try {
77            mCameraService.addListener(new CameraServiceListener());
78        } catch(CameraRuntimeException e) {
79            throw new IllegalStateException("Failed to register a camera service listener",
80                    e.asChecked());
81        } catch (RemoteException e) {
82            // impossible
83        }
84    }
85
86    /**
87     * <p>Return the list of currently connected camera devices by
88     * identifier. Non-removable cameras use integers starting at 0 for their
89     * identifiers, while removable cameras have a unique identifier for each
90     * individual device, even if they are the same model.</p>
91     *
92     * @return The list of currently connected camera devices.
93     */
94    public String[] getDeviceIdList() throws CameraAccessException {
95        synchronized (mLock) {
96            try {
97                return getOrCreateDeviceIdListLocked().toArray(new String[0]);
98            } catch(CameraAccessException e) {
99                // this should almost never happen, except if mediaserver crashes
100                throw new IllegalStateException(
101                        "Failed to query camera service for device ID list", e);
102            }
103        }
104    }
105
106    /**
107     * Register a listener to be notified about camera device availability.
108     *
109     * Registering a listener more than once has no effect.
110     *
111     * @param listener the new listener to send camera availability notices to.
112     */
113    public void registerCameraListener(CameraListener listener) {
114        synchronized (mLock) {
115            mListenerSet.add(listener);
116        }
117    }
118
119    /**
120     * Remove a previously-added listener; the listener will no longer receive
121     * connection and disconnection callbacks.
122     *
123     * Removing a listener that isn't registered has no effect.
124     *
125     * @param listener the listener to remove from the notification list
126     */
127    public void unregisterCameraListener(CameraListener listener) {
128        synchronized (mLock) {
129            mListenerSet.remove(listener);
130        }
131    }
132
133    /**
134     * <p>Query the capabilities of a camera device. These capabilities are
135     * immutable for a given camera.</p>
136     *
137     * @param cameraId The id of the camera device to query
138     * @return The properties of the given camera
139     *
140     * @throws IllegalArgumentException if the cameraId does not match any
141     * currently connected camera device.
142     * @throws CameraAccessException if the camera is disabled by device policy.
143     * @throws SecurityException if the application does not have permission to
144     * access the camera
145     *
146     * @see #getDeviceIdList
147     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
148     */
149    public CameraProperties getCameraProperties(String cameraId)
150            throws CameraAccessException {
151
152        synchronized (mLock) {
153            if (!getOrCreateDeviceIdListLocked().contains(cameraId)) {
154                throw new IllegalArgumentException(String.format("Camera id %s does not match any" +
155                        " currently connected camera device", cameraId));
156            }
157        }
158
159        // TODO: implement and call a service function to get the capabilities on C++ side
160
161        // TODO: get properties from service
162        return new CameraProperties();
163    }
164
165    /**
166     * Open a connection to a camera with the given ID. Use
167     * {@link #getDeviceIdList} to get the list of available camera
168     * devices. Note that even if an id is listed, open may fail if the device
169     * is disconnected between the calls to {@link #getDeviceIdList} and
170     * {@link #openCamera}.
171     *
172     * @param cameraId The unique identifier of the camera device to open
173     *
174     * @throws IllegalArgumentException if the cameraId does not match any
175     * currently connected camera device.
176     * @throws CameraAccessException if the camera is disabled by device policy,
177     * or too many camera devices are already open.
178     * @throws SecurityException if the application does not have permission to
179     * access the camera
180     *
181     * @see #getDeviceIdList
182     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
183     */
184    public CameraDevice openCamera(String cameraId) throws CameraAccessException {
185
186        try {
187
188            synchronized (mLock) {
189
190                ICameraDeviceUser cameraUser;
191
192                android.hardware.camera2.impl.CameraDevice device =
193                        new android.hardware.camera2.impl.CameraDevice(cameraId);
194
195                cameraUser = mCameraService.connectDevice(device.getCallbacks(),
196                        Integer.parseInt(cameraId),
197                        mContext.getPackageName(), USE_CALLING_UID);
198
199                // TODO: change ICameraService#connectDevice to return status_t
200                if (cameraUser == null) {
201                    // TEMPORARY CODE.
202                    // catch-all exception since we aren't yet getting the actual error code
203                    throw new IllegalStateException("Failed to open camera device");
204                }
205
206                // TODO: factor out listener to be non-nested, then move setter to constructor
207                device.setRemoteDevice(cameraUser);
208
209                return device;
210
211            }
212
213        } catch (NumberFormatException e) {
214            throw new IllegalArgumentException("Expected cameraId to be numeric, but it was: "
215                    + cameraId);
216        } catch (CameraRuntimeException e) {
217            if (e.getReason() == CameraAccessException.CAMERA_DISCONNECTED) {
218                throw new IllegalArgumentException("Invalid camera ID specified -- " +
219                        "perhaps the camera was physically disconnected", e);
220            } else {
221                throw e.asChecked();
222            }
223        } catch (RemoteException e) {
224            // impossible
225            return null;
226        }
227    }
228
229    /**
230     * Interface for listening to cameras becoming available or unavailable.
231     * Cameras become available when they are no longer in use, or when a new
232     * removable camera is connected. They become unavailable when some
233     * application or service starts using a camera, or when a removable camera
234     * is disconnected.
235     */
236    public interface CameraListener {
237        /**
238         * A new camera has become available to use.
239         *
240         * @param cameraId The unique identifier of the new camera.
241         */
242        public void onCameraAvailable(String cameraId);
243
244        /**
245         * A previously-available camera has become unavailable for use. If an
246         * application had an active CameraDevice instance for the
247         * now-disconnected camera, that application will receive a {@link
248         * CameraDevice.ErrorListener#DEVICE_DISCONNECTED disconnection error}.
249         *
250         * @param cameraId The unique identifier of the disconnected camera.
251         */
252        public void onCameraUnavailable(String cameraId);
253    }
254
255    private ArrayList<String> getOrCreateDeviceIdListLocked() throws CameraAccessException {
256        if (mDeviceIdList == null) {
257            int numCameras = 0;
258
259            try {
260                numCameras = mCameraService.getNumberOfCameras();
261            } catch(CameraRuntimeException e) {
262                throw e.asChecked();
263            } catch (RemoteException e) {
264                // impossible
265                return null;
266            }
267
268            mDeviceIdList = new ArrayList<String>();
269            for (int i = 0; i < numCameras; ++i) {
270                // Non-removable cameras use integers starting at 0 for their
271                // identifiers
272                mDeviceIdList.add(String.valueOf(i));
273            }
274
275        }
276        return mDeviceIdList;
277    }
278
279    // TODO: this class needs unit tests
280    // TODO: extract class into top level
281    private class CameraServiceListener extends ICameraServiceListener.Stub {
282
283        // Keep up-to-date with ICameraServiceListener.h
284
285        // Device physically unplugged
286        public static final int STATUS_NOT_PRESENT = 0;
287        // Device physically has been plugged in
288        // and the camera can be used exclusively
289        public static final int STATUS_PRESENT = 1;
290        // Device physically has been plugged in
291        // but it will not be connect-able until enumeration is complete
292        public static final int STATUS_ENUMERATING = 2;
293        // Camera is in use by another app and cannot be used exclusively
294        public static final int STATUS_NOT_AVAILABLE = 0x80000000;
295
296        // Camera ID -> Status map
297        private final HashMap<String, Integer> mDeviceStatus = new HashMap<String, Integer>();
298
299        private static final String TAG = "CameraServiceListener";
300
301        @Override
302        public IBinder asBinder() {
303            return this;
304        }
305
306        private boolean isAvailable(int status) {
307            switch (status) {
308                case STATUS_PRESENT:
309                    return true;
310                default:
311                    return false;
312            }
313        }
314
315        private boolean validStatus(int status) {
316            switch (status) {
317                case STATUS_NOT_PRESENT:
318                case STATUS_PRESENT:
319                case STATUS_ENUMERATING:
320                case STATUS_NOT_AVAILABLE:
321                    return true;
322                default:
323                    return false;
324            }
325        }
326
327        @Override
328        public void onStatusChanged(int status, int cameraId) throws RemoteException {
329            synchronized(CameraManager.this.mLock) {
330
331                Log.v(TAG,
332                        String.format("Camera id %d has status changed to 0x%x", cameraId, status));
333
334                String id = String.valueOf(cameraId);
335
336                if (!validStatus(status)) {
337                    Log.e(TAG, String.format("Ignoring invalid device %d status 0x%x", cameraId,
338                            status));
339                    return;
340                }
341
342                Integer oldStatus = mDeviceStatus.put(id, status);
343
344                if (oldStatus == status) {
345                    Log.v(TAG, String.format(
346                            "Device status changed to 0x%x, which is what it already was",
347                            status));
348                    return;
349                }
350
351                // TODO: consider abstracting out this state minimization + transition
352                // into a separate
353                // more easily testable class
354                // i.e. (new State()).addState(STATE_AVAILABLE)
355                //                   .addState(STATE_NOT_AVAILABLE)
356                //                   .addTransition(STATUS_PRESENT, STATE_AVAILABLE),
357                //                   .addTransition(STATUS_NOT_PRESENT, STATE_NOT_AVAILABLE)
358                //                   .addTransition(STATUS_ENUMERATING, STATE_NOT_AVAILABLE);
359                //                   .addTransition(STATUS_NOT_AVAILABLE, STATE_NOT_AVAILABLE);
360
361                // Translate all the statuses to either 'available' or 'not available'
362                //  available -> available         => no new update
363                //  not available -> not available => no new update
364                if (oldStatus != null && isAvailable(status) == isAvailable(oldStatus)) {
365
366                    Log.v(TAG,
367                            String.format(
368                                    "Device status was previously available (%d), " +
369                                            " and is now again available (%d)" +
370                                            "so no new client visible update will be sent",
371                                    isAvailable(status), isAvailable(status)));
372                    return;
373                }
374
375                for (CameraListener listener : mListenerSet) {
376                    if (isAvailable(status)) {
377                        listener.onCameraAvailable(id);
378                    } else {
379                        listener.onCameraUnavailable(id);
380                    }
381                } // for
382            } // synchronized
383        } // onStatusChanged
384    } // CameraServiceListener
385} // CameraManager
386