Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 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 | |
| 17 | package android.hardware.photography; |
| 18 | |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 19 | import android.content.Context; |
| 20 | import android.hardware.ICameraService; |
| 21 | import android.hardware.ICameraServiceListener; |
| 22 | import android.hardware.IProCameraUser; |
| 23 | import android.hardware.photography.utils.CameraBinderDecorator; |
| 24 | import android.hardware.photography.utils.CameraRuntimeException; |
| 25 | import android.os.Binder; |
| 26 | import android.os.IBinder; |
| 27 | import android.os.RemoteException; |
| 28 | import android.os.ServiceManager; |
| 29 | import android.util.Log; |
| 30 | |
| 31 | import java.util.ArrayList; |
| 32 | import java.util.HashMap; |
| 33 | import java.util.HashSet; |
| 34 | |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 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.photography photography} |
| 46 | * package documentation.</p> |
| 47 | */ |
| 48 | public final class CameraManager { |
| 49 | |
| 50 | /** |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 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 | /** |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 63 | * @hide |
| 64 | */ |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 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 | } |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 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 | */ |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 95 | public String[] getDeviceIdList() throws CameraAccessException { |
| 96 | synchronized (mLock) { |
Igor Murashkin | 7072550 | 2013-06-25 20:27:06 +0000 | [diff] [blame] | 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 | } |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 104 | } |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Register a listener to be notified about camera device availability. |
| 109 | * |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 110 | * Registering a listener more than once has no effect. |
| 111 | * |
| 112 | * @param listener the new listener to send camera availability notices to. |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 113 | */ |
| 114 | public void registerCameraListener(CameraListener listener) { |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 115 | synchronized (mLock) { |
| 116 | mListenerSet.add(listener); |
| 117 | } |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 118 | } |
| 119 | |
| 120 | /** |
| 121 | * Remove a previously-added listener; the listener will no longer receive |
| 122 | * connection and disconnection callbacks. |
| 123 | * |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 124 | * Removing a listener that isn't registered has no effect. |
| 125 | * |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 126 | * @param listener the listener to remove from the notification list |
| 127 | */ |
| 128 | public void unregisterCameraListener(CameraListener listener) { |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 129 | synchronized (mLock) { |
| 130 | mListenerSet.remove(listener); |
| 131 | } |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 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 { |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 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(); |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 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 { |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 186 | |
| 187 | try { |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 188 | |
| 189 | synchronized (mLock) { |
Igor Murashkin | 7072550 | 2013-06-25 20:27:06 +0000 | [diff] [blame] | 190 | |
| 191 | ICameraDeviceUser cameraUser; |
| 192 | |
| 193 | android.hardware.photography.impl.CameraDevice device = |
| 194 | new android.hardware.photography.impl.CameraDevice(cameraId); |
| 195 | |
| 196 | cameraUser = mCameraService.connectDevice(device.getCallbacks(), |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 197 | Integer.parseInt(cameraId), |
| 198 | mContext.getPackageName(), USE_CALLING_UID); |
| 199 | |
Igor Murashkin | 7072550 | 2013-06-25 20:27:06 +0000 | [diff] [blame] | 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 | |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 212 | } |
| 213 | |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 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 | } |
Eino-Ville Talvala | b267554 | 2012-12-12 13:29:45 -0800 | [diff] [blame] | 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 | } |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 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 { |
Igor Murashkin | 7072550 | 2013-06-25 20:27:06 +0000 | [diff] [blame] | 330 | synchronized(CameraManager.this.mLock) { |
Igor Murashkin | e363fbb | 2013-06-25 20:26:06 +0000 | [diff] [blame] | 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 |