diff --git a/android-m3/AndroidManifest.xml b/android-m3/AndroidManifest.xml new file mode 100644 index 000000000..8f925eb85 --- /dev/null +++ b/android-m3/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-m3/build.xml b/android-m3/build.xml new file mode 100644 index 000000000..57b6d5379 --- /dev/null +++ b/android-m3/build.xml @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Generating R.java... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Packaging resources and assets... + + + + + + + + + + + + + + + + + + + Packaging resources... + + + + + + + + + + + + + + + + + + + + + + + + + + Packaging java... + + + + + + Packaging dex... + + + + + + + + + + + + + + + diff --git a/android-m3/res/drawable/icon.png b/android-m3/res/drawable/icon.png new file mode 100644 index 000000000..ee33194c0 Binary files /dev/null and b/android-m3/res/drawable/icon.png differ diff --git a/android-m3/res/layout/main.xml b/android-m3/res/layout/main.xml new file mode 100644 index 000000000..a841f282c --- /dev/null +++ b/android-m3/res/layout/main.xml @@ -0,0 +1,21 @@ + + + + diff --git a/android-m3/res/values/ids.xml b/android-m3/res/values/ids.xml new file mode 100644 index 000000000..776e72925 --- /dev/null +++ b/android-m3/res/values/ids.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/android-m3/src/com/google/zxing/client/android/AndroidGraphicsGridSampler.java b/android-m3/src/com/google/zxing/client/android/AndroidGraphicsGridSampler.java new file mode 100755 index 000000000..9aa3f78af --- /dev/null +++ b/android-m3/src/com/google/zxing/client/android/AndroidGraphicsGridSampler.java @@ -0,0 +1,82 @@ +/* + * Copyright 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.graphics.Matrix; +import com.google.zxing.MonochromeBitmapSource; +import com.google.zxing.ReaderException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.GridSampler; + +/** + * Implementation based on Android's + * {@link Matrix#setPolyToPoly(float[], int, float[], int, int)} + * class, which should offer faster performance for these matrix + * operations. + * + * @author srowen@google.com (Sean Owen) + */ +public final class AndroidGraphicsGridSampler extends GridSampler { + + @Override + public BitMatrix sampleGrid(MonochromeBitmapSource image, + int dimension, + float p1ToX, float p1ToY, + float p2ToX, float p2ToY, + float p3ToX, float p3ToY, + float p4ToX, float p4ToY, + float p1FromX, float p1FromY, + float p2FromX, float p2FromY, + float p3FromX, float p3FromY, + float p4FromX, float p4FromY) throws ReaderException { + + Matrix transformMatrix = new Matrix(); + boolean succeeded = transformMatrix.setPolyToPoly( + new float[] { p1FromX, p1FromY, p2FromX, p2FromY, p3FromX, p3FromY, p4FromX, p4FromY }, + 0, + new float[] { p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY }, + 0, + 4 + ); + if (!succeeded) { + throw new ReaderException("Could not establish transformation matrix"); + } + + BitMatrix bits = new BitMatrix(dimension); + float[] points = new float[dimension << 1]; + for (int i = 0; i < dimension; i++) { + int max = points.length; + float iValue = (float) i + 0.5f; + for (int j = 0; j < max; j += 2) { + points[j] = (float) (j >> 1) + 0.5f; + points[j + 1] = iValue; + } + transformMatrix.mapPoints(points); + // Quick check to see if points transformed to something inside the image; + // sufficent to check the endpoints + checkAndNudgePoints(image, points); + for (int j = 0; j < max; j += 2) { + if (image.isBlack((int) points[j], (int) points[j + 1])) { + // Black(-ish) pixel + bits.set(i, j >> 1); + } + } + } + return bits; + } + +} diff --git a/android-m3/src/com/google/zxing/client/android/AndroidIntentParsedResult.java b/android-m3/src/com/google/zxing/client/android/AndroidIntentParsedResult.java new file mode 100644 index 000000000..471410c82 --- /dev/null +++ b/android-m3/src/com/google/zxing/client/android/AndroidIntentParsedResult.java @@ -0,0 +1,59 @@ +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.content.Intent; +import com.google.zxing.client.result.ParsedReaderResult; +import com.google.zxing.client.result.ParsedReaderResultType; + +import java.net.URISyntaxException; + +/** + * A {@link ParsedReaderResult} derived from a URI that encodes an Android + * {@link Intent}, and which should presumably trigger that intent on Android. + * + * @author srowen@google.com (Sean Owen) + */ +public final class AndroidIntentParsedResult extends ParsedReaderResult { + + private final Intent intent; + + private AndroidIntentParsedResult(Intent intent) { + super(ParsedReaderResultType.ANDROID_INTENT); + this.intent = intent; + } + + public static AndroidIntentParsedResult parse(String rawText) { + try { + return new AndroidIntentParsedResult(Intent.getIntent(rawText)); + } catch (URISyntaxException urise) { + return null; + } catch (IllegalArgumentException iae) { + return null; + } + } + + public Intent getIntent() { + return intent; + } + + @Override + public String getDisplayResult() { + return intent.toString(); + } + +} \ No newline at end of file diff --git a/android-m3/src/com/google/zxing/client/android/BarcodeReaderCaptureActivity.java b/android-m3/src/com/google/zxing/client/android/BarcodeReaderCaptureActivity.java new file mode 100644 index 000000000..31966552d --- /dev/null +++ b/android-m3/src/com/google/zxing/client/android/BarcodeReaderCaptureActivity.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.Window; +import android.view.WindowManager.LayoutParams; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.client.result.ParsedReaderResult; +import com.google.zxing.client.result.ParsedReaderResultType; + +/** + * The barcode reader activity itself. This is loosely based on the CameraPreview + * example included in the Android SDK. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Android Team (for CameraPreview example) + */ +public final class BarcodeReaderCaptureActivity extends Activity { + + private CameraManager cameraManager; + private CameraSurfaceView surfaceView; + private WorkerThread workerThread; + + private static final int ABOUT_ID = Menu.FIRST; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + // Make sure to create a TRANSLUCENT window. This is required for SurfaceView to work. + // Eventually this'll be done by the system automatically. + getWindow().setAttributes(new LayoutParams(LayoutParams.APPLICATION_TYPE, + LayoutParams.NO_STATUS_BAR_FLAG)); + getWindow().setFormat(PixelFormat.TRANSLUCENT); + + cameraManager = new CameraManager(getApplication()); + surfaceView = new CameraSurfaceView(getApplication(), cameraManager); + setContentView(surfaceView); + workerThread = new WorkerThread(surfaceView, cameraManager, messageHandler); + workerThread.requestPreviewLoop(); + workerThread.start(); + + // TODO re-enable this when issues with Matrix.setPolyToPoly() are resolved + //GridSampler.setGridSampler(new AndroidGraphicsGridSampler()); + } + + @Override + protected boolean isFullscreenOpaque() { + // Our main window is set to translucent, but we know that we will + // fill it with opaque data. Tell the system that so it can perform + // some important optimizations. + return true; + } + + @Override + protected void onResume() { + super.onResume(); + cameraManager.openDriver(); + if (workerThread == null) { + workerThread = new WorkerThread(surfaceView, cameraManager, messageHandler); + workerThread.requestPreviewLoop(); + workerThread.start(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (workerThread != null) { + workerThread.requestExitAndWait(); + workerThread = null; + } + cameraManager.closeDriver(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + workerThread.requestStillAndDecode(); + return true; + } else { + return super.onKeyDown(keyCode, event); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + menu.add(0, ABOUT_ID, R.string.menu_about); + return true; + } + + @Override + public boolean onOptionsItemSelected(Menu.Item item) { + switch (item.getId()) { + case ABOUT_ID: + Context context = getApplication(); + showAlert(context.getString(R.string.title_about), + context.getString(R.string.msg_about), + context.getString(R.string.button_ok), null, true, null); + break; + } + return super.onOptionsItemSelected(item); + } + + private final Handler messageHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case R.id.decoding_succeeded_message: + handleDecode((Result) message.obj); + break; + case R.id.decoding_failed_message: + Context context = getApplication(); + showAlert(context.getString(R.string.title_no_barcode_detected), + context.getString(R.string.msg_no_barcode_detected), + context.getString(R.string.button_ok), null, true, null); + break; + } + } + }; + + public void restartPreview() { + workerThread.requestPreviewLoop(); + } + + // TODO(dswitkin): These deprecated showAlert calls need to be updated. + private void handleDecode(Result rawResult) { + ResultPoint[] points = rawResult.getResultPoints(); + if (points != null && points.length > 0) { + surfaceView.drawResultPoints(points); + } + + Context context = getApplication(); + ParsedReaderResult readerResult = parseReaderResult(rawResult); + ResultHandler handler = new ResultHandler(this, readerResult); + if (handler.getIntent() != null) { + // Can be handled by some external app; ask if the user wants to + // proceed first though + Message yesMessage = handler.obtainMessage(R.string.button_yes); + Message noMessage = handler.obtainMessage(R.string.button_no); + showAlert(context.getString(getDialogTitleID(readerResult.getType())), + readerResult.getDisplayResult(), context.getString(R.string.button_yes), + yesMessage, context.getString(R.string.button_no), noMessage, true, noMessage); + } else { + // Just show information to user + Message okMessage = handler.obtainMessage(R.string.button_ok); + showAlert(context.getString(R.string.title_barcode_detected), + readerResult.getDisplayResult(), context.getString(R.string.button_ok), okMessage, null, + null, true, okMessage); + } + } + + private static ParsedReaderResult parseReaderResult(Result rawResult) { + ParsedReaderResult readerResult = ParsedReaderResult.parseReaderResult(rawResult); + if (readerResult.getType().equals(ParsedReaderResultType.TEXT)) { + String rawText = rawResult.getText(); + AndroidIntentParsedResult androidResult = AndroidIntentParsedResult.parse(rawText); + if (androidResult != null) { + Intent intent = androidResult.getIntent(); + if (!Intent.VIEW_ACTION.equals(intent.getAction())) { + // For now, don't take anything that just parses as a View action. A lot + // of things are accepted as a View action by default. + readerResult = androidResult; + } + } + } + return readerResult; + } + + private static int getDialogTitleID(ParsedReaderResultType type) { + if (type.equals(ParsedReaderResultType.ADDRESSBOOK)) { + return R.string.title_add_contact; + } else if (type.equals(ParsedReaderResultType.URI) || + type.equals(ParsedReaderResultType.BOOKMARK) || + type.equals(ParsedReaderResultType.URLTO)) { + return R.string.title_open_url; + } else if (type.equals(ParsedReaderResultType.EMAIL) || + type.equals(ParsedReaderResultType.EMAIL_ADDRESS)) { + return R.string.title_compose_email; + } else if (type.equals(ParsedReaderResultType.UPC)) { + return R.string.title_lookup_barcode; + } else if (type.equals(ParsedReaderResultType.TEL)) { + return R.string.title_dial; + } else if (type.equals(ParsedReaderResultType.GEO)) { + return R.string.title_view_maps; + } else { + return R.string.title_barcode_detected; + } + } + +} \ No newline at end of file diff --git a/android-m3/src/com/google/zxing/client/android/CameraManager.java b/android-m3/src/com/google/zxing/client/android/CameraManager.java new file mode 100644 index 000000000..0cd3782f6 --- /dev/null +++ b/android-m3/src/com/google/zxing/client/android/CameraManager.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.CameraDevice; +import android.util.Log; +import android.view.Display; +import android.view.WindowManager; +import com.google.zxing.ResultPoint; + +/** + * 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. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class CameraManager { + + private static final String TAG = "CameraManager"; + + private final Context context; + private Point cameraResolution; + private Point stillResolution; + private int stillMultiplier; + private Point screenResolution; + private Rect framingRect; + private final Bitmap bitmap; + private CameraDevice camera; + private final CameraDevice.CaptureParams params; + private boolean previewMode; + + CameraManager(Context context) { + this.context = context; + calculateStillResolution(); + getScreenResolution(); + bitmap = Bitmap.createBitmap(stillResolution.x, stillResolution.y, false); + camera = CameraDevice.open(); + params = new CameraDevice.CaptureParams(); + previewMode = false; + setPreviewMode(true); + } + + public void openDriver() { + if (camera == null) { + camera = CameraDevice.open(); + } + } + + public void closeDriver() { + if (camera != null) { + camera.close(); + camera = null; + } + } + + public void capturePreview(Canvas canvas) { + setPreviewMode(true); + camera.capture(canvas); + } + + public Bitmap captureStill() { + setPreviewMode(false); + Canvas canvas = new Canvas(bitmap); + camera.capture(canvas); + return bitmap; + } + + /** + * 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. + * + * @return The rectangle to draw on screen in window coordinates. + */ + public Rect getFramingRect() { + if (framingRect == null) { + int size = stillResolution.x * screenResolution.x / cameraResolution.x; + int leftOffset = (screenResolution.x - size) / 2; + int topOffset = (screenResolution.y - size) / 2; + framingRect = new Rect(leftOffset, topOffset, leftOffset + size, topOffset + size); + } + return framingRect; + } + + /** + * Converts the result points from still resolution coordinates to screen coordinates. + * + * @param points The points returned by the Reader subclass through Result.getResultPoints(). + * @return An array of Points scaled to the size of the framing rect and offset appropriately + * so they can be drawn in screen coordinates. + */ + 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(); + 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); + } + return output; + } + + /** + * Images for the live preview are taken at low resolution in RGB. The final stills for the + * decoding step are taken in YUV, since we only need the luminance channel. Other code depends + * on the ability to call this method for free if the correct mode is already set. + * + * @param on Setting on true will engage preview mode, setting it false will request still mode. + */ + private void setPreviewMode(boolean on) { + if (on != previewMode) { + if (on) { + params.type = 1; // preview + if (cameraResolution.x / (float) cameraResolution.y < + screenResolution.x / (float) screenResolution.y) { + params.srcWidth = cameraResolution.x; + params.srcHeight = cameraResolution.x * screenResolution.y / screenResolution.x; + params.leftPixel = 0; + params.topPixel = (cameraResolution.y - params.srcHeight) / 2; + } else { + params.srcWidth = cameraResolution.y * screenResolution.x / screenResolution.y; + params.srcHeight = cameraResolution.y; + params.leftPixel = (cameraResolution.x - params.srcWidth) / 2; + params.topPixel = 0; + } + 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); + camera.setCaptureParams(params); + previewMode = on; + } + } + + /** + * 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. + */ + 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; + } + + // 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); + } + + // 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); + } + + // Temporary: the diagonal field of view in degrees. + private static float getFieldOfView() { + return 60.0f; + } + + // Temporary: the minimum focus distance in inches. + private static float getMinimumFocusDistance() { + return 12.0f; + } + + private Point getScreenResolution() { + if (screenResolution == null) { + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = manager.getDefaultDisplay(); + screenResolution = new Point(display.getWidth(), display.getHeight()); + } + return screenResolution; + } + +} diff --git a/android-m3/src/com/google/zxing/client/android/CameraSurfaceView.java b/android-m3/src/com/google/zxing/client/android/CameraSurfaceView.java new file mode 100644 index 000000000..0ab8d2b71 --- /dev/null +++ b/android-m3/src/com/google/zxing/client/android/CameraSurfaceView.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import com.google.zxing.ResultPoint; + +/** + * @author dswitkin@google.com (Daniel Switkin) + */ +final class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback { + + private static final int[] SCANNER_ALPHA = {0, 64, 128, 192, 255, 192, 128, 64}; + + private final CameraManager cameraManager; + private final SurfaceHolder surfaceHolder; + private boolean hasSurface; + private int scannerAlpha; + + CameraSurfaceView(Context context, CameraManager cameraManager) { + super(context); + this.cameraManager = cameraManager; + + // Install a SurfaceHolder.Callback so we get notified when the underlying surface is created + // and destroyed. + surfaceHolder = getHolder(); + surfaceHolder.setCallback(this); + hasSurface = false; + scannerAlpha = 0; + surfaceHolder.setSizeFromLayout(); + } + + public boolean surfaceCreated(SurfaceHolder holder) { + hasSurface = true; + + // Tell the system that we filled the surface in this call. This is a lie to prevent the system + // from filling the surface for us automatically. THIS IS REQUIRED because otherwise we'll + // access the Surface object from 2 different threads which is not allowed. + return true; + } + + public void surfaceDestroyed(SurfaceHolder holder) { + // FIXME(dswitkin): The docs say this surface will be destroyed when this method returns. In + // practice this has not been a problem so far. I need to investigate. + hasSurface = false; + } + + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + // Surface size or format has changed. This won't happen because of the setFixedSize() call. + } + + /** + * This method is only called from the WorkerThread. It's job is to grab the next preview frame + * from the camera, draw the framing rectangle, and blit everything to the screen. + */ + public void capturePreviewAndDraw() { + if (hasSurface) { + Canvas canvas = surfaceHolder.lockCanvas(); + cameraManager.capturePreview(canvas); + Rect frame = cameraManager.getFramingRect(); + int width = canvas.getBitmapWidth(); + int height = canvas.getBitmapHeight(); + + // Draw the exterior (i.e. outside the framing rect) as half darkened + Paint paint = new Paint(); + paint.setColor(Color.BLACK); + paint.setAlpha(96); + Rect box = new Rect(0, 0, width, frame.top); + canvas.drawRect(box, paint); + box.set(0, frame.top, frame.left, frame.bottom + 1); + canvas.drawRect(box, paint); + box.set(frame.right + 1, frame.top, width, frame.bottom + 1); + canvas.drawRect(box, paint); + box.set(0, frame.bottom + 1, width, height); + canvas.drawRect(box, paint); + + // Draw a two pixel solid black border inside the framing rect + paint.setAlpha(255); + box.set(frame.left, frame.top, frame.right + 1, frame.top + 2); + canvas.drawRect(box, paint); + box.set(frame.left, frame.top + 2, frame.left + 2, frame.bottom - 1); + canvas.drawRect(box, paint); + box.set(frame.right - 1, frame.top, frame.right + 1, frame.bottom - 1); + canvas.drawRect(box, paint); + box.set(frame.left, frame.bottom - 1, frame.right + 1, frame.bottom + 1); + canvas.drawRect(box, paint); + + // Draw a red "laser scanner" line through the middle + paint.setColor(Color.RED); + paint.setAlpha(SCANNER_ALPHA[scannerAlpha]); + int middle = frame.height() / 2 + frame.top; + box.set(frame.left + 2, middle - 1, frame.right - 1, middle + 2); + canvas.drawRect(box, paint); + + surfaceHolder.unlockCanvasAndPost(canvas); + + // This cheap animation is tied to the rate at which we pull previews from the camera. + scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length; + } + } + + /** + * Draw a line for 1D barcodes (which return two points) or otherwise a set of points returned + * from the decoder to indicate what we found. + * TODO(dswitkin): It might be nice to clear the framing rect and zoom in on the actual still that + * was captured, then paint the green points on it. This would also clear the red scanner line + * which doesn't make sense after the capture. + * + * @param resultPoints An array of points from the decoder, whose coordinates are expressed + * relative to the still image from the camera. + */ + public void drawResultPoints(ResultPoint[] resultPoints) { + if (hasSurface) { + Canvas canvas = surfaceHolder.lockCanvas(); + Paint paint = new Paint(); + paint.setColor(Color.GREEN); + paint.setAlpha(128); + + Point[] points = cameraManager.convertResultPoints(resultPoints); + if (points.length == 2) { + paint.setStrokeWidth(4); + canvas.drawLine(points[0].x, points[0].y, points[1].x, points[1].y, paint); + } else { + paint.setStrokeWidth(10); + for (int x = 0; x < points.length; x++) { + canvas.drawPoint(points[x].x, points[x].y, paint); + } + } + + surfaceHolder.unlockCanvasAndPost(canvas); + } + } + +} diff --git a/android-m3/src/com/google/zxing/client/android/RGBMonochromeBitmapSource.java b/android-m3/src/com/google/zxing/client/android/RGBMonochromeBitmapSource.java new file mode 100755 index 000000000..6d3f2bd8f --- /dev/null +++ b/android-m3/src/com/google/zxing/client/android/RGBMonochromeBitmapSource.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.graphics.Bitmap; +import com.google.zxing.BlackPointEstimationMethod; +import com.google.zxing.MonochromeBitmapSource; +import com.google.zxing.ReaderException; +import com.google.zxing.common.BitArray; +import com.google.zxing.common.BlackPointEstimator; + +/** + * This object implements MonochromeBitmapSource around an Android Bitmap. Rather than capturing an + * RGB image and calculating the grey value at each pixel, we ask the camera driver for YUV data and + * strip out the luminance channel directly. This should be faster but provides fewer bits, i.e. + * fewer grey levels. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author srowen@google.com (Sean Owen) + */ +final class RGBMonochromeBitmapSource implements MonochromeBitmapSource { + + private final Bitmap image; + private int blackPoint; + private BlackPointEstimationMethod lastMethod; + private int lastArgument; + + private static final int LUMINANCE_BITS = 5; + private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS; + private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS; + + RGBMonochromeBitmapSource(Bitmap image) { + this.image = image; + blackPoint = 0x7F; + lastMethod = null; + lastArgument = 0; + } + + public boolean isBlack(int x, int y) { + return computeRGBLuminance(image.getPixel(x, y)) < blackPoint; + } + + public BitArray getBlackRow(int y, BitArray row, int startX, int getWidth) { + if (row == null) { + row = new BitArray(getWidth); + } else { + row.clear(); + } + int[] pixelRow = new int[getWidth]; + image.getPixels(pixelRow, 0, getWidth, startX, y, getWidth, 1); + for (int i = 0; i < getWidth; i++) { + if (computeRGBLuminance(pixelRow[i]) < blackPoint) { + row.set(i); + } + } + return row; + } + + public int getHeight() { + return image.height(); + } + + public int getWidth() { + return image.width(); + } + + public void estimateBlackPoint(BlackPointEstimationMethod method, int argument) throws ReaderException { + if (!method.equals(lastMethod) || argument != lastArgument) { + int width = image.width(); + int height = image.height(); + int[] histogram = new int[LUMINANCE_BUCKETS]; + if (method.equals(BlackPointEstimationMethod.TWO_D_SAMPLING)) { + int minDimension = width < height ? width : height; + int startI = height == minDimension ? 0 : (height - width) >> 1; + int startJ = width == minDimension ? 0 : (width - height) >> 1; + for (int n = 0; n < minDimension; n++) { + int pixel = image.getPixel(startJ + n, startI + n); + histogram[computeRGBLuminance(pixel) >> LUMINANCE_SHIFT]++; + } + } else if (method.equals(BlackPointEstimationMethod.ROW_SAMPLING)) { + if (argument < 0 || argument >= height) { + throw new IllegalArgumentException("Row is not within the image: " + argument); + } + int[] pixelRow = new int[width]; + image.getPixels(pixelRow, 0, width, 0, argument, width, 1); + for (int x = 0; x < width; x++) { + histogram[computeRGBLuminance(pixelRow[x]) >> LUMINANCE_SHIFT]++; + } + } else { + throw new IllegalArgumentException("Unknown method: " + method); + } + blackPoint = BlackPointEstimator.estimate(histogram) << LUMINANCE_SHIFT; + lastMethod = method; + lastArgument = argument; + } + } + + public BlackPointEstimationMethod getLastEstimationMethod() { + return lastMethod; + } + + public MonochromeBitmapSource rotateCounterClockwise() { + throw new IllegalStateException("Rotate not supported"); + } + + public boolean isRotateSupported() { + return false; + } + + /** + * An optimized approximation of a more proper conversion from RGB to luminance which + * only uses shifts. See BufferedImageMonochromeBitmapSource for an original version. + */ + private static int computeRGBLuminance(int pixel) { + // Instead of multiplying by 306, 601, 117, we multiply by 256, 512, 256, so that + // the multiplies can be implemented as shifts. + // + // Really, it's: + // + // return ((((pixel >> 16) & 0xFF) << 8) + + // (((pixel >> 8) & 0xFF) << 9) + + // (( pixel & 0xFF) << 8)) >> 10; + // + // That is, we're replacing the coefficients in the original with powers of two, + // which can be implemented as shifts, even though changing the coefficients slightly + // corrupts the conversion. Not significant for our purposes. + // + // But we can get even cleverer and eliminate a few shifts: + return (((pixel & 0x00FF0000) >> 8) + + ((pixel & 0x0000FF00) << 1) + + ((pixel & 0x000000FF) << 8)) >> 10; + } + +} \ No newline at end of file diff --git a/android-m3/src/com/google/zxing/client/android/ResultHandler.java b/android-m3/src/com/google/zxing/client/android/ResultHandler.java new file mode 100755 index 000000000..cfe4df607 --- /dev/null +++ b/android-m3/src/com/google/zxing/client/android/ResultHandler.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.content.Intent; +import android.net.ContentURI; +import android.os.Handler; +import android.os.Message; +import android.provider.Contacts; +import com.google.zxing.client.result.AddressBookAUParsedResult; +import com.google.zxing.client.result.AddressBookDoCoMoParsedResult; +import com.google.zxing.client.result.BookmarkDoCoMoParsedResult; +import com.google.zxing.client.result.EmailAddressParsedResult; +import com.google.zxing.client.result.EmailDoCoMoParsedResult; +import com.google.zxing.client.result.GeoParsedResult; +import com.google.zxing.client.result.ParsedReaderResult; +import com.google.zxing.client.result.ParsedReaderResultType; +import com.google.zxing.client.result.TelParsedResult; +import com.google.zxing.client.result.UPCParsedResult; +import com.google.zxing.client.result.URIParsedResult; +import com.google.zxing.client.result.URLTOParsedResult; + +import java.net.URISyntaxException; + +/** + * Handles the result of barcode decoding in the context of the Android platform, + * by dispatching the proper intents and so on. + * + * @author srowen@google.com (Sean Owen) + * @author dswitkin@google.com (Daniel Switkin) + */ +final class ResultHandler extends Handler { + + private final Intent intent; + private final BarcodeReaderCaptureActivity captureActivity; + + ResultHandler(BarcodeReaderCaptureActivity captureActivity, ParsedReaderResult result) { + this.captureActivity = captureActivity; + this.intent = resultToIntent(result); + } + + private static Intent resultToIntent(ParsedReaderResult result) { + Intent intent = null; + ParsedReaderResultType type = result.getType(); + if (type.equals(ParsedReaderResultType.ADDRESSBOOK)) { + AddressBookDoCoMoParsedResult addressResult = (AddressBookDoCoMoParsedResult) result; + intent = new Intent(Contacts.Intents.Insert.ACTION, Contacts.People.CONTENT_URI); + putExtra(intent, Contacts.Intents.Insert.NAME, addressResult.getName()); + putExtra(intent, Contacts.Intents.Insert.PHONE, addressResult.getPhoneNumbers()); + putExtra(intent, Contacts.Intents.Insert.EMAIL, addressResult.getEmail()); + putExtra(intent, Contacts.Intents.Insert.NOTES, addressResult.getNote()); + putExtra(intent, Contacts.Intents.Insert.POSTAL, addressResult.getAddress()); + } else if (type.equals(ParsedReaderResultType.ADDRESSBOOK_AU)) { + AddressBookAUParsedResult addressResult = (AddressBookAUParsedResult) result; + intent = new Intent(Contacts.Intents.Insert.ACTION, Contacts.People.CONTENT_URI); + putExtra(intent, Contacts.Intents.Insert.NAME, addressResult.getNames()); + putExtra(intent, Contacts.Intents.Insert.PHONE, addressResult.getPhoneNumbers()); + putExtra(intent, Contacts.Intents.Insert.EMAIL, addressResult.getEmails()); + putExtra(intent, Contacts.Intents.Insert.NOTES, addressResult.getNote()); + putExtra(intent, Contacts.Intents.Insert.POSTAL, addressResult.getAddress()); + } else if (type.equals(ParsedReaderResultType.BOOKMARK)) { + // For now, we can only open the browser, and not actually add a bookmark + try { + intent = new Intent(Intent.VIEW_ACTION, new ContentURI(((BookmarkDoCoMoParsedResult) result).getURI())); + } catch (URISyntaxException e) { + } + } else if (type.equals(ParsedReaderResultType.URLTO)) { + try { + intent = new Intent(Intent.VIEW_ACTION, new ContentURI(((URLTOParsedResult) result).getURI())); + } catch (URISyntaxException e) { + } + } else if (type.equals(ParsedReaderResultType.EMAIL)) { + EmailDoCoMoParsedResult emailResult = (EmailDoCoMoParsedResult) result; + try { + intent = new Intent(Intent.SENDTO_ACTION, new ContentURI(emailResult.getTo())); + } catch (URISyntaxException e) { + } + putExtra(intent, "subject", emailResult.getSubject()); + putExtra(intent, "body", emailResult.getBody()); + } else if (type.equals(ParsedReaderResultType.EMAIL_ADDRESS)) { + EmailAddressParsedResult emailResult = (EmailAddressParsedResult) result; + try { + intent = new Intent(Intent.SENDTO_ACTION, new ContentURI(emailResult.getEmailAddress())); + } catch (URISyntaxException e) { + } + } else if (type.equals(ParsedReaderResultType.TEL)) { + TelParsedResult telResult = (TelParsedResult) result; + try { + intent = new Intent(Intent.DIAL_ACTION, new ContentURI("tel:" + telResult.getNumber())); + } catch (URISyntaxException e) { + } + } else if (type.equals(ParsedReaderResultType.GEO)) { + GeoParsedResult geoResult = (GeoParsedResult) result; + try { + intent = new Intent(Intent.VIEW_ACTION, new ContentURI(geoResult.getGeoURI())); + } catch (URISyntaxException e) { + } + } else if (type.equals(ParsedReaderResultType.UPC)) { + UPCParsedResult upcResult = (UPCParsedResult) result; + try { + ContentURI uri = new ContentURI("http://www.upcdatabase.com/item.asp?upc=" + upcResult.getUPC()); + intent = new Intent(Intent.VIEW_ACTION, uri); + } catch (URISyntaxException e) { + } + } else if (type.equals(ParsedReaderResultType.URI)) { + URIParsedResult uriResult = (URIParsedResult) result; + try { + intent = new Intent(Intent.VIEW_ACTION, new ContentURI(uriResult.getURI())); + } catch (URISyntaxException e) { + } + } else if (type.equals(ParsedReaderResultType.ANDROID_INTENT)) { + intent = ((AndroidIntentParsedResult) result).getIntent(); + } + return intent; + } + + @Override + public void handleMessage(Message message) { + if (message.what == R.string.button_yes) { + if (intent != null) { + captureActivity.startActivity(intent); + } + } else { + captureActivity.restartPreview(); + } + } + + Intent getIntent() { + return intent; + } + + private static void putExtra(Intent intent, String key, String value) { + if (value != null && value.length() > 0) { + intent.putExtra(key, value); + } + } + + private static void putExtra(Intent intent, String key, String[] value) { + if (value != null && value.length > 0) { + putExtra(intent, key, value[0]); + } + } + +} diff --git a/android-m3/src/com/google/zxing/client/android/WorkerThread.java b/android-m3/src/com/google/zxing/client/android/WorkerThread.java new file mode 100644 index 000000000..c0f6b3541 --- /dev/null +++ b/android-m3/src/com/google/zxing/client/android/WorkerThread.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Message; +import com.google.zxing.MonochromeBitmapSource; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; + +/** + * This thread does all the heavy lifting, both during preview and for the final capture and + * decoding. That leaves the main thread free to handle UI tasks. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class WorkerThread extends Thread { + + private final CameraSurfaceView surfaceView; + private final CameraManager cameraManager; + private final Handler handler; + private final Object idleLock; + private State state; + + private enum State { + IDLE, + PREVIEW_LOOP, + STILL_AND_DECODE, + DONE + } + + WorkerThread(CameraSurfaceView surfaceView, CameraManager cameraManager, Handler handler) { + this.surfaceView = surfaceView; + this.cameraManager = cameraManager; + this.handler = handler; + this.idleLock = new Object(); + state = State.IDLE; + } + + @Override + public void run() { + while (true) { + switch (state) { + case IDLE: + idle(); + break; + case PREVIEW_LOOP: + surfaceView.capturePreviewAndDraw(); + break; + case STILL_AND_DECODE: + Bitmap bitmap = cameraManager.captureStill(); + Result rawResult; + try { + MonochromeBitmapSource source = new RGBMonochromeBitmapSource(bitmap); + rawResult = new MultiFormatReader().decode(source); + } catch (ReaderException e) { + Message message = Message.obtain(handler, R.id.decoding_failed_message); + message.sendToTarget(); + state = State.PREVIEW_LOOP; + break; + } + Message message = Message.obtain(handler, R.id.decoding_succeeded_message, rawResult); + message.sendToTarget(); + state = State.IDLE; + break; + case DONE: + return; + } + } + } + + public void requestPreviewLoop() { + state = State.PREVIEW_LOOP; + wakeFromIdle(); + } + + public void requestStillAndDecode() { + state = State.STILL_AND_DECODE; + wakeFromIdle(); + } + + public void requestExitAndWait() { + state = State.DONE; + wakeFromIdle(); + try { + join(); + } catch (InterruptedException e) { + } + } + + private void idle() { + try { + synchronized (idleLock) { + idleLock.wait(); + } + } catch (InterruptedException ie) { + // continue + } + } + + private void wakeFromIdle() { + synchronized (idleLock) { + idleLock.notifyAll(); + } + } + +} diff --git a/android-m3/strings.xml.template b/android-m3/strings.xml.template new file mode 100644 index 000000000..e9af99e21 --- /dev/null +++ b/android-m3/strings.xml.template @@ -0,0 +1,35 @@ + + + + Barcode Reader + No + OK + Yes + About... + ZXing Barcode Reader v@VERSION@\nhttp://code.google.com/p/zxing + Sorry, no barcode was found. + About + Barcode Detected + No Barcode Detected + Error + Open Web Page? + Add Contact? + Compose E-mail? + Look Up Barcode Online? + Dial Number? + View In Google Maps? +