1 package org.robolectric.shadows; 2 3 import static org.robolectric.shadow.api.Shadow.newInstanceOf; 4 5 import android.hardware.Camera; 6 import android.view.SurfaceHolder; 7 import com.google.common.base.Joiner; 8 import com.google.common.collect.ImmutableList; 9 import com.google.common.collect.ImmutableMap; 10 import java.util.ArrayList; 11 import java.util.Collections; 12 import java.util.HashMap; 13 import java.util.List; 14 import java.util.Map; 15 import org.robolectric.annotation.Implementation; 16 import org.robolectric.annotation.Implements; 17 import org.robolectric.annotation.RealObject; 18 import org.robolectric.annotation.Resetter; 19 import org.robolectric.shadow.api.Shadow; 20 import org.robolectric.util.ReflectionHelpers; 21 import org.robolectric.util.ReflectionHelpers.ClassParameter; 22 23 @Implements(Camera.class) 24 public class ShadowCamera { 25 // These are completely arbitrary and likely outdated default parameters that have been added long 26 // ago. 27 private static final ImmutableMap<String, String> DEFAULT_PARAMS = 28 ImmutableMap.<String, String>builder() 29 .put("picture-size", "1280x960") 30 .put("preview-size", "640x480") 31 .put("preview-fps-range", "10,30") 32 .put("preview-frame-rate", "30") 33 .put("preview-format", "yuv420sp") 34 .put("picture-format-values", "yuv420sp,jpeg") 35 .put("preview-format-values", "yuv420sp,jpeg") 36 .put("picture-size-values", "320x240,640x480,800x600") 37 .put("preview-size-values", "320x240,640x480") 38 .put("preview-fps-range-values", "(15000,15000),(10000,30000)") 39 .put("preview-frame-rate-values", "10,15,30") 40 .put("exposure-compensation", "0") 41 .put("exposure-compensation-step", "0.5") 42 .put("min-exposure-compensation", "-6") 43 .put("max-exposure-compensation", "6") 44 .put("focus-mode-values", Camera.Parameters.FOCUS_MODE_AUTO) 45 .put("focus-mode", Camera.Parameters.FOCUS_MODE_AUTO) 46 .put( 47 "flash-mode-values", 48 Camera.Parameters.FLASH_MODE_AUTO 49 + "," 50 + Camera.Parameters.FLASH_MODE_ON 51 + "," 52 + Camera.Parameters.FLASH_MODE_OFF) 53 .put("flash-mode", Camera.Parameters.FLASH_MODE_AUTO) 54 .put("max-num-focus-areas", "1") 55 .put("max-num-metering-areas", "1") 56 .build(); 57 58 private static int lastOpenedCameraId; 59 60 private int id; 61 private boolean locked = true; 62 private boolean previewing; 63 private boolean released; 64 private Camera.Parameters parameters; 65 private Camera.PreviewCallback previewCallback; 66 private List<byte[]> callbackBuffers = new ArrayList<>(); 67 private SurfaceHolder surfaceHolder; 68 private int displayOrientation; 69 private Camera.AutoFocusCallback autoFocusCallback; 70 private boolean autoFocusing; 71 private boolean shutterSoundEnabled = true; 72 73 private static final Map<Integer, Camera.CameraInfo> cameras = new HashMap<>(); 74 private static final Map<Integer, Camera.Parameters> cameraParameters = new HashMap<>(); 75 76 @RealObject private Camera realCamera; 77 78 @Implementation open()79 protected static Camera open() { 80 return open(0); 81 } 82 83 @Implementation open(int cameraId)84 protected static Camera open(int cameraId) { 85 lastOpenedCameraId = cameraId; 86 Camera camera = newInstanceOf(Camera.class); 87 ShadowCamera shadowCamera = Shadow.extract(camera); 88 shadowCamera.id = cameraId; 89 if (cameraParameters.containsKey(cameraId)) { 90 shadowCamera.parameters = cameraParameters.get(cameraId); 91 } else { 92 cameraParameters.put(cameraId, camera.getParameters()); 93 } 94 return camera; 95 } 96 getLastOpenedCameraId()97 public static int getLastOpenedCameraId() { 98 return lastOpenedCameraId; 99 } 100 101 @Implementation unlock()102 protected void unlock() { 103 locked = false; 104 } 105 106 @Implementation reconnect()107 protected void reconnect() { 108 locked = true; 109 } 110 111 @Implementation getParameters()112 protected Camera.Parameters getParameters() { 113 if (parameters == null) { 114 parameters = 115 ReflectionHelpers.callConstructor( 116 Camera.Parameters.class, ClassParameter.from(Camera.class, realCamera)); 117 Joiner.MapJoiner mapJoiner = Joiner.on(";").withKeyValueSeparator("="); 118 parameters.unflatten(mapJoiner.join(DEFAULT_PARAMS)); 119 } 120 return parameters; 121 } 122 123 @Implementation setParameters(Camera.Parameters params)124 protected void setParameters(Camera.Parameters params) { 125 parameters = params; 126 } 127 128 @Implementation setPreviewDisplay(SurfaceHolder holder)129 protected void setPreviewDisplay(SurfaceHolder holder) { 130 surfaceHolder = holder; 131 } 132 133 @Implementation startPreview()134 protected void startPreview() { 135 previewing = true; 136 } 137 138 @Implementation stopPreview()139 protected void stopPreview() { 140 previewing = false; 141 } 142 143 @Implementation release()144 protected void release() { 145 released = true; 146 } 147 148 @Implementation setPreviewCallback(Camera.PreviewCallback cb)149 protected void setPreviewCallback(Camera.PreviewCallback cb) { 150 previewCallback = cb; 151 } 152 153 @Implementation setOneShotPreviewCallback(Camera.PreviewCallback cb)154 protected void setOneShotPreviewCallback(Camera.PreviewCallback cb) { 155 previewCallback = cb; 156 } 157 158 @Implementation setPreviewCallbackWithBuffer(Camera.PreviewCallback cb)159 protected void setPreviewCallbackWithBuffer(Camera.PreviewCallback cb) { 160 previewCallback = cb; 161 } 162 163 /** 164 * Allows test cases to invoke the preview callback, to simulate a frame of camera data. 165 * 166 * @param data byte buffer of simulated camera data 167 */ invokePreviewCallback(byte[] data)168 public void invokePreviewCallback(byte[] data) { 169 if (previewCallback != null) { 170 previewCallback.onPreviewFrame(data, realCamera); 171 } 172 } 173 174 @Implementation addCallbackBuffer(byte[] callbackBuffer)175 protected void addCallbackBuffer(byte[] callbackBuffer) { 176 callbackBuffers.add(callbackBuffer); 177 } 178 getAddedCallbackBuffers()179 public List<byte[]> getAddedCallbackBuffers() { 180 return Collections.unmodifiableList(callbackBuffers); 181 } 182 183 @Implementation setDisplayOrientation(int degrees)184 protected void setDisplayOrientation(int degrees) { 185 displayOrientation = degrees; 186 if (cameras.containsKey(id)) { 187 cameras.get(id).orientation = degrees; 188 } 189 } 190 getDisplayOrientation()191 public int getDisplayOrientation() { 192 return displayOrientation; 193 } 194 195 @Implementation autoFocus(Camera.AutoFocusCallback callback)196 protected void autoFocus(Camera.AutoFocusCallback callback) { 197 autoFocusCallback = callback; 198 autoFocusing = true; 199 } 200 201 @Implementation cancelAutoFocus()202 protected void cancelAutoFocus() { 203 autoFocusCallback = null; 204 autoFocusing = false; 205 } 206 hasRequestedAutoFocus()207 public boolean hasRequestedAutoFocus() { 208 return autoFocusing; 209 } 210 invokeAutoFocusCallback(boolean success, Camera camera)211 public void invokeAutoFocusCallback(boolean success, Camera camera) { 212 if (autoFocusCallback == null) { 213 throw new IllegalStateException( 214 "cannot invoke AutoFocusCallback before autoFocus() has been called " 215 + "or after cancelAutoFocus() has been called " 216 + "or after the callback has been invoked."); 217 } 218 autoFocusCallback.onAutoFocus(success, camera); 219 autoFocusCallback = null; 220 autoFocusing = false; 221 } 222 223 @Implementation getCameraInfo(int cameraId, Camera.CameraInfo cameraInfo)224 protected static void getCameraInfo(int cameraId, Camera.CameraInfo cameraInfo) { 225 Camera.CameraInfo foundCam = cameras.get(cameraId); 226 cameraInfo.facing = foundCam.facing; 227 cameraInfo.orientation = foundCam.orientation; 228 cameraInfo.canDisableShutterSound = foundCam.canDisableShutterSound; 229 } 230 231 @Implementation getNumberOfCameras()232 protected static int getNumberOfCameras() { 233 return cameras.size(); 234 } 235 236 @Implementation takePicture( Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg)237 protected void takePicture( 238 Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg) { 239 if (shutter != null) { 240 shutter.onShutter(); 241 } 242 243 if (raw != null) { 244 raw.onPictureTaken(new byte[0], realCamera); 245 } 246 247 if (jpeg != null) { 248 jpeg.onPictureTaken(new byte[0], realCamera); 249 } 250 } 251 252 @Implementation enableShutterSound(boolean enabled)253 protected boolean enableShutterSound(boolean enabled) { 254 if (!enabled && cameras.containsKey(id) && !cameras.get(id).canDisableShutterSound) { 255 return false; 256 } 257 shutterSoundEnabled = enabled; 258 return true; 259 } 260 261 /** Returns {@code true} if the default shutter sound is played when taking a picture. */ isShutterSoundEnabled()262 public boolean isShutterSoundEnabled() { 263 return shutterSoundEnabled; 264 } 265 isLocked()266 public boolean isLocked() { 267 return locked; 268 } 269 isPreviewing()270 public boolean isPreviewing() { 271 return previewing; 272 } 273 isReleased()274 public boolean isReleased() { 275 return released; 276 } 277 getPreviewDisplay()278 public SurfaceHolder getPreviewDisplay() { 279 return surfaceHolder; 280 } 281 282 /** 283 * Add a mock {@code Camera.CameraInfo} object to simulate the existence of one or more cameras. 284 * By default, no cameras are defined. 285 * 286 * @param id The camera id 287 * @param camInfo The CameraInfo 288 */ addCameraInfo(int id, Camera.CameraInfo camInfo)289 public static void addCameraInfo(int id, Camera.CameraInfo camInfo) { 290 cameras.put(id, camInfo); 291 } 292 293 @Resetter clearCameraInfo()294 public static void clearCameraInfo() { 295 cameras.clear(); 296 cameraParameters.clear(); 297 } 298 299 /** Shadows the Android {@code Camera.Parameters} class. */ 300 @Implements(Camera.Parameters.class) 301 public static class ShadowParameters { 302 303 @SuppressWarnings("nullness:initialization.field.uninitialized") // Managed by Robolectric 304 @RealObject 305 private Camera.Parameters realParameters; 306 initSupportedPreviewSizes()307 public void initSupportedPreviewSizes() { 308 realParameters.remove("preview-size-values"); 309 } 310 setSupportedFocusModes(String... focusModes)311 public void setSupportedFocusModes(String... focusModes) { 312 realParameters.set("focus-mode-values", Joiner.on(",").join(focusModes)); 313 if (focusModes.length == 0) { 314 realParameters.remove("focus-mode"); 315 } 316 } 317 setSupportedFlashModes(String... flashModes)318 public void setSupportedFlashModes(String... flashModes) { 319 realParameters.set("flash-mode-values", Joiner.on(",").join(flashModes)); 320 if (flashModes.length == 0) { 321 realParameters.remove("flash-mode"); 322 } 323 } 324 325 /** 326 * Allows test cases to set the maximum number of focus areas. See {@link 327 * Camera.Parameters#getMaxNumFocusAreas}. 328 */ setMaxNumFocusAreas(int maxNumFocusAreas)329 public void setMaxNumFocusAreas(int maxNumFocusAreas) { 330 realParameters.set("max-num-focus-areas", maxNumFocusAreas); 331 } 332 addSupportedPreviewSize(int width, int height)333 public void addSupportedPreviewSize(int width, int height) { 334 List<String> sizesStrings = new ArrayList<>(); 335 List<Camera.Size> sizes = realParameters.getSupportedPreviewSizes(); 336 if (sizes == null) { 337 sizes = ImmutableList.of(); 338 } 339 for (Camera.Size size : sizes) { 340 sizesStrings.add(size.width + "x" + size.height); 341 } 342 sizesStrings.add(width + "x" + height); 343 realParameters.set("preview-size-values", Joiner.on(",").join(sizesStrings)); 344 } 345 346 /** 347 * Allows test cases to set the maximum number of metering areas. See {@link 348 * Camera.Parameters#getMaxNumMeteringAreas}. 349 */ setMaxNumMeteringAreas(int maxNumMeteringAreas)350 public void setMaxNumMeteringAreas(int maxNumMeteringAreas) { 351 realParameters.set("max-num-metering-areas", maxNumMeteringAreas); 352 } 353 getPreviewWidth()354 public int getPreviewWidth() { 355 return realParameters.getPreviewSize().width; 356 } 357 getPreviewHeight()358 public int getPreviewHeight() { 359 return realParameters.getPreviewSize().height; 360 } 361 getPictureWidth()362 public int getPictureWidth() { 363 return realParameters.getPictureSize().width; 364 } 365 getPictureHeight()366 public int getPictureHeight() { 367 return realParameters.getPictureSize().height; 368 } 369 getRotation()370 public int getRotation() { 371 return realParameters.getInt("rotation"); 372 } 373 } 374 } 375