package com.google.zxing.client.android;
+import com.google.zxing.ResultPoint;
+
import android.content.Context;
+import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.Camera;
+import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Display;
import android.view.SurfaceHolder;
import android.view.WindowManager;
-import com.google.zxing.ResultPoint;
+
+import java.io.IOException;
/**
* This object wraps the Camera service object and expects to be the only one talking to it. The
* implementation encapsulates the steps needed to take preview-sized images, which are used for
* both preview and decoding.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
*/
final class CameraManager {
-
private static final String TAG = "CameraManager";
+ private static final int MIN_FRAME_WIDTH = 240;
+ private static final int MIN_FRAME_HEIGHT = 240;
+ private static final int MAX_FRAME_WIDTH = 480;
+ private static final int MAX_FRAME_HEIGHT = 360;
+
+ private static CameraManager cameraManager;
+ private Camera camera;
+ private final Context context;
+ private Point screenResolution;
+ private Point cameraResolution;
+ private Rect framingRect;
+ private Handler previewHandler;
+ private int previewMessage;
+ private Handler autoFocusHandler;
+ private int autoFocusMessage;
+ private boolean initialized;
+ private boolean previewing;
+ private int previewFormat;
+ private String previewFormatString;
+ private boolean useOneShotPreviewCallback;
- private static CameraManager mCameraManager;
- private Camera mCamera;
- private final Context mContext;
- private Point mScreenResolution;
- private Rect mFramingRect;
- private Handler mPreviewHandler;
- private int mPreviewMessage;
- private Handler mAutoFocusHandler;
- private int mAutoFocusMessage;
- private boolean mInitialized;
- private boolean mPreviewing;
+ /**
+ * Preview frames are delivered here, which we pass on to the registered handler. Make sure to
+ * clear the handler so it will only receive one message.
+ */
+ private final Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() {
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ if (!useOneShotPreviewCallback) {
+ camera.setPreviewCallback(null);
+ }
+ if (previewHandler != null) {
+ Message message = previewHandler.obtainMessage(previewMessage, cameraResolution.x,
+ cameraResolution.y, data);
+ message.sendToTarget();
+ previewHandler = null;
+ }
+ }
+ };
+ /**
+ * Autofocus callbacks arrive here, and are dispatched to the Handler which requested them.
+ */
+ private final Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() {
+ public void onAutoFocus(boolean success, Camera camera) {
+ if (autoFocusHandler != null) {
+ Message message = autoFocusHandler.obtainMessage(autoFocusMessage, success);
+ // Simulate continuous autofocus by sending a focus request every 1.5 seconds.
+ autoFocusHandler.sendMessageDelayed(message, 1500L);
+ autoFocusHandler = null;
+ }
+ }
+ };
+
+ /**
+ * Initializes this static object with the Context of the calling Activity.
+ *
+ * @param context The Activity which wants to use the camera.
+ */
public static void init(Context context) {
- if (mCameraManager == null) {
- mCameraManager = new CameraManager(context);
+ if (cameraManager == null) {
+ cameraManager = new CameraManager(context);
}
}
+ /**
+ * Gets the CameraManager singleton instance.
+ *
+ * @return A reference to the CameraManager singleton.
+ */
public static CameraManager get() {
- return mCameraManager;
+ return cameraManager;
}
private CameraManager(Context context) {
- mContext = context;
- mCamera = null;
- mInitialized = false;
- mPreviewing = false;
+ this.context = context;
+ camera = null;
+ initialized = false;
+ previewing = false;
+
+ // Camera.setOneShotPreviewCallback() has a race condition in Cupcake, so we use the older
+ // Camera.setPreviewCallback() on 1.5 and earlier. For Donut and later, we need to use
+ // the more efficient one shot callback, as the older one can swamp the system and cause it
+ // to run out of memory. We can't use SDK_INT because it was introduced in the Donut SDK.
+ if (Integer.parseInt(Build.VERSION.SDK) <= Build.VERSION_CODES.CUPCAKE) {
+ useOneShotPreviewCallback = false;
+ } else {
+ useOneShotPreviewCallback = true;
+ }
}
- public void openDriver(SurfaceHolder holder) {
- if (mCamera == null) {
- mCamera = Camera.open();
- mCamera.setPreviewDisplay(holder);
+ /**
+ * Opens the camera driver and initializes the hardware parameters.
+ *
+ * @param holder The surface object which the camera will draw preview frames into.
+ * @throws IOException Indicates the camera driver failed to open.
+ */
+ public void openDriver(SurfaceHolder holder) throws IOException {
+ if (camera == null) {
+ camera = Camera.open();
+ camera.setPreviewDisplay(holder);
- if (!mInitialized) {
- mInitialized = true;
+ if (!initialized) {
+ initialized = true;
getScreenResolution();
}
}
}
+ /**
+ * Closes the camera driver if still in use.
+ */
public void closeDriver() {
- if (mCamera != null) {
- mCamera.release();
- mCamera = null;
+ if (camera != null) {
+ camera.release();
+ camera = null;
}
}
+ /**
+ * Asks the camera hardware to begin drawing preview frames to the screen.
+ */
public void startPreview() {
- if (mCamera != null && !mPreviewing) {
- mCamera.startPreview();
- mPreviewing = true;
+ if (camera != null && !previewing) {
+ camera.startPreview();
+ previewing = true;
}
}
+ /**
+ * Tells the camera to stop drawing preview frames.
+ */
public void stopPreview() {
- if (mCamera != null && mPreviewing) {
- mCamera.setPreviewCallback(null);
- mCamera.stopPreview();
- mPreviewHandler = null;
- mAutoFocusHandler = null;
- mPreviewing = false;
+ if (camera != null && previewing) {
+ if (!useOneShotPreviewCallback) {
+ camera.setPreviewCallback(null);
+ }
+ camera.stopPreview();
+ previewHandler = null;
+ autoFocusHandler = null;
+ previewing = false;
}
}
* @param message The what field of the message to be sent.
*/
public void requestPreviewFrame(Handler handler, int message) {
- if (mCamera != null && mPreviewing) {
- mPreviewHandler = handler;
- mPreviewMessage = message;
- mCamera.setPreviewCallback(previewCallback);
+ if (camera != null && previewing) {
+ previewHandler = handler;
+ previewMessage = message;
+ if (useOneShotPreviewCallback) {
+ camera.setOneShotPreviewCallback(previewCallback);
+ } else {
+ camera.setPreviewCallback(previewCallback);
+ }
}
}
+ /**
+ * Asks the camera hardware to perform an autofocus.
+ *
+ * @param handler The Handler to notify when the autofocus completes.
+ * @param message The message to deliver.
+ */
public void requestAutoFocus(Handler handler, int message) {
- if (mCamera != null && mPreviewing) {
- mAutoFocusHandler = handler;
- mAutoFocusMessage = message;
- mCamera.autoFocus(autoFocusCallback);
+ if (camera != null && previewing) {
+ autoFocusHandler = handler;
+ autoFocusMessage = message;
+ camera.autoFocus(autoFocusCallback);
}
}
/**
* Calculates the framing rect which the UI should draw to show the user where to place the
- * barcode. The actual captured image should be a bit larger than indicated because they might
- * frame the shot too tightly. This target helps with alignment as well as forces the user to hold
- * the device far enough away to ensure the image will be in focus.
+ * barcode. This target helps with alignment as well as forces the user to hold the device
+ * far enough away to ensure the image will be in focus.
*
* @return The rectangle to draw on screen in window coordinates.
*/
public Rect getFramingRect() {
- if (mFramingRect == null) {
- int size = ((mScreenResolution.x < mScreenResolution.y) ? mScreenResolution.x :
- mScreenResolution.y) * 3 / 4;
- int leftOffset = (mScreenResolution.x - size) / 2;
- int topOffset = (mScreenResolution.y - size) / 2;
- mFramingRect = new Rect(leftOffset, topOffset, leftOffset + size, topOffset + size);
- Log.v(TAG, "Calculated framing rect: " + mFramingRect);
+ if (framingRect == null) {
+ if (camera == null) {
+ return null;
+ }
+ int width = cameraResolution.x * 3 / 4;
+ if (width < MIN_FRAME_WIDTH) {
+ width = MIN_FRAME_WIDTH;
+ } else if (width > MAX_FRAME_WIDTH) {
+ width = MAX_FRAME_WIDTH;
+ }
+ int height = cameraResolution.y * 3 / 4;
+ if (height < MIN_FRAME_HEIGHT) {
+ height = MIN_FRAME_HEIGHT;
+ } else if (height > MAX_FRAME_HEIGHT) {
+ height = MAX_FRAME_HEIGHT;
+ }
+ int leftOffset = (cameraResolution.x - width) / 2;
+ int topOffset = (cameraResolution.y - height) / 2;
+ framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
+ Log.v(TAG, "Calculated framing rect: " + framingRect);
}
- return mFramingRect;
+ return framingRect;
}
/**
}
/**
- * Preview frames are delivered here, which we pass on to the registered handler. Make sure to
- * clear the handler so it will only receive one message.
+ * A factory method to build the appropriate LuminanceSource object based on the format
+ * of the preview buffers, as described by Camera.Parameters.
+ *
+ * @param data A preview frame.
+ * @param width The width of the image.
+ * @param height The height of the image.
+ * @return A PlanarYUVLuminanceSource instance.
*/
- private final Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() {
- public void onPreviewFrame(byte[] data, Camera camera) {
- camera.setPreviewCallback(null);
- if (mPreviewHandler != null) {
- Message message = mPreviewHandler.obtainMessage(mPreviewMessage, mScreenResolution.x,
- mScreenResolution.y, data);
- message.sendToTarget();
- mPreviewHandler = null;
- }
+ public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
+ Rect rect = getFramingRect();
+ switch (previewFormat) {
+ // This is the standard Android format which all devices are REQUIRED to support.
+ // In theory, it's the only one we should ever care about.
+ case PixelFormat.YCbCr_420_SP:
+ return new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top,
+ rect.width(), rect.height());
+ // This format has never been seen in the wild, but is compatible as we only care
+ // about the Y channel, so allow it.
+ case PixelFormat.YCbCr_422_SP:
+ return new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top,
+ rect.width(), rect.height());
+ default:
+ // The Samsung Moment incorrectly uses this variant instead of the 'sp' version.
+ // Fortunately, it too has all the Y data up front, so we can read it.
+ if (previewFormatString.equals("yuv420p")) {
+ return new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top,
+ rect.width(), rect.height());
+ }
}
- };
-
- private final Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() {
- public void onAutoFocus(boolean success, Camera camera) {
- if (mAutoFocusHandler != null) {
- Message message = mAutoFocusHandler.obtainMessage(mAutoFocusMessage, success);
- // Simulate continuous autofocus by sending a focus request every second.
- mAutoFocusHandler.sendMessageDelayed(message, 1000);
- mAutoFocusHandler = null;
- }
- }
- };
+ throw new IllegalArgumentException("Unsupported picture format: " +
+ previewFormat + '/' + previewFormatString);
+ }
/**
- * Sets the camera up to take preview images which are used for both preview and decoding. We're
- * counting on the default YUV420 semi-planar data. If that changes in the future, we'll need to
- * specify it explicitly with setPreviewFormat().
+ * Sets the camera up to take preview images which are used for both preview and decoding.
+ * We detect the preview format here so that buildLuminanceSource() can build an appropriate
+ * LuminanceSource subclass. In the future we may want to force YUV420SP as it's the smallest,
+ * and the planar Y can be used for barcode scanning without a copy in some cases.
*/
private void setCameraParameters() {
- Camera.Parameters parameters = mCamera.getParameters();
- parameters.setPreviewSize(mScreenResolution.x, mScreenResolution.y);
- mCamera.setParameters(parameters);
- Log.v(TAG, "Setting params for preview: width " + mScreenResolution.x + " height " +
- mScreenResolution.y);
+ Camera.Parameters parameters = camera.getParameters();
+ Camera.Size size = parameters.getPreviewSize();
+ Log.v(TAG, "Default preview size: " + size.width + ", " + size.height);
+ previewFormat = parameters.getPreviewFormat();
+ previewFormatString = parameters.get("preview-format");
+ Log.v(TAG, "Default preview format: " + previewFormat + '/' + previewFormatString);
+
+ // Ensure that the camera resolution is a multiple of 8, as the screen may not be.
+ // TODO: A better solution would be to request the supported preview resolutions
+ // and pick the best match, but this parameter is not standardized in Cupcake.
+ cameraResolution = new Point();
+ cameraResolution.x = (screenResolution.x >> 3) << 3;
+ cameraResolution.y = (screenResolution.y >> 3) << 3;
+ Log.v(TAG, "Setting preview size: " + cameraResolution.x + ", " + cameraResolution.y);
+ parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
+
+ // FIXME: This is a hack to turn the flash off on the Samsung Galaxy.
+ parameters.set("flash-value", 2);
+
+ // This is the standard setting to turn the flash off that all devices should honor.
+ parameters.set("flash-mode", "off");
+
+ camera.setParameters(parameters);
}
private Point getScreenResolution() {
- if (mScreenResolution == null) {
- WindowManager manager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ if (screenResolution == null) {
+ WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();
- mScreenResolution = new Point(display.getWidth(), display.getHeight());
+ screenResolution = new Point(display.getWidth(), display.getHeight());
}
- return mScreenResolution;
+ return screenResolution;
}
}