1 package org.robolectric.shadows; 2 3 import static org.robolectric.Shadows.shadowOf; 4 import static org.robolectric.util.reflector.Reflector.reflector; 5 6 import android.content.Context; 7 import android.hardware.camera2.CameraAccessException; 8 import android.hardware.camera2.CameraCharacteristics; 9 import android.hardware.camera2.CameraDevice; 10 import android.hardware.camera2.CameraDevice.StateCallback; 11 import android.hardware.camera2.CameraManager; 12 import android.hardware.camera2.impl.CameraDeviceImpl; 13 import android.os.Build; 14 import android.os.Build.VERSION_CODES; 15 import android.os.Handler; 16 import com.google.common.base.Preconditions; 17 import java.util.Collections; 18 import java.util.HashMap; 19 import java.util.HashSet; 20 import java.util.LinkedHashMap; 21 import java.util.Map; 22 import java.util.Set; 23 import java.util.WeakHashMap; 24 import java.util.concurrent.Executor; 25 import javax.annotation.Nonnull; 26 import javax.annotation.Nullable; 27 import org.robolectric.RuntimeEnvironment; 28 import org.robolectric.annotation.Implementation; 29 import org.robolectric.annotation.Implements; 30 import org.robolectric.annotation.InDevelopment; 31 import org.robolectric.annotation.RealObject; 32 import org.robolectric.annotation.Resetter; 33 import org.robolectric.util.ReflectionHelpers; 34 import org.robolectric.util.ReflectionHelpers.ClassParameter; 35 import org.robolectric.util.reflector.Accessor; 36 import org.robolectric.util.reflector.Constructor; 37 import org.robolectric.util.reflector.ForType; 38 import org.robolectric.util.reflector.WithType; 39 import org.robolectric.versioning.AndroidVersions.Baklava; 40 import org.robolectric.versioning.AndroidVersions.U; 41 import org.robolectric.versioning.AndroidVersions.V; 42 43 /** Shadow class for {@link CameraManager} */ 44 @Implements(value = CameraManager.class) 45 public class ShadowCameraManager { 46 // Keep references to cameras so they can be closed after each test 47 protected static final Set<CameraDeviceImpl> createdCameras = 48 Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>())); 49 // LinkedHashMap used to ensure getCameraIdList returns ids in the order in which they were added 50 private static final Map<String, CameraCharacteristics> cameraIdToCharacteristics = 51 new LinkedHashMap<>(); 52 private static final Map<String, Boolean> cameraTorches = new HashMap<>(); 53 private static final Set<CameraManager.AvailabilityCallback> registeredCallbacks = 54 new HashSet<>(); 55 // Cannot reference the torch callback in < Android M 56 private static final Set<Object> torchCallbacks = new HashSet<>(); 57 // Most recent camera device opened with openCamera 58 private static CameraDevice lastDevice; 59 // Most recent callback passed to openCamera 60 private static CameraDevice.StateCallback lastCallback; 61 @Nullable private static Executor lastCallbackExecutor; 62 @Nullable private static Handler lastCallbackHandler; 63 @RealObject private CameraManager realObject; 64 65 @Resetter reset()66 public static void reset() { 67 for (CameraDeviceImpl cameraDevice : createdCameras) { 68 if (cameraDevice != null) { 69 cameraDevice.close(); 70 } 71 } 72 createdCameras.clear(); 73 cameraIdToCharacteristics.clear(); 74 cameraTorches.clear(); 75 registeredCallbacks.clear(); 76 torchCallbacks.clear(); 77 if (lastDevice != null) { 78 lastDevice.close(); 79 } 80 lastDevice = null; 81 lastCallback = null; 82 lastCallbackExecutor = null; 83 if (lastCallbackHandler != null) { 84 // Flush existing handler tasks to ensure camera related callbacks are called properly. 85 shadowOf(lastCallbackHandler.getLooper()).idle(); 86 lastCallbackHandler.removeCallbacksAndMessages(null); 87 } 88 lastCallbackHandler = null; 89 } 90 91 @Implementation 92 @Nonnull getCameraIdList()93 protected String[] getCameraIdList() throws CameraAccessException { 94 Set<String> cameraIds = cameraIdToCharacteristics.keySet(); 95 return cameraIds.toArray(new String[0]); 96 } 97 98 @Implementation 99 @Nonnull getCameraCharacteristics(@onnull String cameraId)100 protected CameraCharacteristics getCameraCharacteristics(@Nonnull String cameraId) { 101 Preconditions.checkNotNull(cameraId); 102 CameraCharacteristics characteristics = cameraIdToCharacteristics.get(cameraId); 103 Preconditions.checkArgument(characteristics != null); 104 return characteristics; 105 } 106 107 @Implementation(minSdk = VERSION_CODES.M) setTorchMode(@onnull String cameraId, boolean enabled)108 protected void setTorchMode(@Nonnull String cameraId, boolean enabled) { 109 Preconditions.checkNotNull(cameraId); 110 Preconditions.checkArgument(cameraIdToCharacteristics.keySet().contains(cameraId)); 111 cameraTorches.put(cameraId, enabled); 112 for (Object callback : torchCallbacks) { 113 ((CameraManager.TorchCallback) callback).onTorchModeChanged(cameraId, enabled); 114 } 115 } 116 117 @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT) openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid, final int oomScoreOffset, boolean overrideToPortrait)118 protected CameraDevice openCameraDeviceUserAsync( 119 String cameraId, 120 CameraDevice.StateCallback callback, 121 Executor executor, 122 final int uid, 123 final int oomScoreOffset, 124 boolean overrideToPortrait) { 125 return openCameraDeviceUserAsync(cameraId, callback, executor, uid, oomScoreOffset); 126 } 127 128 @Implementation(minSdk = V.SDK_INT, maxSdk = V.SDK_INT) openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid, final int oomScoreOffset, int rotationOverride)129 protected CameraDevice openCameraDeviceUserAsync( 130 String cameraId, 131 CameraDevice.StateCallback callback, 132 Executor executor, 133 final int uid, 134 final int oomScoreOffset, 135 int rotationOverride) { 136 return openCameraDeviceUserAsync(cameraId, callback, executor, uid, oomScoreOffset); 137 } 138 139 // in development API has reverted back to the T signature. Just use a different method name 140 // to avoid conflicts. 141 // TODO: increment this to minSdk next-SDK-after-V once V is fully released 142 @Implementation(methodName = "openCameraDeviceUserAsync", minSdk = Baklava.SDK_INT) 143 @InDevelopment openCameraDeviceUserAsyncPostV( String cameraId, CameraDevice.StateCallback callback, Executor executor, int unusedClientUid, int unusedOomScoreOffset, boolean unused)144 protected CameraDevice openCameraDeviceUserAsyncPostV( 145 String cameraId, 146 CameraDevice.StateCallback callback, 147 Executor executor, 148 int unusedClientUid, 149 int unusedOomScoreOffset, 150 boolean unused) { 151 return openCameraDeviceUserAsync( 152 cameraId, callback, executor, unusedClientUid, unusedOomScoreOffset); 153 } 154 155 @Implementation(minSdk = Build.VERSION_CODES.S, maxSdk = Build.VERSION_CODES.TIRAMISU) openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Executor executor, int unusedClientUid, int unusedOomScoreOffset)156 protected CameraDevice openCameraDeviceUserAsync( 157 String cameraId, 158 CameraDevice.StateCallback callback, 159 Executor executor, 160 int unusedClientUid, 161 int unusedOomScoreOffset) { 162 CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); 163 Context context = RuntimeEnvironment.getApplication(); 164 CameraDeviceImpl deviceImpl = 165 createCameraDeviceImpl(cameraId, callback, executor, characteristics, context); 166 createdCameras.add(deviceImpl); 167 updateCameraCallback(deviceImpl, callback, null, executor); 168 executor.execute(() -> callback.onOpened(deviceImpl)); 169 return deviceImpl; 170 } 171 172 @Implementation(minSdk = VERSION_CODES.P, maxSdk = VERSION_CODES.R) openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid)173 protected CameraDevice openCameraDeviceUserAsync( 174 String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid) 175 throws CameraAccessException { 176 CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); 177 Context context = reflector(ReflectorCameraManager.class, realObject).getContext(); 178 179 CameraDeviceImpl deviceImpl = 180 ReflectionHelpers.callConstructor( 181 CameraDeviceImpl.class, 182 ClassParameter.from(String.class, cameraId), 183 ClassParameter.from(CameraDevice.StateCallback.class, callback), 184 ClassParameter.from(Executor.class, executor), 185 ClassParameter.from(CameraCharacteristics.class, characteristics), 186 ClassParameter.from(int.class, context.getApplicationInfo().targetSdkVersion)); 187 188 createdCameras.add(deviceImpl); 189 updateCameraCallback(deviceImpl, callback, null, executor); 190 executor.execute(() -> callback.onOpened(deviceImpl)); 191 return deviceImpl; 192 } 193 194 @Implementation(minSdk = VERSION_CODES.N_MR1, maxSdk = VERSION_CODES.O_MR1) openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Handler handler, final int uid)195 protected CameraDevice openCameraDeviceUserAsync( 196 String cameraId, CameraDevice.StateCallback callback, Handler handler, final int uid) 197 throws CameraAccessException { 198 CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); 199 Context context = reflector(ReflectorCameraManager.class, realObject).getContext(); 200 201 CameraDeviceImpl deviceImpl; 202 if (Build.VERSION.SDK_INT == VERSION_CODES.N_MR1) { 203 deviceImpl = 204 ReflectionHelpers.callConstructor( 205 CameraDeviceImpl.class, 206 ClassParameter.from(String.class, cameraId), 207 ClassParameter.from(CameraDevice.StateCallback.class, callback), 208 ClassParameter.from(Handler.class, handler), 209 ClassParameter.from(CameraCharacteristics.class, characteristics)); 210 } else { 211 deviceImpl = 212 ReflectionHelpers.callConstructor( 213 CameraDeviceImpl.class, 214 ClassParameter.from(String.class, cameraId), 215 ClassParameter.from(CameraDevice.StateCallback.class, callback), 216 ClassParameter.from(Handler.class, handler), 217 ClassParameter.from(CameraCharacteristics.class, characteristics), 218 ClassParameter.from(int.class, context.getApplicationInfo().targetSdkVersion)); 219 } 220 createdCameras.add(deviceImpl); 221 updateCameraCallback(deviceImpl, callback, handler, null); 222 handler.post(() -> callback.onOpened(deviceImpl)); 223 return deviceImpl; 224 } 225 226 /** 227 * Enables {@link CameraManager#openCamera(String, StateCallback, Handler)} to open a {@link 228 * CameraDevice}. 229 * 230 * <p>If the provided cameraId exists, this will always post {@link 231 * CameraDevice.StateCallback#onOpened(CameraDevice)} to the provided {@link Handler}. Unlike on 232 * real Android, this will not check if the camera has been disabled by device policy and does not 233 * attempt to connect to the camera service, so {@link 234 * CameraDevice.StateCallback#onError(CameraDevice, int)} and {@link 235 * CameraDevice.StateCallback#onDisconnected(CameraDevice)} will not be triggered by {@link 236 * CameraManager#openCamera(String, StateCallback, Handler)}. 237 */ 238 @Implementation(maxSdk = VERSION_CODES.N) openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Handler handler)239 protected CameraDevice openCameraDeviceUserAsync( 240 String cameraId, CameraDevice.StateCallback callback, Handler handler) 241 throws CameraAccessException { 242 CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); 243 244 CameraDeviceImpl deviceImpl = 245 ReflectionHelpers.callConstructor( 246 CameraDeviceImpl.class, 247 ClassParameter.from(String.class, cameraId), 248 ClassParameter.from(CameraDevice.StateCallback.class, callback), 249 ClassParameter.from(Handler.class, handler), 250 ClassParameter.from(CameraCharacteristics.class, characteristics)); 251 252 createdCameras.add(deviceImpl); 253 updateCameraCallback(deviceImpl, callback, handler, null); 254 handler.post(() -> callback.onOpened(deviceImpl)); 255 return deviceImpl; 256 } 257 258 @Implementation registerAvailabilityCallback( CameraManager.AvailabilityCallback callback, Handler handler)259 protected void registerAvailabilityCallback( 260 CameraManager.AvailabilityCallback callback, Handler handler) { 261 Preconditions.checkNotNull(callback); 262 registeredCallbacks.add(callback); 263 } 264 265 @Implementation unregisterAvailabilityCallback(CameraManager.AvailabilityCallback callback)266 protected void unregisterAvailabilityCallback(CameraManager.AvailabilityCallback callback) { 267 Preconditions.checkNotNull(callback); 268 registeredCallbacks.remove(callback); 269 } 270 271 @Implementation(minSdk = VERSION_CODES.M) registerTorchCallback(CameraManager.TorchCallback callback, Handler handler)272 protected void registerTorchCallback(CameraManager.TorchCallback callback, Handler handler) { 273 Preconditions.checkNotNull(callback); 274 torchCallbacks.add(callback); 275 } 276 277 @Implementation(minSdk = VERSION_CODES.M) unregisterTorchCallback(CameraManager.TorchCallback callback)278 protected void unregisterTorchCallback(CameraManager.TorchCallback callback) { 279 Preconditions.checkNotNull(callback); 280 torchCallbacks.remove(callback); 281 } 282 createCameraDeviceImpl( String cameraId, CameraDevice.StateCallback callback, Executor executor, CameraCharacteristics characteristics, Context context)283 private CameraDeviceImpl createCameraDeviceImpl( 284 String cameraId, 285 CameraDevice.StateCallback callback, 286 Executor executor, 287 CameraCharacteristics characteristics, 288 Context context) { 289 Map<String, CameraCharacteristics> cameraCharacteristicsMap = Collections.emptyMap(); 290 if (RuntimeEnvironment.getApiLevel() >= Baklava.SDK_INT) { 291 return reflector(ReflectorCameraDeviceImpl.class) 292 .newCameraDeviceImplPostV( 293 cameraId, 294 callback, 295 executor, 296 characteristics, 297 realObject, 298 context.getApplicationInfo().targetSdkVersion, 299 context, 300 null, 301 false); 302 303 } else if (RuntimeEnvironment.getApiLevel() == V.SDK_INT) { 304 return reflector(ReflectorCameraDeviceImpl.class) 305 .newCameraDeviceImplV( 306 cameraId, 307 callback, 308 executor, 309 characteristics, 310 realObject, 311 context.getApplicationInfo().targetSdkVersion, 312 context, 313 null); 314 } else { 315 return reflector(ReflectorCameraDeviceImpl.class) 316 .newCameraDeviceImpl( 317 cameraId, 318 callback, 319 executor, 320 characteristics, 321 cameraCharacteristicsMap, 322 context.getApplicationInfo().targetSdkVersion, 323 context); 324 } 325 } 326 327 /** 328 * Calls all registered callbacks's onCameraAvailable method. This is a no-op if no callbacks are 329 * registered. 330 */ triggerOnCameraAvailable(@onnull String cameraId)331 private void triggerOnCameraAvailable(@Nonnull String cameraId) { 332 Preconditions.checkNotNull(cameraId); 333 for (CameraManager.AvailabilityCallback callback : registeredCallbacks) { 334 callback.onCameraAvailable(cameraId); 335 } 336 } 337 338 /** 339 * Calls all registered callbacks's onCameraUnavailable method. This is a no-op if no callbacks 340 * are registered. 341 */ triggerOnCameraUnavailable(@onnull String cameraId)342 private void triggerOnCameraUnavailable(@Nonnull String cameraId) { 343 Preconditions.checkNotNull(cameraId); 344 for (CameraManager.AvailabilityCallback callback : registeredCallbacks) { 345 callback.onCameraUnavailable(cameraId); 346 } 347 } 348 349 /** 350 * Adds the given cameraId and characteristics to this shadow. 351 * 352 * <p>The result from {@link #getCameraIdList()} will be in the order in which cameras were added. 353 * 354 * @throws IllegalArgumentException if there's already an existing camera with the given id. 355 */ addCamera(@onnull String cameraId, @Nonnull CameraCharacteristics characteristics)356 public void addCamera(@Nonnull String cameraId, @Nonnull CameraCharacteristics characteristics) { 357 Preconditions.checkNotNull(cameraId); 358 Preconditions.checkNotNull(characteristics); 359 Preconditions.checkArgument(!cameraIdToCharacteristics.containsKey(cameraId)); 360 361 cameraIdToCharacteristics.put(cameraId, characteristics); 362 triggerOnCameraAvailable(cameraId); 363 } 364 365 /** 366 * Removes the given cameraId and associated characteristics from this shadow. 367 * 368 * @throws IllegalArgumentException if there is not an existing camera with the given id. 369 */ removeCamera(@onnull String cameraId)370 public void removeCamera(@Nonnull String cameraId) { 371 Preconditions.checkNotNull(cameraId); 372 Preconditions.checkArgument(cameraIdToCharacteristics.containsKey(cameraId)); 373 374 cameraIdToCharacteristics.remove(cameraId); 375 triggerOnCameraUnavailable(cameraId); 376 } 377 378 /** Returns what the supplied camera's torch is set to. */ getTorchMode(@onnull String cameraId)379 public boolean getTorchMode(@Nonnull String cameraId) { 380 Preconditions.checkNotNull(cameraId); 381 Preconditions.checkArgument(cameraIdToCharacteristics.keySet().contains(cameraId)); 382 Boolean torchState = cameraTorches.get(cameraId); 383 return torchState; 384 } 385 386 /** 387 * Triggers a disconnect event, where any open camera will be disconnected (simulating the case 388 * where another app takes control of the camera). 389 */ triggerDisconnect()390 public void triggerDisconnect() { 391 if (lastCallbackHandler != null) { 392 lastCallbackHandler.post(() -> lastCallback.onDisconnected(lastDevice)); 393 } else if (lastCallbackExecutor != null) { 394 lastCallbackExecutor.execute(() -> lastCallback.onDisconnected(lastDevice)); 395 } 396 } 397 updateCameraCallback( CameraDevice device, CameraDevice.StateCallback callback, @Nullable Handler handler, @Nullable Executor executor)398 protected void updateCameraCallback( 399 CameraDevice device, 400 CameraDevice.StateCallback callback, 401 @Nullable Handler handler, 402 @Nullable Executor executor) { 403 lastDevice = device; 404 lastCallback = callback; 405 lastCallbackHandler = handler; 406 lastCallbackExecutor = executor; 407 } 408 409 @ForType(CameraDeviceImpl.class) 410 interface ReflectorCameraDeviceImpl { 411 @Constructor newCameraDeviceImpl( String cameraId, CameraDevice.StateCallback callback, Executor executor, CameraCharacteristics characteristics, Map<String, CameraCharacteristics> characteristicsMap, int targetSdkVersion, Context context)412 CameraDeviceImpl newCameraDeviceImpl( 413 String cameraId, 414 CameraDevice.StateCallback callback, 415 Executor executor, 416 CameraCharacteristics characteristics, 417 Map<String, CameraCharacteristics> characteristicsMap, 418 int targetSdkVersion, 419 Context context); 420 421 @Constructor newCameraDeviceImplV( String cameraId, CameraDevice.StateCallback callback, Executor executor, CameraCharacteristics characteristics, CameraManager cameraManager, int targetSdkVersion, Context context, @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") Object cameraDeviceSetup)422 CameraDeviceImpl newCameraDeviceImplV( 423 String cameraId, 424 CameraDevice.StateCallback callback, 425 Executor executor, 426 CameraCharacteristics characteristics, 427 CameraManager cameraManager, 428 int targetSdkVersion, 429 Context context, 430 @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") 431 Object cameraDeviceSetup); 432 433 @Constructor newCameraDeviceImplPostV( String cameraId, CameraDevice.StateCallback callback, Executor executor, CameraCharacteristics characteristics, CameraManager cameraManager, int targetSdkVersion, Context context, @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") Object cameraDeviceSetup, boolean unused)434 CameraDeviceImpl newCameraDeviceImplPostV( 435 String cameraId, 436 CameraDevice.StateCallback callback, 437 Executor executor, 438 CameraCharacteristics characteristics, 439 CameraManager cameraManager, 440 int targetSdkVersion, 441 Context context, 442 @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") 443 Object cameraDeviceSetup, 444 boolean unused); 445 } 446 447 /** Accessor interface for {@link CameraManager}'s internals. */ 448 @ForType(CameraManager.class) 449 private interface ReflectorCameraManager { 450 451 @Accessor("mContext") getContext()452 Context getContext(); 453 } 454 455 /** Shadow class for internal class CameraManager$CameraManagerGlobal */ 456 @Implements( 457 className = "android.hardware.camera2.CameraManager$CameraManagerGlobal", 458 minSdk = VERSION_CODES.LOLLIPOP_MR1) 459 public static class ShadowCameraManagerGlobal { 460 461 /** 462 * Cannot create a CameraService connection within Robolectric. Avoid endless reconnect loop. 463 */ 464 @Implementation(minSdk = VERSION_CODES.N) scheduleCameraServiceReconnectionLocked()465 protected void scheduleCameraServiceReconnectionLocked() {} 466 } 467 } 468