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