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