Consolidated all the Android LuminanceSource classes into one file. Either a device...
[zxing.git] / android / src / com / google / zxing / client / android / CameraManager.java
old mode 100644 (file)
new mode 100755 (executable)
index 756e1b0..8b7ae73
@@ -1,5 +1,5 @@
 /*
- * Copyright 2008 ZXing authors
+ * Copyright (C) 2008 ZXing authors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
 
 package com.google.zxing.client.android;
 
+import com.google.zxing.ResultPoint;
+
 import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
+import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.hardware.CameraDevice;
+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 com.tomgibara.android.camera.BitmapCamera;
-import com.tomgibara.android.camera.CameraSource;
+
+import java.io.IOException;
 
 /**
- * This object wraps the CameraDevice and expects to be the only one talking to it. The
- * implementation encapsulates the steps needed to take preview-sized images and well as high
- * resolution stills.
+ * 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 cameraResolution;
-  private Point stillResolution;
-  private Point previewResolution;
-  private int stillMultiplier;
   private Point screenResolution;
+  private Point cameraResolution;
   private Rect framingRect;
-  private Bitmap bitmap;
-  // TODO switch back to CameraDevice later
-  // private CameraDevice camera;
-  private CameraSource cameraSource;
-  // end TODO
-  private final CameraDevice.CaptureParams params;
-  private boolean previewMode;
-  private boolean usePreviewForDecode;
+  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;
 
-  CameraManager(Context context) {
-    this.context = context;
-    getScreenResolution();
-    calculateStillResolution();
-    calculatePreviewResolution();
+  /**
+   * 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;
+      }
+    }
+  };
 
-    usePreviewForDecode = true;
-    setUsePreviewForDecode(false);
+  /**
+   * 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;
+      }
+    }
+  };
 
-    // TODO switch back to CameraDevice later
-    // camera = null;
-    Bitmap fakeBitmap = BitmapFactory.decodeFile("/tmp/barcode.jpg");
-    if (fakeBitmap == null) {
-      throw new RuntimeException("/tmp/barcode.jpg was not found");
+  /**
+   * 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 (cameraManager == null) {
+      cameraManager = new CameraManager(context);
     }
-    cameraSource = new BitmapCamera(fakeBitmap, stillResolution.x, stillResolution.y);
-    // end TODO
+  }
+
+  /**
+   * Gets the CameraManager singleton instance.
+   *
+   * @return A reference to the CameraManager singleton.
+   */
+  public static CameraManager get() {
+    return cameraManager;
+  }
+
+  private CameraManager(Context context) {
+    this.context = context;
+    camera = null;
+    initialized = false;
+    previewing = false;
 
-    params = new CameraDevice.CaptureParams();
+    // 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() {
-//    TODO switch back to CameraDevice later
-//    if (camera == null) {
-//      camera = CameraDevice.open();
-//      // If we're reopening the camera, we need to reset the capture params.
-//      previewMode = false;
-//      setPreviewMode(true);
-//    }
-//    end TODO
+  /**
+   * 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 (!initialized) {
+        initialized = true;
+        getScreenResolution();
+      }
+
+      setCameraParameters();
+    }
   }
 
+  /**
+   * Closes the camera driver if still in use.
+   */
   public void closeDriver() {
-    // TODO switch back to CameraDevice later
-    // if (camera != null) {
-    //   camera.close();
-    //   camera = null;
-    // }
-    // end TODO
+    if (camera != null) {
+      camera.release();
+      camera = null;
+    }
   }
 
-  public void capturePreview(Canvas canvas) {
-    setPreviewMode(true);
-    // TODO switch back to CameraDevice later
-    // camera.capture(canvas);
-    cameraSource.capture(canvas);
-    // end TODO
+  /**
+   * Asks the camera hardware to begin drawing preview frames to the screen.
+   */
+  public void startPreview() {
+    if (camera != null && !previewing) {
+      camera.startPreview();
+      previewing = true;
+    }
   }
 
-  public Bitmap captureStill() {
-    setPreviewMode(usePreviewForDecode);
-    Canvas canvas = new Canvas(bitmap);
-    // TODO switch back to CameraDevice later
-    // camera.capture(canvas);
-    cameraSource.capture(canvas);
-    // end TODO
-    return bitmap;
+  /**
+   * Tells the camera to stop drawing preview frames.
+   */
+  public void stopPreview() {
+    if (camera != null && previewing) {
+      if (!useOneShotPreviewCallback) {
+        camera.setPreviewCallback(null);
+      }
+      camera.stopPreview();
+      previewHandler = null;
+      autoFocusHandler = null;
+      previewing = false;
+    }
   }
 
   /**
-   * This method exists to help us evaluate how to best set up and use the camera.
-   * @param usePreview Decode at preview resolution if true, else use still resolution.
+   * A single preview frame will be returned to the handler supplied. The data will arrive as byte[]
+   * in the message.obj field, with width and height encoded as message.arg1 and message.arg2,
+   * respectively.
+   *
+   * @param handler The handler to send the message to.
+   * @param message The what field of the message to be sent.
    */
-  public void setUsePreviewForDecode(boolean usePreview) {
-    if (usePreviewForDecode != usePreview) {
-      usePreviewForDecode = usePreview;
-      if (usePreview) {
-        Log.v(TAG, "Creating bitmap at screen resolution: " + screenResolution.x + "," +
-            screenResolution.y);
-        bitmap = Bitmap.createBitmap(screenResolution.x, screenResolution.y, false);
+  public void requestPreviewFrame(Handler handler, int message) {
+    if (camera != null && previewing) {
+      previewHandler = handler;
+      previewMessage = message;
+      if (useOneShotPreviewCallback) {
+        camera.setOneShotPreviewCallback(previewCallback);
       } else {
-        Log.v(TAG, "Creating bitmap at still resolution: " + stillResolution.x + "," +
-            stillResolution.y);
-        bitmap = Bitmap.createBitmap(stillResolution.x, stillResolution.y, false);
+        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 (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 (framingRect == null) {
-      int size = stillResolution.x * screenResolution.x / previewResolution.x;
-      int leftOffset = (screenResolution.x - size) / 2;
-      int topOffset = (screenResolution.y - size) / 2;
-      framingRect = new Rect(leftOffset, topOffset, leftOffset + size, topOffset + size);
+      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 framingRect;
@@ -163,140 +262,80 @@ final class CameraManager {
    */
   public Point[] convertResultPoints(ResultPoint[] points) {
     Rect frame = getFramingRect();
-    int frameSize = frame.width();
     int count = points.length;
     Point[] output = new Point[count];
     for (int x = 0; x < count; x++) {
       output[x] = new Point();
-      if (usePreviewForDecode) {
-        output[x].x = (int) (points[x].getX() + 0.5f);
-        output[x].y = (int) (points[x].getY() + 0.5f);
-      } else {
-        output[x].x = frame.left + (int) (points[x].getX() * frameSize / stillResolution.x + 0.5f);
-        output[x].y = frame.top + (int) (points[x].getY() * frameSize / stillResolution.y + 0.5f);
-      }
+      output[x].x = frame.left + (int) (points[x].getX() + 0.5f);
+      output[x].y = frame.top + (int) (points[x].getY() + 0.5f);
     }
     return output;
   }
 
   /**
-   * Images for the live preview are taken at low resolution in RGB. Other code depends
-   * on the ability to call this method for free if the correct mode is already set.
+   * A factory method to build the appropriate LuminanceSource object based on the format
+   * of the preview buffers, as described by Camera.Parameters.
    *
-   * @param on Setting on true will engage preview mode, setting it false will request still mode.
+   * @param data A preview frame.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @return A PlanarYUVLuminanceSource instance.
    */
-  private void setPreviewMode(boolean on) {
-    if (on != previewMode) {
-      if (on) {
-        params.type = 1; // preview
-        params.srcWidth = previewResolution.x;
-        params.srcHeight = previewResolution.y;
-        params.leftPixel = (cameraResolution.x - params.srcWidth) / 2;
-        params.topPixel = (cameraResolution.y - params.srcHeight) / 2;
-        params.outputWidth = screenResolution.x;
-        params.outputHeight = screenResolution.y;
-        params.dataFormat = 2; // RGB565
-      } else {
-        params.type = 0; // still
-        params.srcWidth = stillResolution.x * stillMultiplier;
-        params.srcHeight = stillResolution.y * stillMultiplier;
-        params.leftPixel = (cameraResolution.x - params.srcWidth) / 2;
-        params.topPixel = (cameraResolution.y - params.srcHeight) / 2;
-        params.outputWidth = stillResolution.x;
-        params.outputHeight = stillResolution.y;
-        params.dataFormat = 2; // RGB565
-      }
-      String captureType = on ? "preview" : "still";
-      Log.v(TAG, "Setting params for " + captureType + ": srcWidth " + params.srcWidth +
-          " srcHeight " + params.srcHeight + " leftPixel " + params.leftPixel + " topPixel " +
-          params.topPixel + " outputWidth " + params.outputWidth + " outputHeight " +
-          params.outputHeight);
-      // TODO switch back to CameraDevice later
-      // camera.setCaptureParams(params);
-      // end TODO
-      previewMode = on;
+  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());
+        }
     }
+    throw new IllegalArgumentException("Unsupported picture format: " +
+        previewFormat + '/' + previewFormatString);
   }
 
   /**
-   * This method determines how to take the highest quality image (i.e. the one which has the best
-   * chance of being decoded) given the capabilities of the camera. It is a balancing act between
-   * having enough resolution to read UPCs and having few enough pixels to keep the QR Code
-   * processing fast. The result is the dimensions of the rectangle to capture from the center of
-   * the sensor, plus a stillMultiplier which indicates whether we'll ask the driver to downsample
-   * for us. This has the added benefit of keeping the memory footprint of the bitmap as small as
-   * possible.
+   * 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 calculateStillResolution() {
-    cameraResolution = getMaximumCameraResolution();
-    int minDimension = (cameraResolution.x < cameraResolution.y) ? cameraResolution.x :
-        cameraResolution.y;
-    int diagonalResolution = (int) Math.sqrt(cameraResolution.x * cameraResolution.x +
-        cameraResolution.y * cameraResolution.y);
-    float diagonalFov = getFieldOfView();
-
-    // Determine the field of view in the smaller dimension, then calculate how large an object
-    // would be at the minimum focus distance.
-    float fov = diagonalFov * minDimension / diagonalResolution;
-    double objectSize = Math.tan(Math.toRadians(fov / 2.0)) * getMinimumFocusDistance() * 2;
-
-    // Let's assume the largest barcode we might photograph at this distance is 3 inches across. By
-    // cropping to this size, we can avoid processing surrounding pixels, which helps with speed and
-    // accuracy. 
-    // TODO(dswitkin): Handle a device with a great macro mode where objectSize < 4 inches.
-    double crop = 3.0 / objectSize;
-    int nativeResolution = (int) (minDimension * crop);
-
-    // The camera driver can only capture images which are a multiple of eight, so it's necessary to
-    // round up.
-    nativeResolution = ((nativeResolution + 7) >> 3) << 3;
-    if (nativeResolution > minDimension) {
-      nativeResolution = minDimension;
-    }
+  private void setCameraParameters() {
+    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);
 
-    // There's no point in capturing too much detail, so ask the driver to downsample. I haven't
-    // tried a non-integer multiple, but it seems unlikely to work.
-    double dpi = nativeResolution / objectSize;
-    stillMultiplier = 1;
-    if (dpi > 200) {
-      stillMultiplier = (int) (dpi / 200 + 1);
-    }
-    stillResolution = new Point(nativeResolution, nativeResolution);
-    Log.v(TAG, "FOV " + fov + " objectSize " + objectSize + " crop " + crop + " dpi " + dpi +
-        " nativeResolution " + nativeResolution + " stillMultiplier " + stillMultiplier);
-  }
+    // 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);
 
-  /**
-   * The goal of the preview resolution is to show a little context around the framing rectangle
-   * which is the actual captured area in still mode.
-   */
-  private void calculatePreviewResolution() {
-    if (previewResolution == null) {
-      int previewHeight = (int) (stillResolution.x * stillMultiplier * 1.5f);
-      int previewWidth = previewHeight * screenResolution.x / screenResolution.y;
-      previewWidth = ((previewWidth + 7) >> 3) << 3;
-      if (previewWidth > cameraResolution.x) previewWidth = cameraResolution.x;
-      previewHeight = previewWidth * screenResolution.y / screenResolution.x;
-      previewResolution = new Point(previewWidth, previewHeight);
-      Log.v(TAG, "previewWidth " + previewWidth + " previewHeight " + previewHeight);
-    }
-  }
-
-  // FIXME(dswitkin): These three methods have temporary constants until the new Camera API can
-  // provide the real values for the current device.
-  // Temporary: the camera's maximum resolution in pixels.
-  private static Point getMaximumCameraResolution() {
-    return new Point(1280, 1024);
-  }
+    // FIXME: This is a hack to turn the flash off on the Samsung Galaxy.
+    parameters.set("flash-value", 2);
 
-  // Temporary: the diagonal field of view in degrees.
-  private static float getFieldOfView() {
-    return 60.0f;
-  }
+    // This is the standard setting to turn the flash off that all devices should honor.
+    parameters.set("flash-mode", "off");
 
-  // Temporary: the minimum focus distance in inches.
-  private static float getMinimumFocusDistance() {
-    return 6.0f;
+    camera.setParameters(parameters);
   }
 
   private Point getScreenResolution() {